From 8ffd5b3a196c53b4b8b9424426420023afd57a06 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 24 Mar 2023 15:13:24 +0100 Subject: some changes --- colorPicker/.gitignore | 1 + colorPicker/build.gradle | 39 + colorPicker/gradle.properties | 19 + colorPicker/proguard-rules.pro | 17 + colorPicker/src/main/AndroidManifest.xml | 1 + .../android/colorpicker/AlphaPatternDrawable.java | 113 +++ .../android/colorpicker/ColorPaletteAdapter.java | 150 ++++ .../android/colorpicker/ColorPanelView.java | 316 +++++++ .../android/colorpicker/ColorPickerDialog.java | 968 ++++++++++++++++++++ .../colorpicker/ColorPickerDialogListener.java | 40 + .../android/colorpicker/ColorPickerView.java | 972 +++++++++++++++++++++ .../android/colorpicker/ColorPreference.java | 239 +++++ .../android/colorpicker/ColorPreferenceCompat.java | 234 +++++ .../android/colorpicker/ColorShape.java | 30 + .../android/colorpicker/DrawingUtils.java | 32 + .../android/colorpicker/NestedGridView.java | 45 + .../src/main/res/drawable-hdpi/cpv_alpha.png | Bin 0 -> 81 bytes .../src/main/res/drawable-xhdpi/cpv_alpha.png | Bin 0 -> 93 bytes .../src/main/res/drawable-xxhdpi/cpv_alpha.png | Bin 0 -> 101 bytes .../src/main/res/drawable/cpv_btn_background.xml | 7 + .../res/drawable/cpv_btn_background_pressed.xml | 14 + .../res/drawable/cpv_ic_arrow_right_black_24dp.xml | 9 + .../src/main/res/drawable/cpv_preset_checked.xml | 9 + .../src/main/res/layout/cpv_color_item_circle.xml | 23 + .../src/main/res/layout/cpv_color_item_square.xml | 23 + .../main/res/layout/cpv_dialog_color_picker.xml | 86 ++ .../src/main/res/layout/cpv_dialog_presets.xml | 89 ++ .../src/main/res/layout/cpv_preference_circle.xml | 6 + .../res/layout/cpv_preference_circle_large.xml | 6 + .../src/main/res/layout/cpv_preference_square.xml | 6 + .../res/layout/cpv_preference_square_large.xml | 6 + colorPicker/src/main/res/values-ar/strings.xml | 22 + colorPicker/src/main/res/values-be/strings.xml | 22 + colorPicker/src/main/res/values-cs/strings.xml | 22 + colorPicker/src/main/res/values-de/strings.xml | 22 + colorPicker/src/main/res/values-es/strings.xml | 9 + colorPicker/src/main/res/values-fr-rFR/strings.xml | 19 + colorPicker/src/main/res/values-it-rIT/strings.xml | 21 + colorPicker/src/main/res/values-iw/strings.xml | 8 + colorPicker/src/main/res/values-ja/strings.xml | 22 + colorPicker/src/main/res/values-nl-rNL/strings.xml | 19 + colorPicker/src/main/res/values-pl/strings.xml | 22 + colorPicker/src/main/res/values-pt/strings.xml | 9 + colorPicker/src/main/res/values-ru/strings.xml | 22 + colorPicker/src/main/res/values-sk/strings.xml | 22 + colorPicker/src/main/res/values-tr/strings.xml | 19 + colorPicker/src/main/res/values-zh/strings.xml | 22 + colorPicker/src/main/res/values/attrs.xml | 38 + colorPicker/src/main/res/values/dimen.xml | 11 + colorPicker/src/main/res/values/ids.xml | 8 + colorPicker/src/main/res/values/strings.xml | 22 + colorPicker/src/main/res/values/styles.xml | 25 + 52 files changed, 3906 insertions(+) create mode 100644 colorPicker/.gitignore create mode 100644 colorPicker/build.gradle create mode 100644 colorPicker/gradle.properties create mode 100644 colorPicker/proguard-rules.pro create mode 100644 colorPicker/src/main/AndroidManifest.xml create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/AlphaPatternDrawable.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPaletteAdapter.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPanelView.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialog.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialogListener.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerView.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPreference.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPreferenceCompat.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorShape.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/DrawingUtils.java create mode 100644 colorPicker/src/main/java/com/jaredrummler/android/colorpicker/NestedGridView.java create mode 100644 colorPicker/src/main/res/drawable-hdpi/cpv_alpha.png create mode 100644 colorPicker/src/main/res/drawable-xhdpi/cpv_alpha.png create mode 100644 colorPicker/src/main/res/drawable-xxhdpi/cpv_alpha.png create mode 100644 colorPicker/src/main/res/drawable/cpv_btn_background.xml create mode 100644 colorPicker/src/main/res/drawable/cpv_btn_background_pressed.xml create mode 100644 colorPicker/src/main/res/drawable/cpv_ic_arrow_right_black_24dp.xml create mode 100644 colorPicker/src/main/res/drawable/cpv_preset_checked.xml create mode 100644 colorPicker/src/main/res/layout/cpv_color_item_circle.xml create mode 100644 colorPicker/src/main/res/layout/cpv_color_item_square.xml create mode 100644 colorPicker/src/main/res/layout/cpv_dialog_color_picker.xml create mode 100644 colorPicker/src/main/res/layout/cpv_dialog_presets.xml create mode 100644 colorPicker/src/main/res/layout/cpv_preference_circle.xml create mode 100644 colorPicker/src/main/res/layout/cpv_preference_circle_large.xml create mode 100644 colorPicker/src/main/res/layout/cpv_preference_square.xml create mode 100644 colorPicker/src/main/res/layout/cpv_preference_square_large.xml create mode 100644 colorPicker/src/main/res/values-ar/strings.xml create mode 100644 colorPicker/src/main/res/values-be/strings.xml create mode 100644 colorPicker/src/main/res/values-cs/strings.xml create mode 100644 colorPicker/src/main/res/values-de/strings.xml create mode 100644 colorPicker/src/main/res/values-es/strings.xml create mode 100644 colorPicker/src/main/res/values-fr-rFR/strings.xml create mode 100644 colorPicker/src/main/res/values-it-rIT/strings.xml create mode 100644 colorPicker/src/main/res/values-iw/strings.xml create mode 100644 colorPicker/src/main/res/values-ja/strings.xml create mode 100644 colorPicker/src/main/res/values-nl-rNL/strings.xml create mode 100644 colorPicker/src/main/res/values-pl/strings.xml create mode 100644 colorPicker/src/main/res/values-pt/strings.xml create mode 100644 colorPicker/src/main/res/values-ru/strings.xml create mode 100644 colorPicker/src/main/res/values-sk/strings.xml create mode 100644 colorPicker/src/main/res/values-tr/strings.xml create mode 100644 colorPicker/src/main/res/values-zh/strings.xml create mode 100644 colorPicker/src/main/res/values/attrs.xml create mode 100644 colorPicker/src/main/res/values/dimen.xml create mode 100644 colorPicker/src/main/res/values/ids.xml create mode 100644 colorPicker/src/main/res/values/strings.xml create mode 100644 colorPicker/src/main/res/values/styles.xml (limited to 'colorPicker') diff --git a/colorPicker/.gitignore b/colorPicker/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/colorPicker/.gitignore @@ -0,0 +1 @@ +/build diff --git a/colorPicker/build.gradle b/colorPicker/build.gradle new file mode 100644 index 000000000..0f3f1c23a --- /dev/null +++ b/colorPicker/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 33 + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 33 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + + +// Add a new configuration to hold your dependencies +configurations { + libConfig +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //noinspection GradleCompatible + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'androidx.preference:preference:1.2.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' + +} + diff --git a/colorPicker/gradle.properties b/colorPicker/gradle.properties new file mode 100644 index 000000000..f81fb845d --- /dev/null +++ b/colorPicker/gradle.properties @@ -0,0 +1,19 @@ +VERSION_NAME=1.1.0 +VERSION_CODE=110 +GROUP=com.jaredrummler +ARTIFACT_ID=colorpicker +POM_NAME=colorpicker +POM_ARTIFACT_ID=colorpicker +POM_PACKAGING=aar +POM_DESCRIPTION=A simply good looking color picker component for Android +POM_URL=https://github.com/jaredrummler/ColorPicker +POM_SCM_URL=https://github.com/jaredrummler/ColorPicker +POM_SCM_CONNECTION=scm:git@github.com:jaredrummler/ColorPicker.git +POM_SCM_DEV_CONNECTION=scm:git@github.com:jaredrummler/ColorPicker.git +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo +POM_DEVELOPER_ID=jaredrummler +POM_DEVELOPER_NAME=Jared Rummler +SNAPSHOT_REPOSITORY_URL=https://oss.sonatype.org/content/repositories/snapshots +RELEASE_REPOSITORY_URL=https://oss.sonatype.org/service/local/staging/deploy/maven2 \ No newline at end of file diff --git a/colorPicker/proguard-rules.pro b/colorPicker/proguard-rules.pro new file mode 100644 index 000000000..677edc8f4 --- /dev/null +++ b/colorPicker/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/colorPicker/src/main/AndroidManifest.xml b/colorPicker/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c48f9fb9d --- /dev/null +++ b/colorPicker/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/AlphaPatternDrawable.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/AlphaPatternDrawable.java new file mode 100644 index 000000000..806ab027c --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/AlphaPatternDrawable.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +/** + * This drawable will draw a simple white and gray chessboard pattern. + * It's the pattern you will often see as a background behind a partly transparent image in many applications. + */ +class AlphaPatternDrawable extends Drawable { + + private final Paint paint = new Paint(); + private final Paint paintWhite = new Paint(); + private final Paint paintGray = new Paint(); + private int rectangleSize = 10; + private int numRectanglesHorizontal; + private int numRectanglesVertical; + + /** + * Bitmap in which the pattern will be cached. + * This is so the pattern will not have to be recreated each time draw() gets called. + * Because recreating the pattern i rather expensive. I will only be recreated if the size changes. + */ + private Bitmap bitmap; + + AlphaPatternDrawable(int rectangleSize) { + this.rectangleSize = rectangleSize; + paintWhite.setColor(0xFFFFFFFF); + paintGray.setColor(0xFFCBCBCB); + } + + @Override + public void draw(Canvas canvas) { + if (bitmap != null && !bitmap.isRecycled()) { + canvas.drawBitmap(bitmap, null, getBounds(), paint); + } + } + + @Override + public int getOpacity() { + return 0; + } + + @Override + public void setAlpha(int alpha) { + throw new UnsupportedOperationException("Alpha is not supported by this drawable."); + } + + @Override + public void setColorFilter(ColorFilter cf) { + throw new UnsupportedOperationException("ColorFilter is not supported by this drawable."); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + int height = bounds.height(); + int width = bounds.width(); + numRectanglesHorizontal = (int) Math.ceil((width / rectangleSize)); + numRectanglesVertical = (int) Math.ceil(height / rectangleSize); + generatePatternBitmap(); + } + + /** + * This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on. + * We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few + * milliseconds + */ + private void generatePatternBitmap() { + if (getBounds().width() <= 0 || getBounds().height() <= 0) { + return; + } + + bitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Rect r = new Rect(); + boolean verticalStartWhite = true; + for (int i = 0; i <= numRectanglesVertical; i++) { + boolean isWhite = verticalStartWhite; + for (int j = 0; j <= numRectanglesHorizontal; j++) { + r.top = i * rectangleSize; + r.left = j * rectangleSize; + r.bottom = r.top + rectangleSize; + r.right = r.left + rectangleSize; + canvas.drawRect(r, isWhite ? paintWhite : paintGray); + isWhite = !isWhite; + } + verticalStartWhite = !verticalStartWhite; + } + } +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPaletteAdapter.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPaletteAdapter.java new file mode 100644 index 000000000..8e960d547 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPaletteAdapter.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; + +import androidx.core.graphics.ColorUtils; + +class ColorPaletteAdapter extends BaseAdapter { + + /*package*/ final OnColorSelectedListener listener; + /*package*/ final int[] colors; + /*package*/ int selectedPosition; + /*package*/ int colorShape; + + ColorPaletteAdapter(OnColorSelectedListener listener, int[] colors, int selectedPosition, + @ColorShape int colorShape) { + this.listener = listener; + this.colors = colors; + this.selectedPosition = selectedPosition; + this.colorShape = colorShape; + } + + @Override + public int getCount() { + return colors.length; + } + + @Override + public Object getItem(int position) { + return colors[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final ViewHolder holder; + if (convertView == null) { + holder = new ViewHolder(parent.getContext()); + convertView = holder.view; + } else { + holder = (ViewHolder) convertView.getTag(); + } + holder.setup(position); + return convertView; + } + + void selectNone() { + selectedPosition = -1; + notifyDataSetChanged(); + } + + interface OnColorSelectedListener { + + void onColorSelected(int color); + } + + private final class ViewHolder { + + View view; + ColorPanelView colorPanelView; + ImageView imageView; + int originalBorderColor; + + ViewHolder(Context context) { + int layoutResId; + if (colorShape == ColorShape.SQUARE) { + layoutResId = R.layout.cpv_color_item_square; + } else { + layoutResId = R.layout.cpv_color_item_circle; + } + view = View.inflate(context, layoutResId, null); + colorPanelView = view.findViewById(R.id.cpv_color_panel_view); + imageView = view.findViewById(R.id.cpv_color_image_view); + originalBorderColor = colorPanelView.getBorderColor(); + view.setTag(this); + } + + void setup(int position) { + int color = colors[position]; + int alpha = Color.alpha(color); + colorPanelView.setColor(color); + imageView.setImageResource(selectedPosition == position ? R.drawable.cpv_preset_checked : 0); + if (alpha != 255) { + if (alpha <= ColorPickerDialog.ALPHA_THRESHOLD) { + colorPanelView.setBorderColor(color | 0xFF000000); + imageView.setColorFilter(/*color | 0xFF000000*/Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + colorPanelView.setBorderColor(originalBorderColor); + imageView.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); + } + } else { + setColorFilter(position); + } + setOnClickListener(position); + } + + private void setOnClickListener(final int position) { + colorPanelView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (selectedPosition != position) { + selectedPosition = position; + notifyDataSetChanged(); + } + listener.onColorSelected(colors[position]); + } + }); + colorPanelView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + colorPanelView.showHint(); + return true; + } + }); + } + + private void setColorFilter(int position) { + if (position == selectedPosition && ColorUtils.calculateLuminance(colors[position]) >= 0.65) { + imageView.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + imageView.setColorFilter(null); + } + } + } +} \ No newline at end of file diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPanelView.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPanelView.java new file mode 100644 index 000000000..ad047682d --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPanelView.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.ColorInt; +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; + +import java.util.Locale; + +/** + * This class draws a panel which which will be filled with a color which can be set. It can be used to show the + * currently selected color which you will get from the {@link ColorPickerView}. + */ +public class ColorPanelView extends View { + + private final static int DEFAULT_BORDER_COLOR = 0xFF6E6E6E; + + private Drawable alphaPattern; + private Paint borderPaint; + private Paint colorPaint; + private Paint alphaPaint; + private Paint originalPaint; + private Rect drawingRect; + private Rect colorRect; + private RectF centerRect = new RectF(); + private boolean showOldColor; + + /* The width in pixels of the border surrounding the color panel. */ + private int borderWidthPx; + private int borderColor = DEFAULT_BORDER_COLOR; + private int color = Color.BLACK; + private int shape; + + public ColorPanelView(Context context) { + this(context, null); + } + + public ColorPanelView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ColorPanelView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs); + } + + @Override + public Parcelable onSaveInstanceState() { + Bundle state = new Bundle(); + state.putParcelable("instanceState", super.onSaveInstanceState()); + state.putInt("color", color); + return state; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle bundle = (Bundle) state; + color = bundle.getInt("color"); + state = bundle.getParcelable("instanceState"); + } + super.onRestoreInstanceState(state); + } + + private void init(Context context, AttributeSet attrs) { + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView); + shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE); + showOldColor = a.getBoolean(R.styleable.ColorPanelView_cpv_showOldColor, false); + if (showOldColor && shape != ColorShape.CIRCLE) { + throw new IllegalStateException("Color preview is only available in circle mode"); + } + borderColor = a.getColor(R.styleable.ColorPanelView_cpv_borderColor, DEFAULT_BORDER_COLOR); + a.recycle(); + if (borderColor == DEFAULT_BORDER_COLOR) { + // If no specific border color has been set we take the default secondary text color as border/slider color. + // Thus it will adopt to theme changes automatically. + final TypedValue value = new TypedValue(); + TypedArray typedArray = + context.obtainStyledAttributes(value.data, new int[]{android.R.attr.textColorSecondary}); + borderColor = typedArray.getColor(0, borderColor); + typedArray.recycle(); + } + borderWidthPx = DrawingUtils.dpToPx(context, 1); + borderPaint = new Paint(); + borderPaint.setAntiAlias(true); + colorPaint = new Paint(); + colorPaint.setAntiAlias(true); + if (showOldColor) { + originalPaint = new Paint(); + } + if (shape == ColorShape.CIRCLE) { + Bitmap bitmap = ((BitmapDrawable) context.getResources().getDrawable(R.drawable.cpv_alpha)).getBitmap(); + alphaPaint = new Paint(); + alphaPaint.setAntiAlias(true); + BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); + alphaPaint.setShader(shader); + } + } + + @Override + protected void onDraw(Canvas canvas) { + borderPaint.setColor(borderColor); + colorPaint.setColor(color); + if (shape == ColorShape.SQUARE) { + if (borderWidthPx > 0) { + canvas.drawRect(drawingRect, borderPaint); + } + if (alphaPattern != null) { + alphaPattern.draw(canvas); + } + canvas.drawRect(colorRect, colorPaint); + } else if (shape == ColorShape.CIRCLE) { + final int outerRadius = getMeasuredWidth() / 2; + if (borderWidthPx > 0) { + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, outerRadius, borderPaint); + } + if (Color.alpha(color) < 255) { + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, outerRadius - borderWidthPx, alphaPaint); + } + if (showOldColor) { + canvas.drawArc(centerRect, 90, 180, true, originalPaint); + canvas.drawArc(centerRect, 270, 180, true, colorPaint); + } else { + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, outerRadius - borderWidthPx, colorPaint); + } + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (shape == ColorShape.SQUARE) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); + } else if (shape == ColorShape.CIRCLE) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (shape == ColorShape.SQUARE || showOldColor) { + drawingRect = new Rect(); + drawingRect.left = getPaddingLeft(); + drawingRect.right = w - getPaddingRight(); + drawingRect.top = getPaddingTop(); + drawingRect.bottom = h - getPaddingBottom(); + if (showOldColor) { + setUpCenterRect(); + } else { + setUpColorRect(); + } + } + } + + private void setUpCenterRect() { + final Rect dRect = drawingRect; + int left = dRect.left + borderWidthPx; + int top = dRect.top + borderWidthPx; + int bottom = dRect.bottom - borderWidthPx; + int right = dRect.right - borderWidthPx; + centerRect = new RectF(left, top, right, bottom); + } + + private void setUpColorRect() { + final Rect dRect = drawingRect; + int left = dRect.left + borderWidthPx; + int top = dRect.top + borderWidthPx; + int bottom = dRect.bottom - borderWidthPx; + int right = dRect.right - borderWidthPx; + colorRect = new Rect(left, top, right, bottom); + alphaPattern = new AlphaPatternDrawable(DrawingUtils.dpToPx(getContext(), 4)); + alphaPattern.setBounds(Math.round(colorRect.left), Math.round(colorRect.top), Math.round(colorRect.right), + Math.round(colorRect.bottom)); + } + + /** + * Get the color currently show by this view. + * + * @return the color value + */ + public int getColor() { + return color; + } + + /** + * Set the color that should be shown by this view. + * + * @param color the color value + */ + public void setColor(int color) { + this.color = color; + invalidate(); + } + + /** + * Set the original color. This is only used for previewing colors. + * + * @param color The original color + */ + public void setOriginalColor(@ColorInt int color) { + if (originalPaint != null) { + originalPaint.setColor(color); + } + } + + /** + * @return the color of the border surrounding the panel. + */ + public int getBorderColor() { + return borderColor; + } + + /** + * Set the color of the border surrounding the panel. + * + * @param color the color value + */ + public void setBorderColor(int color) { + borderColor = color; + invalidate(); + } + + /** + * Get the shape + * + * @return Either {@link ColorShape#SQUARE} or {@link ColorShape#CIRCLE}. + */ + @ColorShape + public int getShape() { + return shape; + } + + /** + * Set the shape. + * + * @param shape Either {@link ColorShape#SQUARE} or {@link ColorShape#CIRCLE}. + */ + public void setShape(@ColorShape int shape) { + this.shape = shape; + invalidate(); + } + + /** + * Show a toast message with the hex color code below the view. + */ + public void showHint() { + final int[] screenPos = new int[2]; + final Rect displayFrame = new Rect(); + getLocationOnScreen(screenPos); + getWindowVisibleDisplayFrame(displayFrame); + final Context context = getContext(); + final int width = getWidth(); + final int height = getHeight(); + final int midy = screenPos[1] + height / 2; + int referenceX = screenPos[0] + width / 2; + if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) { + final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; + referenceX = screenWidth - referenceX; // mirror + } + StringBuilder hint = new StringBuilder("#"); + if (Color.alpha(color) != 255) { + hint.append(Integer.toHexString(color).toUpperCase(Locale.ENGLISH)); + } else { + hint.append(String.format("%06X", 0xFFFFFF & color).toUpperCase(Locale.ENGLISH)); + } + Toast cheatSheet = Toast.makeText(context, hint.toString(), Toast.LENGTH_SHORT); + if (midy < displayFrame.height()) { + // Show along the top; follow action buttons + cheatSheet.setGravity(Gravity.TOP | GravityCompat.END, referenceX, screenPos[1] + height - displayFrame.top); + } else { + // Show along the bottom center + cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height); + } + cheatSheet.show(); + } +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialog.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialog.java new file mode 100644 index 000000000..7cbac4007 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialog.java @@ -0,0 +1,968 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.os.Bundle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.util.Log; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.core.graphics.ColorUtils; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentActivity; + +import java.util.Arrays; +import java.util.Locale; + +/** + *

A dialog to pick a color.

+ * + *

The {@link Activity activity} that shows this dialog should implement {@link ColorPickerDialogListener}

+ * + *

Example usage:

+ * + *
+ *   ColorPickerDialog.newBuilder().show(activity);
+ * 
+ */ +public class ColorPickerDialog extends DialogFragment implements ColorPickerView.OnColorChangedListener, TextWatcher { + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_PRESETS = 1; + /** + * Material design colors used as the default color presets + */ + public static final int[] MATERIAL_COLORS = { + 0xFFF44336, // RED 500 + 0xFFE91E63, // PINK 500 + 0xFFFF2C93, // LIGHT PINK 500 + 0xFF9C27B0, // PURPLE 500 + 0xFF673AB7, // DEEP PURPLE 500 + 0xFF3F51B5, // INDIGO 500 + 0xFF2196F3, // BLUE 500 + 0xFF03A9F4, // LIGHT BLUE 500 + 0xFF00BCD4, // CYAN 500 + 0xFF009688, // TEAL 500 + 0xFF4CAF50, // GREEN 500 + 0xFF8BC34A, // LIGHT GREEN 500 + 0xFFCDDC39, // LIME 500 + 0xFFFFEB3B, // YELLOW 500 + 0xFFFFC107, // AMBER 500 + 0xFFFF9800, // ORANGE 500 + 0xFF795548, // BROWN 500 + 0xFF607D8B, // BLUE GREY 500 + 0xFF9E9E9E, // GREY 500 + }; + static final int ALPHA_THRESHOLD = 165; + private static final String TAG = "ColorPickerDialog"; + private static final String ARG_ID = "id"; + private static final String ARG_TYPE = "dialogType"; + private static final String ARG_COLOR = "color"; + private static final String ARG_ALPHA = "alpha"; + private static final String ARG_PRESETS = "presets"; + private static final String ARG_ALLOW_PRESETS = "allowPresets"; + private static final String ARG_ALLOW_CUSTOM = "allowCustom"; + private static final String ARG_DIALOG_TITLE = "dialogTitle"; + private static final String ARG_SHOW_COLOR_SHADES = "showColorShades"; + private static final String ARG_COLOR_SHAPE = "colorShape"; + private static final String ARG_PRESETS_BUTTON_TEXT = "presetsButtonText"; + private static final String ARG_CUSTOM_BUTTON_TEXT = "customButtonText"; + private static final String ARG_SELECTED_BUTTON_TEXT = "selectedButtonText"; + + ColorPickerDialogListener colorPickerDialogListener; + FrameLayout rootView; + int[] presets; + @ColorInt + int color; + int dialogType; + int dialogId; + boolean showColorShades; + int colorShape; + + // -- PRESETS -------------------------- + ColorPaletteAdapter adapter; + LinearLayout shadesLayout; + SeekBar transparencySeekBar; + TextView transparencyPercText; + + // -- CUSTOM --------------------------- + ColorPickerView colorPicker; + ColorPanelView newColorPanel; + EditText hexEditText; + private final OnTouchListener onPickerTouchListener = new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (v != hexEditText && hexEditText.hasFocus()) { + hexEditText.clearFocus(); + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(hexEditText.getWindowToken(), 0); + hexEditText.clearFocus(); + return true; + } + return false; + } + }; + boolean showAlphaSlider; + private int presetsButtonStringRes; + private boolean fromEditText; + private int customButtonStringRes; + + /** + * Create a new Builder for creating a {@link ColorPickerDialog} instance + * + * @return The {@link Builder builder} to create the {@link ColorPickerDialog}. + */ + public static Builder newBuilder() { + return new Builder(); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + dialogId = getArguments().getInt(ARG_ID); + showAlphaSlider = getArguments().getBoolean(ARG_ALPHA); + showColorShades = getArguments().getBoolean(ARG_SHOW_COLOR_SHADES); + colorShape = getArguments().getInt(ARG_COLOR_SHAPE); + if (savedInstanceState == null) { + color = getArguments().getInt(ARG_COLOR); + dialogType = getArguments().getInt(ARG_TYPE); + } else { + color = savedInstanceState.getInt(ARG_COLOR); + dialogType = savedInstanceState.getInt(ARG_TYPE); + } + + rootView = new FrameLayout(requireActivity()); + if (dialogType == TYPE_CUSTOM) { + rootView.addView(createPickerView()); + } else if (dialogType == TYPE_PRESETS) { + rootView.addView(createPresetsView()); + } + + int selectedButtonStringRes = getArguments().getInt(ARG_SELECTED_BUTTON_TEXT); + if (selectedButtonStringRes == 0) { + selectedButtonStringRes = R.string.cpv_select; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()).setView(rootView) + .setPositiveButton(selectedButtonStringRes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + onColorSelected(color); + } + }); + + int dialogTitleStringRes = getArguments().getInt(ARG_DIALOG_TITLE); + if (dialogTitleStringRes != 0) { + builder.setTitle(dialogTitleStringRes); + } + + presetsButtonStringRes = getArguments().getInt(ARG_PRESETS_BUTTON_TEXT); + customButtonStringRes = getArguments().getInt(ARG_CUSTOM_BUTTON_TEXT); + + int neutralButtonStringRes; + if (dialogType == TYPE_CUSTOM && getArguments().getBoolean(ARG_ALLOW_PRESETS)) { + neutralButtonStringRes = (presetsButtonStringRes != 0 ? presetsButtonStringRes : R.string.cpv_presets); + } else if (dialogType == TYPE_PRESETS && getArguments().getBoolean(ARG_ALLOW_CUSTOM)) { + neutralButtonStringRes = (customButtonStringRes != 0 ? customButtonStringRes : R.string.cpv_custom); + } else { + neutralButtonStringRes = 0; + } + + if (neutralButtonStringRes != 0) { + builder.setNeutralButton(neutralButtonStringRes, null); + } + + return builder.create(); + } + + @Override + public void onStart() { + super.onStart(); + AlertDialog dialog = (AlertDialog) getDialog(); + + // http://stackoverflow.com/a/16972670/1048340 + //noinspection ConstantConditions + dialog.getWindow() + .clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + + // Do not dismiss the dialog when clicking the neutral button. + Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (neutralButton != null) { + neutralButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + rootView.removeAllViews(); + switch (dialogType) { + case TYPE_CUSTOM: + dialogType = TYPE_PRESETS; + ((Button) v).setText(customButtonStringRes != 0 ? customButtonStringRes : R.string.cpv_custom); + rootView.addView(createPresetsView()); + break; + case TYPE_PRESETS: + dialogType = TYPE_CUSTOM; + ((Button) v).setText(presetsButtonStringRes != 0 ? presetsButtonStringRes : R.string.cpv_presets); + rootView.addView(createPickerView()); + } + } + }); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + onDialogDismissed(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putInt(ARG_COLOR, color); + outState.putInt(ARG_TYPE, dialogType); + super.onSaveInstanceState(outState); + } + + /** + * Set the callback. + *

+ * Note: The preferred way to handle the callback is to have the calling Activity implement + * {@link ColorPickerDialogListener} as this will not survive an orientation change. + * + * @param colorPickerDialogListener The callback invoked when a color is selected or the dialog is dismissed. + */ + public void setColorPickerDialogListener(ColorPickerDialogListener colorPickerDialogListener) { + this.colorPickerDialogListener = colorPickerDialogListener; + } + + // region Custom Picker + + View createPickerView() { + View contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null); + colorPicker = contentView.findViewById(R.id.cpv_color_picker_view); + ColorPanelView oldColorPanel = contentView.findViewById(R.id.cpv_color_panel_old); + newColorPanel = contentView.findViewById(R.id.cpv_color_panel_new); + ImageView arrowRight = contentView.findViewById(R.id.cpv_arrow_right); + hexEditText = contentView.findViewById(R.id.cpv_hex); + + try { + final TypedValue value = new TypedValue(); + TypedArray typedArray = + getActivity().obtainStyledAttributes(value.data, new int[]{android.R.attr.textColorPrimary}); + int arrowColor = typedArray.getColor(0, Color.BLACK); + typedArray.recycle(); + arrowRight.setColorFilter(arrowColor); + } catch (Exception ignored) { + } + + colorPicker.setAlphaSliderVisible(showAlphaSlider); + oldColorPanel.setColor(getArguments().getInt(ARG_COLOR)); + colorPicker.setColor(color, true); + newColorPanel.setColor(color); + setHex(color); + + if (!showAlphaSlider) { + hexEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(6)}); + } + + newColorPanel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (newColorPanel.getColor() == color) { + onColorSelected(color); + dismiss(); + } + } + }); + + contentView.setOnTouchListener(onPickerTouchListener); + colorPicker.setOnColorChangedListener(this); + hexEditText.addTextChangedListener(this); + + hexEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(hexEditText, InputMethodManager.SHOW_IMPLICIT); + } + } + }); + + return contentView; + } + + @Override + public void onColorChanged(int newColor) { + color = newColor; + if (newColorPanel != null) { + newColorPanel.setColor(newColor); + } + if (!fromEditText && hexEditText != null) { + setHex(newColor); + if (hexEditText.hasFocus()) { + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(hexEditText.getWindowToken(), 0); + hexEditText.clearFocus(); + } + } + fromEditText = false; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + if (hexEditText.isFocused()) { + int color = parseColorString(s.toString()); + if (color != colorPicker.getColor()) { + fromEditText = true; + colorPicker.setColor(color, true); + } + } + } + + private void setHex(int color) { + if (showAlphaSlider) { + hexEditText.setText(String.format("%08X", (color))); + } else { + hexEditText.setText(String.format("%06X", (0xFFFFFF & color))); + } + } + + private int parseColorString(String colorString) throws NumberFormatException { + int a, r, g, b = 0; + if (colorString.startsWith("#")) { + colorString = colorString.substring(1); + } + if (colorString.length() == 0) { + r = 0; + a = 255; + g = 0; + } else if (colorString.length() <= 2) { + a = 255; + r = 0; + b = Integer.parseInt(colorString, 16); + g = 0; + } else if (colorString.length() == 3) { + a = 255; + r = Integer.parseInt(colorString.substring(0, 1), 16); + g = Integer.parseInt(colorString.substring(1, 2), 16); + b = Integer.parseInt(colorString.substring(2, 3), 16); + } else if (colorString.length() == 4) { + a = 255; + r = Integer.parseInt(colorString.substring(0, 2), 16); + g = r; + r = 0; + b = Integer.parseInt(colorString.substring(2, 4), 16); + } else if (colorString.length() == 5) { + a = 255; + r = Integer.parseInt(colorString.substring(0, 1), 16); + g = Integer.parseInt(colorString.substring(1, 3), 16); + b = Integer.parseInt(colorString.substring(3, 5), 16); + } else if (colorString.length() == 6) { + a = 255; + r = Integer.parseInt(colorString.substring(0, 2), 16); + g = Integer.parseInt(colorString.substring(2, 4), 16); + b = Integer.parseInt(colorString.substring(4, 6), 16); + } else if (colorString.length() == 7) { + a = Integer.parseInt(colorString.substring(0, 1), 16); + r = Integer.parseInt(colorString.substring(1, 3), 16); + g = Integer.parseInt(colorString.substring(3, 5), 16); + b = Integer.parseInt(colorString.substring(5, 7), 16); + } else if (colorString.length() == 8) { + a = Integer.parseInt(colorString.substring(0, 2), 16); + r = Integer.parseInt(colorString.substring(2, 4), 16); + g = Integer.parseInt(colorString.substring(4, 6), 16); + b = Integer.parseInt(colorString.substring(6, 8), 16); + } else { + b = -1; + g = -1; + r = -1; + a = -1; + } + return Color.argb(a, r, g, b); + } + + // -- endregion -- + + // region Presets Picker + + View createPresetsView() { + View contentView = View.inflate(getActivity(), R.layout.cpv_dialog_presets, null); + shadesLayout = contentView.findViewById(R.id.shades_layout); + transparencySeekBar = contentView.findViewById(R.id.transparency_seekbar); + transparencyPercText = contentView.findViewById(R.id.transparency_text); + GridView gridView = contentView.findViewById(R.id.gridView); + + loadPresets(); + + if (showColorShades) { + createColorShades(color); + } else { + shadesLayout.setVisibility(View.GONE); + contentView.findViewById(R.id.shades_divider).setVisibility(View.GONE); + } + + adapter = new ColorPaletteAdapter(new ColorPaletteAdapter.OnColorSelectedListener() { + @Override + public void onColorSelected(int newColor) { + if (color == newColor) { + // Double tab selects the color + ColorPickerDialog.this.onColorSelected(color); + dismiss(); + return; + } + color = newColor; + if (showColorShades) { + createColorShades(color); + } + } + }, presets, getSelectedItemPosition(), colorShape); + + gridView.setAdapter(adapter); + + if (showAlphaSlider) { + setupTransparency(); + } else { + contentView.findViewById(R.id.transparency_layout).setVisibility(View.GONE); + contentView.findViewById(R.id.transparency_title).setVisibility(View.GONE); + } + + return contentView; + } + + private void loadPresets() { + int alpha = Color.alpha(color); + presets = getArguments().getIntArray(ARG_PRESETS); + if (presets == null) presets = MATERIAL_COLORS; + boolean isMaterialColors = presets == MATERIAL_COLORS; + presets = Arrays.copyOf(presets, presets.length); // don't update the original array when modifying alpha + if (alpha != 255) { + // add alpha to the presets + for (int i = 0; i < presets.length; i++) { + int color = presets[i]; + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + presets[i] = Color.argb(alpha, red, green, blue); + } + } + presets = unshiftIfNotExists(presets, color); + int initialColor = getArguments().getInt(ARG_COLOR); + if (initialColor != color) { + // The user clicked a color and a configuration change occurred. Make sure the initial color is in the presets + presets = unshiftIfNotExists(presets, initialColor); + } + if (isMaterialColors && presets.length == 19) { + // Add black to have a total of 20 colors if the current color is in the material color palette + presets = pushIfNotExists(presets, Color.argb(alpha, 0, 0, 0)); + } + } + + void createColorShades(@ColorInt final int color) { + final int[] colorShades = getColorShades(color); + + if (shadesLayout.getChildCount() != 0) { + for (int i = 0; i < shadesLayout.getChildCount(); i++) { + FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i); + final ColorPanelView cpv = layout.findViewById(R.id.cpv_color_panel_view); + ImageView iv = layout.findViewById(R.id.cpv_color_image_view); + cpv.setColor(colorShades[i]); + cpv.setTag(false); + iv.setImageDrawable(null); + } + return; + } + + final int horizontalPadding = getResources().getDimensionPixelSize(R.dimen.cpv_item_horizontal_padding); + + for (final int colorShade : colorShades) { + int layoutResId; + if (colorShape == ColorShape.SQUARE) { + layoutResId = R.layout.cpv_color_item_square; + } else { + layoutResId = R.layout.cpv_color_item_circle; + } + + final View view = View.inflate(getActivity(), layoutResId, null); + final ColorPanelView colorPanelView = view.findViewById(R.id.cpv_color_panel_view); + + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) colorPanelView.getLayoutParams(); + params.leftMargin = params.rightMargin = horizontalPadding; + colorPanelView.setLayoutParams(params); + colorPanelView.setColor(colorShade); + shadesLayout.addView(view); + + colorPanelView.post(new Runnable() { + @Override + public void run() { + // The color is black when rotating the dialog. This is a dirty fix. WTF!? + colorPanelView.setColor(colorShade); + } + }); + + colorPanelView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getTag() instanceof Boolean && (Boolean) v.getTag()) { + onColorSelected(ColorPickerDialog.this.color); + dismiss(); + return; // already selected + } + ColorPickerDialog.this.color = colorPanelView.getColor(); + adapter.selectNone(); + for (int i = 0; i < shadesLayout.getChildCount(); i++) { + FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i); + ColorPanelView cpv = layout.findViewById(R.id.cpv_color_panel_view); + ImageView iv = layout.findViewById(R.id.cpv_color_image_view); + iv.setImageResource(cpv == v ? R.drawable.cpv_preset_checked : 0); + if (cpv == v && ColorUtils.calculateLuminance(cpv.getColor()) >= 0.65 + || Color.alpha(cpv.getColor()) <= ALPHA_THRESHOLD) { + iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + iv.setColorFilter(null); + } + cpv.setTag(cpv == v); + } + } + }); + colorPanelView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + colorPanelView.showHint(); + return true; + } + }); + } + } + + private void onColorSelected(int color) { + if (colorPickerDialogListener != null) { + Log.w(TAG, "Using deprecated listener which may be remove in future releases"); + colorPickerDialogListener.onColorSelected(dialogId, color); + return; + } + Activity activity = getActivity(); + if (activity instanceof ColorPickerDialogListener) { + ((ColorPickerDialogListener) activity).onColorSelected(dialogId, color); + } else { + throw new IllegalStateException("The activity must implement ColorPickerDialogListener"); + } + } + + private void onDialogDismissed() { + if (colorPickerDialogListener != null) { + Log.w(TAG, "Using deprecated listener which may be remove in future releases"); + colorPickerDialogListener.onDialogDismissed(dialogId); + return; + } + Activity activity = getActivity(); + if (activity instanceof ColorPickerDialogListener) { + ((ColorPickerDialogListener) activity).onDialogDismissed(dialogId); + } + } + + private int shadeColor(@ColorInt int color, double percent) { + String hex = String.format("#%06X", (0xFFFFFF & color)); + long f = Long.parseLong(hex.substring(1), 16); + double t = percent < 0 ? 0 : 255; + double p = percent < 0 ? percent * -1 : percent; + long R = f >> 16; + long G = f >> 8 & 0x00FF; + long B = f & 0x0000FF; + int alpha = Color.alpha(color); + int red = (int) (Math.round((t - R) * p) + R); + int green = (int) (Math.round((t - G) * p) + G); + int blue = (int) (Math.round((t - B) * p) + B); + return Color.argb(alpha, red, green, blue); + } + + private int[] getColorShades(@ColorInt int color) { + return new int[]{ + shadeColor(color, 0.9), shadeColor(color, 0.7), shadeColor(color, 0.5), shadeColor(color, 0.333), + shadeColor(color, 0.166), shadeColor(color, -0.125), shadeColor(color, -0.25), shadeColor(color, -0.375), + shadeColor(color, -0.5), shadeColor(color, -0.675), shadeColor(color, -0.7), shadeColor(color, -0.775), + }; + } + + private void setupTransparency() { + int progress = 255 - Color.alpha(color); + transparencySeekBar.setMax(255); + transparencySeekBar.setProgress(progress); + int percentage = (int) ((double) progress * 100 / 255); + transparencyPercText.setText(String.format(Locale.ENGLISH, "%d%%", percentage)); + transparencySeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + int percentage = (int) ((double) progress * 100 / 255); + transparencyPercText.setText(String.format(Locale.ENGLISH, "%d%%", percentage)); + int alpha = 255 - progress; + // update items in GridView: + for (int i = 0; i < adapter.colors.length; i++) { + int color = adapter.colors[i]; + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + adapter.colors[i] = Color.argb(alpha, red, green, blue); + } + adapter.notifyDataSetChanged(); + // update shades: + for (int i = 0; i < shadesLayout.getChildCount(); i++) { + FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i); + ColorPanelView cpv = layout.findViewById(R.id.cpv_color_panel_view); + ImageView iv = layout.findViewById(R.id.cpv_color_image_view); + if (layout.getTag() == null) { + // save the original border color + layout.setTag(cpv.getBorderColor()); + } + int color = cpv.getColor(); + color = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + if (alpha <= ALPHA_THRESHOLD) { + cpv.setBorderColor(color | 0xFF000000); + } else { + cpv.setBorderColor((int) layout.getTag()); + } + if (cpv.getTag() != null && (Boolean) cpv.getTag()) { + // The alpha changed on the selected shaded color. Update the checkmark color filter. + if (alpha <= ALPHA_THRESHOLD) { + iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + if (ColorUtils.calculateLuminance(color) >= 0.65) { + iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + iv.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); + } + } + } + cpv.setColor(color); + } + // update color: + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + color = Color.argb(alpha, red, green, blue); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + } + + private int[] unshiftIfNotExists(int[] array, int value) { + boolean present = false; + for (int i : array) { + if (i == value) { + present = true; + break; + } + } + if (!present) { + int[] newArray = new int[array.length + 1]; + newArray[0] = value; + System.arraycopy(array, 0, newArray, 1, newArray.length - 1); + return newArray; + } + return array; + } + + private int[] pushIfNotExists(int[] array, int value) { + boolean present = false; + for (int i : array) { + if (i == value) { + present = true; + break; + } + } + if (!present) { + int[] newArray = new int[array.length + 1]; + newArray[newArray.length - 1] = value; + System.arraycopy(array, 0, newArray, 0, newArray.length - 1); + return newArray; + } + return array; + } + + private int getSelectedItemPosition() { + for (int i = 0; i < presets.length; i++) { + if (presets[i] == color) { + return i; + } + } + return -1; + } + + // endregion + + // region Builder + + @IntDef({TYPE_CUSTOM, TYPE_PRESETS}) + public @interface DialogType { + + } + + public static final class Builder { + + ColorPickerDialogListener colorPickerDialogListener; + @StringRes + int dialogTitle = R.string.cpv_default_title; + @StringRes + int presetsButtonText = R.string.cpv_presets; + @StringRes + int customButtonText = R.string.cpv_custom; + @StringRes + int selectedButtonText = R.string.cpv_select; + @DialogType + int dialogType = TYPE_PRESETS; + int[] presets = MATERIAL_COLORS; + @ColorInt + int color = Color.BLACK; + int dialogId = 0; + boolean showAlphaSlider = false; + boolean allowPresets = true; + boolean allowCustom = true; + boolean showColorShades = true; + @ColorShape + int colorShape = ColorShape.CI