diff --git a/app/build.gradle b/app/build.gradle index 5ecdaee562..80980a279c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -580,7 +580,8 @@ dependencies { implementation "io.noties.markwon:html:$markwon_version" // // https://github.com/QuadFlask/colorpicker - implementation "com.github.QuadFlask:colorpicker:$colorpicker_version" + //implementation "com.github.QuadFlask:colorpicker:$colorpicker_version" + implementation project(':colorpicker') // https://github.com/EverythingMe/overscroll-decor // https://search.maven.org/artifact/io.github.everythingme/overscroll-decor-android diff --git a/colorpicker/.gitignore b/colorpicker/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/colorpicker/.gitignore @@ -0,0 +1 @@ +/build diff --git a/colorpicker/build.gradle b/colorpicker/build.gradle new file mode 100644 index 0000000000..e290c2b4a4 --- /dev/null +++ b/colorpicker/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.0" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 29 + versionCode 17 + versionName "0.0.15" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.1.0' +} diff --git a/colorpicker/proguard-rules.pro b/colorpicker/proguard-rules.pro new file mode 100644 index 0000000000..73f713701f --- /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 /Users/flask/Documents/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 0000000000..804b8ac15e --- /dev/null +++ b/colorpicker/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/colorpicker/src/main/java/com/flask/colorpicker/ColorCircle.java b/colorpicker/src/main/java/com/flask/colorpicker/ColorCircle.java new file mode 100644 index 0000000000..ba28baa62c --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/ColorCircle.java @@ -0,0 +1,54 @@ +package com.flask.colorpicker; + +import android.graphics.Color; + +public class ColorCircle { + private float x, y; + private float[] hsv = new float[3]; + private float[] hsvClone; + private int color; + + public ColorCircle(float x, float y, float[] hsv) { + set(x, y, hsv); + } + + public double sqDist(float x, float y) { + double dx = this.x - x; + double dy = this.y - y; + return dx * dx + dy * dy; + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public float[] getHsv() { + return hsv; + } + + public float[] getHsvWithLightness(float lightness) { + if (hsvClone == null) + hsvClone = hsv.clone(); + hsvClone[0] = hsv[0]; + hsvClone[1] = hsv[1]; + hsvClone[2] = lightness; + return hsvClone; + } + + public void set(float x, float y, float[] hsv) { + this.x = x; + this.y = y; + this.hsv[0] = hsv[0]; + this.hsv[1] = hsv[1]; + this.hsv[2] = hsv[2]; + this.color = Color.HSVToColor(this.hsv); + } + + public int getColor() { + return color; + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/ColorCircleDrawable.java b/colorpicker/src/main/java/com/flask/colorpicker/ColorCircleDrawable.java new file mode 100644 index 0000000000..9284f063e8 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/ColorCircleDrawable.java @@ -0,0 +1,39 @@ +package com.flask.colorpicker; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.ColorDrawable; + +import com.flask.colorpicker.builder.PaintBuilder; + +public class ColorCircleDrawable extends ColorDrawable { + private float strokeWidth; + private Paint strokePaint = PaintBuilder.newPaint().style(Paint.Style.STROKE).stroke(strokeWidth).color(0xff9e9e9e).build(); + private Paint fillPaint = PaintBuilder.newPaint().style(Paint.Style.FILL).color(0).build(); + private Paint fillBackPaint = PaintBuilder.newPaint().shader(PaintBuilder.createAlphaPatternShader(26)).build(); + + public ColorCircleDrawable(int color) { + super(color); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawColor(0); + + int width = canvas.getWidth(); + float radius = width / 2f; + strokeWidth = radius / 8f; + + this.strokePaint.setStrokeWidth(strokeWidth); + this.fillPaint.setColor(getColor()); + canvas.drawCircle(radius, radius, radius - strokeWidth, fillBackPaint); + canvas.drawCircle(radius, radius, radius - strokeWidth, fillPaint); + canvas.drawCircle(radius, radius, radius - strokeWidth, strokePaint); + } + + @Override + public void setColor(int color) { + super.setColor(color); + invalidateSelf(); + } +} diff --git a/colorpicker/src/main/java/com/flask/colorpicker/ColorPickerPreference.java b/colorpicker/src/main/java/com/flask/colorpicker/ColorPickerPreference.java new file mode 100644 index 0000000000..6ef7b590a8 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/ColorPickerPreference.java @@ -0,0 +1,155 @@ +package com.flask.colorpicker; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.preference.Preference; +import androidx.annotation.NonNull; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; + +import com.flask.colorpicker.builder.ColorPickerClickListener; +import com.flask.colorpicker.builder.ColorPickerDialogBuilder; + +public class ColorPickerPreference extends Preference { + + protected boolean alphaSlider; + protected boolean lightSlider; + protected boolean border; + + protected int selectedColor = 0; + + protected ColorPickerView.WHEEL_TYPE wheelType; + protected int density; + + private boolean pickerColorEdit; + private String pickerTitle; + private String pickerButtonCancel; + private String pickerButtonOk; + + protected ImageView colorIndicator; + + public ColorPickerPreference(Context context) { + super(context); + } + + public ColorPickerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + initWith(context, attrs); + } + + public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initWith(context, attrs); + } + + private void initWith(Context context, AttributeSet attrs) { + final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerPreference); + + try { + alphaSlider = typedArray.getBoolean(R.styleable.ColorPickerPreference_alphaSlider, false); + lightSlider = typedArray.getBoolean(R.styleable.ColorPickerPreference_lightnessSlider, false); + border = typedArray.getBoolean(R.styleable.ColorPickerPreference_border, true); + + density = typedArray.getInt(R.styleable.ColorPickerPreference_density, 8); + wheelType = ColorPickerView.WHEEL_TYPE.indexOf(typedArray.getInt(R.styleable.ColorPickerPreference_wheelType, 0)); + + selectedColor = typedArray.getInt(R.styleable.ColorPickerPreference_initialColor, 0xffffffff); + + pickerColorEdit = typedArray.getBoolean(R.styleable.ColorPickerPreference_pickerColorEdit, true); + pickerTitle = typedArray.getString(R.styleable.ColorPickerPreference_pickerTitle); + if (pickerTitle==null) + pickerTitle = "Choose color"; + + pickerButtonCancel = typedArray.getString(R.styleable.ColorPickerPreference_pickerButtonCancel); + if (pickerButtonCancel==null) + pickerButtonCancel = "cancel"; + + pickerButtonOk = typedArray.getString(R.styleable.ColorPickerPreference_pickerButtonOk); + if (pickerButtonOk==null) + pickerButtonOk = "ok"; + + } finally { + typedArray.recycle(); + } + + setWidgetLayoutResource(R.layout.color_widget); + } + + + @Override + protected void onBindView(@NonNull View view) { + super.onBindView(view); + + int tmpColor = isEnabled() + ? selectedColor + : darken(selectedColor, .5f); + + colorIndicator = (ImageView) view.findViewById(R.id.color_indicator); + + ColorCircleDrawable colorChoiceDrawable = null; + Drawable currentDrawable = colorIndicator.getDrawable(); + if (currentDrawable != null && currentDrawable instanceof ColorCircleDrawable) + colorChoiceDrawable = (ColorCircleDrawable) currentDrawable; + + if (colorChoiceDrawable == null) + colorChoiceDrawable = new ColorCircleDrawable(tmpColor); + + colorIndicator.setImageDrawable(colorChoiceDrawable); + } + + public void setValue(int value) { + if (callChangeListener(value)) { + selectedColor = value; + persistInt(value); + notifyChanged(); + } + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValue(restoreValue ? getPersistedInt(0) : (Integer) defaultValue); + } + + @Override + protected void onClick() { + ColorPickerDialogBuilder builder = ColorPickerDialogBuilder + .with(getContext()) + .setTitle(pickerTitle) + .initialColor(selectedColor) + .showBorder(border) + .wheelType(wheelType) + .density(density) + .showColorEdit(pickerColorEdit) + .setPositiveButton(pickerButtonOk, new ColorPickerClickListener() { + @Override + public void onClick(DialogInterface dialog, int selectedColorFromPicker, Integer[] allColors) { + setValue(selectedColorFromPicker); + } + }) + .setNegativeButton(pickerButtonCancel, null); + + if (!alphaSlider && !lightSlider) builder.noSliders(); + else if (!alphaSlider) builder.lightnessSliderOnly(); + else if (!lightSlider) builder.alphaSliderOnly(); + + builder + .build() + .show(); + } + + public static int darken(int color, float factor) { + int a = Color.alpha(color); + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + + return Color.argb(a, + Math.max((int)(r * factor), 0), + Math.max((int)(g * factor), 0), + Math.max((int)(b * factor), 0)); + } +} diff --git a/colorpicker/src/main/java/com/flask/colorpicker/ColorPickerView.java b/colorpicker/src/main/java/com/flask/colorpicker/ColorPickerView.java new file mode 100644 index 0000000000..bad746c0c4 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/ColorPickerView.java @@ -0,0 +1,572 @@ +package com.flask.colorpicker; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.flask.colorpicker.builder.ColorWheelRendererBuilder; +import com.flask.colorpicker.builder.PaintBuilder; +import com.flask.colorpicker.renderer.ColorWheelRenderOption; +import com.flask.colorpicker.renderer.ColorWheelRenderer; +import com.flask.colorpicker.slider.AlphaSlider; +import com.flask.colorpicker.slider.LightnessSlider; + +import java.util.ArrayList; + +public class ColorPickerView extends View { + private static final float STROKE_RATIO = 1.5f; + + private Bitmap colorWheel; + private Canvas colorWheelCanvas; + private Bitmap currentColor; + private Canvas currentColorCanvas; + private boolean showBorder; + private int density = 8; + + private float lightness = 1; + private float alpha = 1; + private int backgroundColor = 0x00000000; + + private Integer initialColors[] = new Integer[]{null, null, null, null, null}; + private int colorSelection = 0; + private Integer initialColor; + private Integer pickerColorEditTextColor; + private Paint colorWheelFill = PaintBuilder.newPaint().color(0).build(); + private Paint selectorStroke = PaintBuilder.newPaint().color(0).build(); + private Paint alphaPatternPaint = PaintBuilder.newPaint().build(); + private ColorCircle currentColorCircle; + + private ArrayList colorChangedListeners = new ArrayList<>(); + private ArrayList listeners = new ArrayList<>(); + + private LightnessSlider lightnessSlider; + private AlphaSlider alphaSlider; + private EditText colorEdit; + private TextWatcher colorTextChange = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + try { + int color = Color.parseColor(s.toString()); + + // set the color without changing the edit text preventing stack overflow + setColor(color, false); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + }; + private LinearLayout colorPreview; + + private ColorWheelRenderer renderer; + + private int alphaSliderViewId, lightnessSliderViewId; + + public ColorPickerView(Context context) { + super(context); + initWith(context, null); + } + + public ColorPickerView(Context context, AttributeSet attrs) { + super(context, attrs); + initWith(context, attrs); + } + + public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initWith(context, attrs); + } + + @TargetApi(21) + public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initWith(context, attrs); + } + + private void initWith(Context context, AttributeSet attrs) { + final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerPreference); + + density = typedArray.getInt(R.styleable.ColorPickerPreference_density, 10); + initialColor = typedArray.getInt(R.styleable.ColorPickerPreference_initialColor, 0xffffffff); + + pickerColorEditTextColor = typedArray.getInt(R.styleable.ColorPickerPreference_pickerColorEditTextColor, 0xffffffff); + + WHEEL_TYPE wheelType = WHEEL_TYPE.indexOf(typedArray.getInt(R.styleable.ColorPickerPreference_wheelType, 0)); + ColorWheelRenderer renderer = ColorWheelRendererBuilder.getRenderer(wheelType); + + alphaSliderViewId = typedArray.getResourceId(R.styleable.ColorPickerPreference_alphaSliderView, 0); + lightnessSliderViewId = typedArray.getResourceId(R.styleable.ColorPickerPreference_lightnessSliderView, 0); + + setRenderer(renderer); + setDensity(density); + setInitialColor(initialColor, true); + + typedArray.recycle(); + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + updateColorWheel(); + currentColorCircle = findNearestByColor(initialColor); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (alphaSliderViewId != 0) + setAlphaSlider((AlphaSlider) getRootView().findViewById(alphaSliderViewId)); + if (lightnessSliderViewId != 0) + setLightnessSlider((LightnessSlider) getRootView().findViewById(lightnessSliderViewId)); + + updateColorWheel(); + currentColorCircle = findNearestByColor(initialColor); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateColorWheel(); + } + + private void updateColorWheel() { + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + + if (height < width) + width = height; + if (width <= 0) + return; + if (colorWheel == null || colorWheel.getWidth() != width) { + colorWheel = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888); + colorWheelCanvas = new Canvas(colorWheel); + alphaPatternPaint.setShader(PaintBuilder.createAlphaPatternShader(26)); + } + if (currentColor == null || currentColor.getWidth() != width) { + currentColor = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888); + currentColorCanvas = new Canvas(currentColor); + } + drawColorWheel(); + invalidate(); + } + + private void drawColorWheel() { + colorWheelCanvas.drawColor(0, PorterDuff.Mode.CLEAR); + currentColorCanvas.drawColor(0, PorterDuff.Mode.CLEAR); + + if (renderer == null) return; + + float half = colorWheelCanvas.getWidth() / 2f; + float strokeWidth = STROKE_RATIO * (1f + ColorWheelRenderer.GAP_PERCENTAGE); + float maxRadius = half - strokeWidth - half / density; + float cSize = maxRadius / (density - 1) / 2; + + ColorWheelRenderOption colorWheelRenderOption = renderer.getRenderOption(); + colorWheelRenderOption.density = this.density; + colorWheelRenderOption.maxRadius = maxRadius; + colorWheelRenderOption.cSize = cSize; + colorWheelRenderOption.strokeWidth = strokeWidth; + colorWheelRenderOption.alpha = alpha; + colorWheelRenderOption.lightness = lightness; + colorWheelRenderOption.targetCanvas = colorWheelCanvas; + + renderer.initWith(colorWheelRenderOption); + renderer.draw(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int width = 0; + if (widthMode == MeasureSpec.UNSPECIFIED) + width = widthMeasureSpec; + else if (widthMode == MeasureSpec.AT_MOST) + width = MeasureSpec.getSize(widthMeasureSpec); + else if (widthMode == MeasureSpec.EXACTLY) + width = MeasureSpec.getSize(widthMeasureSpec); + + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int height = 0; + if (heightMode == MeasureSpec.UNSPECIFIED) + height = heightMeasureSpec; + else if (heightMode == MeasureSpec.AT_MOST) + height = MeasureSpec.getSize(heightMeasureSpec); + else if (heightMode == MeasureSpec.EXACTLY) + height = MeasureSpec.getSize(heightMeasureSpec); + int squareDimen = width; + if (height < width) + squareDimen = height; + setMeasuredDimension(squareDimen, squareDimen); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: { + int lastSelectedColor = getSelectedColor(); + currentColorCircle = findNearestByPosition(event.getX(), event.getY()); + int selectedColor = getSelectedColor(); + + callOnColorChangedListeners(lastSelectedColor, selectedColor); + + initialColor = selectedColor; + setColorToSliders(selectedColor); + updateColorWheel(); + invalidate(); + break; + } + case MotionEvent.ACTION_UP: { + int selectedColor = getSelectedColor(); + if (listeners != null) { + for (OnColorSelectedListener listener : listeners) { + try { + listener.onColorSelected(selectedColor); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + setColorToSliders(selectedColor); + setColorText(selectedColor); + setColorPreviewColor(selectedColor); + invalidate(); + break; + } + } + return true; + } + + protected void callOnColorChangedListeners(int oldColor, int newColor) { + if (colorChangedListeners != null && oldColor != newColor) { + for (OnColorChangedListener listener : colorChangedListeners) { + try { + listener.onColorChanged(newColor); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawColor(backgroundColor); + + float maxRadius = canvas.getWidth() / (1f + ColorWheelRenderer.GAP_PERCENTAGE); + float size = maxRadius / density / 2; + if (colorWheel != null && currentColorCircle != null) { + colorWheelFill.setColor(Color.HSVToColor(currentColorCircle.getHsvWithLightness(this.lightness))); + colorWheelFill.setAlpha((int) (alpha * 0xff)); + + // a separate canvas is used to erase an issue with the alpha pattern around the edges + // draw circle slightly larger than it needs to be, then erase edges to proper dimensions + currentColorCanvas.drawCircle(currentColorCircle.getX(), currentColorCircle.getY(), size + 4, alphaPatternPaint); + currentColorCanvas.drawCircle(currentColorCircle.getX(), currentColorCircle.getY(), size + 4, colorWheelFill); + + selectorStroke = PaintBuilder.newPaint().color(0xffffffff).style(Paint.Style.STROKE).stroke(size * (STROKE_RATIO - 1)).xPerMode(PorterDuff.Mode.CLEAR).build(); + + if (showBorder) colorWheelCanvas.drawCircle(currentColorCircle.getX(), currentColorCircle.getY(), size + (selectorStroke.getStrokeWidth() / 2f), selectorStroke); + canvas.drawBitmap(colorWheel, 0, 0, null); + + currentColorCanvas.drawCircle(currentColorCircle.getX(), currentColorCircle.getY(), size + (selectorStroke.getStrokeWidth() / 2f), selectorStroke); + canvas.drawBitmap(currentColor, 0, 0, null); + } + } + + private ColorCircle findNearestByPosition(float x, float y) { + ColorCircle near = null; + double minDist = Double.MAX_VALUE; + + for (ColorCircle colorCircle : renderer.getColorCircleList()) { + double dist = colorCircle.sqDist(x, y); + if (minDist > dist) { + minDist = dist; + near = colorCircle; + } + } + + return near; + } + + private ColorCircle findNearestByColor(int color) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + ColorCircle near = null; + double minDiff = Double.MAX_VALUE; + double x = hsv[1] * Math.cos(hsv[0] * Math.PI / 180); + double y = hsv[1] * Math.sin(hsv[0] * Math.PI / 180); + + for (ColorCircle colorCircle : renderer.getColorCircleList()) { + float[] hsv1 = colorCircle.getHsv(); + double x1 = hsv1[1] * Math.cos(hsv1[0] * Math.PI / 180); + double y1 = hsv1[1] * Math.sin(hsv1[0] * Math.PI / 180); + double dx = x - x1; + double dy = y - y1; + double dist = dx * dx + dy * dy; + if (dist < minDiff) { + minDiff = dist; + near = colorCircle; + } + } + + return near; + } + + public int getSelectedColor() { + int color = 0; + if (currentColorCircle != null) + color = Utils.colorAtLightness(currentColorCircle.getColor(), this.lightness); + return Utils.adjustAlpha(this.alpha, color); + } + + public Integer[] getAllColors() { + return initialColors; + } + + public void setInitialColors(Integer[] colors, int selectedColor) { + this.initialColors = colors; + this.colorSelection = selectedColor; + Integer initialColor = this.initialColors[this.colorSelection]; + if (initialColor == null) initialColor = 0xffffffff; + setInitialColor(initialColor, true); + } + + public void setInitialColor(int color, boolean updateText) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + + this.alpha = Utils.getAlphaPercent(color); + this.lightness = hsv[2]; + this.initialColors[this.colorSelection] = color; + this.initialColor = color; + setColorPreviewColor(color); + setColorToSliders(color); + if (this.colorEdit != null && updateText) + setColorText(color); + currentColorCircle = findNearestByColor(color); + } + + public void setLightness(float lightness) { + int lastSelectedColor = getSelectedColor(); + + this.lightness = lightness; + if (currentColorCircle != null) { + this.initialColor = Color.HSVToColor(Utils.alphaValueAsInt(this.alpha), currentColorCircle.getHsvWithLightness(lightness)); + if (this.colorEdit != null) + this.colorEdit.setText(Utils.getHexString(this.initialColor, this.alphaSlider != null)); + if (this.alphaSlider != null && this.initialColor != null) + this.alphaSlider.setColor(this.initialColor); + + callOnColorChangedListeners(lastSelectedColor, this.initialColor); + + updateColorWheel(); + invalidate(); + } + } + + public void setColor(int color, boolean updateText) { + setInitialColor(color, updateText); + updateColorWheel(); + invalidate(); + } + + public void setAlphaValue(float alpha) { + int lastSelectedColor = getSelectedColor(); + + this.alpha = alpha; + this.initialColor = Color.HSVToColor(Utils.alphaValueAsInt(this.alpha), currentColorCircle.getHsvWithLightness(this.lightness)); + if (this.colorEdit != null) + this.colorEdit.setText(Utils.getHexString(this.initialColor, this.alphaSlider != null)); + if (this.lightnessSlider != null && this.initialColor != null) + this.lightnessSlider.setColor(this.initialColor); + + callOnColorChangedListeners(lastSelectedColor, this.initialColor); + + updateColorWheel(); + invalidate(); + } + + public void addOnColorChangedListener(OnColorChangedListener listener) { + this.colorChangedListeners.add(listener); + } + + public void addOnColorSelectedListener(OnColorSelectedListener listener) { + this.listeners.add(listener); + } + + public void setLightnessSlider(LightnessSlider lightnessSlider) { + this.lightnessSlider = lightnessSlider; + if (lightnessSlider != null) { + this.lightnessSlider.setColorPicker(this); + this.lightnessSlider.setColor(getSelectedColor()); + } + } + + public void setAlphaSlider(AlphaSlider alphaSlider) { + this.alphaSlider = alphaSlider; + if (alphaSlider != null) { + this.alphaSlider.setColorPicker(this); + this.alphaSlider.setColor(getSelectedColor()); + } + } + + public void setColorEdit(EditText colorEdit) { + this.colorEdit = colorEdit; + if (this.colorEdit != null) { + this.colorEdit.setVisibility(View.VISIBLE); + this.colorEdit.addTextChangedListener(colorTextChange); + setColorEditTextColor(pickerColorEditTextColor); + } + } + + public void setColorEditTextColor(int argb) { + this.pickerColorEditTextColor = argb; + if (colorEdit != null) + colorEdit.setTextColor(argb); + } + + public void setDensity(int density) { + this.density = Math.max(2, density); + invalidate(); + } + + public void setRenderer(ColorWheelRenderer renderer) { + this.renderer = renderer; + invalidate(); + } + + public void setColorPreview(LinearLayout colorPreview, Integer selectedColor) { + if (colorPreview == null) + return; + this.colorPreview = colorPreview; + if (selectedColor == null) + selectedColor = 0; + int children = colorPreview.getChildCount(); + if (children == 0 || colorPreview.getVisibility() != View.VISIBLE) + return; + + for (int i = 0; i < children; i++) { + View childView = colorPreview.getChildAt(i); + if (!(childView instanceof LinearLayout)) + continue; + LinearLayout childLayout = (LinearLayout) childView; + if (i == selectedColor) { + childLayout.setBackgroundColor(Color.WHITE); + } + ImageView childImage = (ImageView) childLayout.findViewById(R.id.image_preview); + childImage.setClickable(true); + childImage.setTag(i); + childImage.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (v == null) + return; + Object tag = v.getTag(); + if (tag == null || !(tag instanceof Integer)) + return; + setSelectedColor((int) tag); + } + }); + } + } + + public void setSelectedColor(int previewNumber) { + if (initialColors == null || initialColors.length < previewNumber) + return; + this.colorSelection = previewNumber; + setHighlightedColor(previewNumber); + Integer color = initialColors[previewNumber]; + if (color == null) + return; + setColor(color, true); + } + + public void setShowBorder(boolean showBorder) { + this.showBorder = showBorder; + } + + private void setHighlightedColor(int previewNumber) { + int children = colorPreview.getChildCount(); + if (children == 0 || colorPreview.getVisibility() != View.VISIBLE) + return; + + for (int i = 0; i < children; i++) { + View childView = colorPreview.getChildAt(i); + if (!(childView instanceof LinearLayout)) + continue; + LinearLayout childLayout = (LinearLayout) childView; + if (i == previewNumber) { + childLayout.setBackgroundColor(Color.WHITE); + } else { + childLayout.setBackgroundColor(Color.TRANSPARENT); + } + } + } + + private void setColorPreviewColor(int newColor) { + if (colorPreview == null || initialColors == null || colorSelection > initialColors.length || initialColors[colorSelection] == null) + return; + + int children = colorPreview.getChildCount(); + if (children == 0 || colorPreview.getVisibility() != View.VISIBLE) + return; + + View childView = colorPreview.getChildAt(colorSelection); + if (!(childView instanceof LinearLayout)) + return; + LinearLayout childLayout = (LinearLayout) childView; + ImageView childImage = (ImageView) childLayout.findViewById(R.id.image_preview); + childImage.setImageDrawable(new ColorCircleDrawable(newColor)); + } + + private void setColorText(int argb) { + if (colorEdit == null) + return; + colorEdit.setText(Utils.getHexString(argb, this.alphaSlider != null)); + } + + private void setColorToSliders(int selectedColor) { + if (lightnessSlider != null) + lightnessSlider.setColor(selectedColor); + if (alphaSlider != null) + alphaSlider.setColor(selectedColor); + } + + public enum WHEEL_TYPE { + FLOWER, CIRCLE; + + public static WHEEL_TYPE indexOf(int index) { + switch (index) { + case 0: + return FLOWER; + case 1: + return CIRCLE; + } + return FLOWER; + } + } +} diff --git a/colorpicker/src/main/java/com/flask/colorpicker/OnColorChangedListener.java b/colorpicker/src/main/java/com/flask/colorpicker/OnColorChangedListener.java new file mode 100644 index 0000000000..eda2a53d95 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/OnColorChangedListener.java @@ -0,0 +1,5 @@ +package com.flask.colorpicker; + +public interface OnColorChangedListener { + void onColorChanged(int selectedColor); +} diff --git a/colorpicker/src/main/java/com/flask/colorpicker/OnColorSelectedListener.java b/colorpicker/src/main/java/com/flask/colorpicker/OnColorSelectedListener.java new file mode 100644 index 0000000000..dbf8f723f1 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/OnColorSelectedListener.java @@ -0,0 +1,5 @@ +package com.flask.colorpicker; + +public interface OnColorSelectedListener { + void onColorSelected(int selectedColor); +} diff --git a/colorpicker/src/main/java/com/flask/colorpicker/Utils.java b/colorpicker/src/main/java/com/flask/colorpicker/Utils.java new file mode 100644 index 0000000000..8e92c5ebf4 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/Utils.java @@ -0,0 +1,40 @@ +package com.flask.colorpicker; + +import android.graphics.Color; + +/** + * Created by Charles Andersons on 4/17/15. + */ +public class Utils { + public static float getAlphaPercent(int argb) { + return Color.alpha(argb) / 255f; + } + + public static int alphaValueAsInt(float alpha) { + return Math.round(alpha * 255); + } + + public static int adjustAlpha(float alpha, int color) { + return alphaValueAsInt(alpha) << 24 | (0x00ffffff & color); + } + + public static int colorAtLightness(int color, float lightness) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] = lightness; + return Color.HSVToColor(hsv); + } + + public static float lightnessOfColor(int color) { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + return hsv[2]; + } + + public static String getHexString(int color, boolean showAlpha) { + int base = showAlpha ? 0xFFFFFFFF : 0xFFFFFF; + String format = showAlpha ? "#%08X" : "#%06X"; + return String.format(format, (base & color)).toUpperCase(); + } + +} diff --git a/colorpicker/src/main/java/com/flask/colorpicker/builder/ColorPickerClickListener.java b/colorpicker/src/main/java/com/flask/colorpicker/builder/ColorPickerClickListener.java new file mode 100644 index 0000000000..35e70e92d6 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/builder/ColorPickerClickListener.java @@ -0,0 +1,10 @@ +package com.flask.colorpicker.builder; + +import android.content.DialogInterface; + +/** + * Created by Charles Anderson on 4/17/15. + */ +public interface ColorPickerClickListener { + void onClick(DialogInterface d, int lastSelectedColor, Integer[] allColors); +} diff --git a/colorpicker/src/main/java/com/flask/colorpicker/builder/ColorPickerDialogBuilder.java b/colorpicker/src/main/java/com/flask/colorpicker/builder/ColorPickerDialogBuilder.java new file mode 100644 index 0000000000..c724bb74e4 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/builder/ColorPickerDialogBuilder.java @@ -0,0 +1,296 @@ +package com.flask.colorpicker.builder; + +import androidx.appcompat.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.text.InputFilter; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.flask.colorpicker.ColorPickerView; +import com.flask.colorpicker.OnColorChangedListener; +import com.flask.colorpicker.OnColorSelectedListener; +import com.flask.colorpicker.R; +import com.flask.colorpicker.Utils; +import com.flask.colorpicker.renderer.ColorWheelRenderer; +import com.flask.colorpicker.slider.AlphaSlider; +import com.flask.colorpicker.slider.LightnessSlider; + +public class ColorPickerDialogBuilder { + private AlertDialog.Builder builder; + private LinearLayout pickerContainer; + private ColorPickerView colorPickerView; + private LightnessSlider lightnessSlider; + private AlphaSlider alphaSlider; + private EditText colorEdit; + private LinearLayout colorPreview; + + private boolean isLightnessSliderEnabled = true; + private boolean isAlphaSliderEnabled = true; + private boolean isBorderEnabled = true; + private boolean isColorEditEnabled = false; + private boolean isPreviewEnabled = false; + private int pickerCount = 1; + private int defaultMargin = 0; + private int defaultMarginTop = 0; + private Integer[] initialColor = new Integer[]{null, null, null, null, null}; + + private ColorPickerDialogBuilder(Context context) { + this(context, 0); + } + + private ColorPickerDialogBuilder(Context context, int theme) { + defaultMargin = getDimensionAsPx(context, R.dimen.default_slider_margin); + defaultMarginTop = getDimensionAsPx(context, R.dimen.default_margin_top); + + builder = new AlertDialog.Builder(context, theme); + pickerContainer = new LinearLayout(context); + pickerContainer.setOrientation(LinearLayout.VERTICAL); + pickerContainer.setGravity(Gravity.CENTER_HORIZONTAL); + pickerContainer.setPadding(defaultMargin, defaultMarginTop, defaultMargin, 0); + + LinearLayout.LayoutParams layoutParamsForColorPickerView = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); + layoutParamsForColorPickerView.weight = 1; + colorPickerView = new ColorPickerView(context); + + pickerContainer.addView(colorPickerView, layoutParamsForColorPickerView); + + builder.setView(pickerContainer); + } + + public static ColorPickerDialogBuilder with(Context context) { + return new ColorPickerDialogBuilder(context); + } + + public static ColorPickerDialogBuilder with(Context context, int theme) { + return new ColorPickerDialogBuilder(context, theme); + } + + public ColorPickerDialogBuilder setTitle(String title) { + builder.setTitle(title); + return this; + } + + public ColorPickerDialogBuilder setTitle(int titleId) { + builder.setTitle(titleId); + return this; + } + + public ColorPickerDialogBuilder initialColor(int initialColor) { + this.initialColor[0] = initialColor; + return this; + } + + public ColorPickerDialogBuilder initialColors(int[] initialColor) { + for (int i = 0; i < initialColor.length && i < this.initialColor.length; i++) { + this.initialColor[i] = initialColor[i]; + } + return this; + } + + public ColorPickerDialogBuilder wheelType(ColorPickerView.WHEEL_TYPE wheelType) { + ColorWheelRenderer renderer = ColorWheelRendererBuilder.getRenderer(wheelType); + colorPickerView.setRenderer(renderer); + return this; + } + + public ColorPickerDialogBuilder density(int density) { + colorPickerView.setDensity(density); + return this; + } + + public ColorPickerDialogBuilder setOnColorChangedListener(OnColorChangedListener onColorChangedListener) { + colorPickerView.addOnColorChangedListener(onColorChangedListener); + return this; + } + + public ColorPickerDialogBuilder setOnColorSelectedListener(OnColorSelectedListener onColorSelectedListener) { + colorPickerView.addOnColorSelectedListener(onColorSelectedListener); + return this; + } + + public ColorPickerDialogBuilder setPositiveButton(CharSequence text, final ColorPickerClickListener onClickListener) { + builder.setPositiveButton(text, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + positiveButtonOnClick(dialog, onClickListener); + } + }); + return this; + } + + public ColorPickerDialogBuilder setPositiveButton(int textId, final ColorPickerClickListener onClickListener) { + builder.setPositiveButton(textId, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + positiveButtonOnClick(dialog, onClickListener); + } + }); + return this; + } + + public ColorPickerDialogBuilder setNegativeButton(CharSequence text, DialogInterface.OnClickListener onClickListener) { + builder.setNegativeButton(text, onClickListener); + return this; + } + + public ColorPickerDialogBuilder setNegativeButton(int textId, DialogInterface.OnClickListener onClickListener) { + builder.setNegativeButton(textId, onClickListener); + return this; + } + + public ColorPickerDialogBuilder noSliders() { + isLightnessSliderEnabled = false; + isAlphaSliderEnabled = false; + return this; + } + + public ColorPickerDialogBuilder alphaSliderOnly() { + isLightnessSliderEnabled = false; + isAlphaSliderEnabled = true; + return this; + } + + public ColorPickerDialogBuilder lightnessSliderOnly() { + isLightnessSliderEnabled = true; + isAlphaSliderEnabled = false; + return this; + } + + public ColorPickerDialogBuilder showAlphaSlider(boolean showAlpha) { + isAlphaSliderEnabled = showAlpha; + return this; + } + + public ColorPickerDialogBuilder showLightnessSlider(boolean showLightness) { + isLightnessSliderEnabled = showLightness; + return this; + } + + public ColorPickerDialogBuilder showBorder(boolean showBorder) { + isBorderEnabled = showBorder; + return this; + } + + public ColorPickerDialogBuilder showColorEdit(boolean showEdit) { + isColorEditEnabled = showEdit; + return this; + } + + public ColorPickerDialogBuilder setColorEditTextColor(int argb) { + colorPickerView.setColorEditTextColor(argb); + return this; + } + + public ColorPickerDialogBuilder showColorPreview(boolean showPreview) { + isPreviewEnabled = showPreview; + if (!showPreview) + pickerCount = 1; + return this; + } + + public ColorPickerDialogBuilder setPickerCount(int pickerCount) throws IndexOutOfBoundsException { + if (pickerCount < 1 || pickerCount > 5) + throw new IndexOutOfBoundsException("Picker Can Only Support 1-5 Colors"); + this.pickerCount = pickerCount; + if (this.pickerCount > 1) + this.isPreviewEnabled = true; + return this; + } + + public AlertDialog build() { + Context context = builder.getContext(); + colorPickerView.setInitialColors(initialColor, getStartOffset(initialColor)); + colorPickerView.setShowBorder(isBorderEnabled); + + if (isLightnessSliderEnabled) { + LinearLayout.LayoutParams layoutParamsForLightnessBar = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getDimensionAsPx(context, R.dimen.default_slider_height)); + lightnessSlider = new LightnessSlider(context); + lightnessSlider.setLayoutParams(layoutParamsForLightnessBar); + pickerContainer.addView(lightnessSlider); + colorPickerView.setLightnessSlider(lightnessSlider); + lightnessSlider.setColor(getStartColor(initialColor)); + lightnessSlider.setShowBorder(isBorderEnabled); + } + if (isAlphaSliderEnabled) { + LinearLayout.LayoutParams layoutParamsForAlphaBar = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getDimensionAsPx(context, R.dimen.default_slider_height)); + alphaSlider = new AlphaSlider(context); + alphaSlider.setLayoutParams(layoutParamsForAlphaBar); + pickerContainer.addView(alphaSlider); + colorPickerView.setAlphaSlider(alphaSlider); + alphaSlider.setColor(getStartColor(initialColor)); + alphaSlider.setShowBorder(isBorderEnabled); + } + if (isColorEditEnabled) { + LinearLayout.LayoutParams layoutParamsForColorEdit = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + colorEdit = (EditText) View.inflate(context, R.layout.color_edit, null); + colorEdit.setFilters(new InputFilter[]{new InputFilter.AllCaps()}); + colorEdit.setSingleLine(); + colorEdit.setVisibility(View.GONE); + + // limit number of characters to hexColors + int maxLength = isAlphaSliderEnabled ? 9 : 7; + colorEdit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)}); + + pickerContainer.addView(colorEdit, layoutParamsForColorEdit); + + colorEdit.setText(Utils.getHexString(getStartColor(initialColor), isAlphaSliderEnabled)); + colorPickerView.setColorEdit(colorEdit); + } + if (isPreviewEnabled) { + colorPreview = (LinearLayout) View.inflate(context, R.layout.color_preview, null); + colorPreview.setVisibility(View.GONE); + pickerContainer.addView(colorPreview); + + if (initialColor.length == 0) { + ImageView colorImage = (ImageView) View.inflate(context, R.layout.color_selector, null); + colorImage.setImageDrawable(new ColorDrawable(Color.WHITE)); + } else { + for (int i = 0; i < initialColor.length && i < this.pickerCount; i++) { + if (initialColor[i] == null) + break; + LinearLayout colorLayout = (LinearLayout) View.inflate(context, R.layout.color_selector, null); + ImageView colorImage = (ImageView) colorLayout.findViewById(R.id.image_preview); + colorImage.setImageDrawable(new ColorDrawable(initialColor[i])); + colorPreview.addView(colorLayout); + } + } + colorPreview.setVisibility(View.VISIBLE); + colorPickerView.setColorPreview(colorPreview, getStartOffset(initialColor)); + } + + return builder.create(); + } + + private Integer getStartOffset(Integer[] colors) { + Integer start = 0; + for (int i = 0; i < colors.length; i++) { + if (colors[i] == null) { + return start; + } + start = (i + 1) / 2; + } + return start; + } + + private int getStartColor(Integer[] colors) { + Integer startColor = getStartOffset(colors); + return startColor == null ? Color.WHITE : colors[startColor]; + } + + private static int getDimensionAsPx(Context context, int rid) { + return (int) (context.getResources().getDimension(rid) + .5f); + } + + private void positiveButtonOnClick(DialogInterface dialog, ColorPickerClickListener onClickListener) { + int selectedColor = colorPickerView.getSelectedColor(); + Integer[] allColors = colorPickerView.getAllColors(); + onClickListener.onClick(dialog, selectedColor, allColors); + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/builder/ColorWheelRendererBuilder.java b/colorpicker/src/main/java/com/flask/colorpicker/builder/ColorWheelRendererBuilder.java new file mode 100644 index 0000000000..cc816ee9d9 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/builder/ColorWheelRendererBuilder.java @@ -0,0 +1,18 @@ +package com.flask.colorpicker.builder; + +import com.flask.colorpicker.ColorPickerView; +import com.flask.colorpicker.renderer.ColorWheelRenderer; +import com.flask.colorpicker.renderer.FlowerColorWheelRenderer; +import com.flask.colorpicker.renderer.SimpleColorWheelRenderer; + +public class ColorWheelRendererBuilder { + public static ColorWheelRenderer getRenderer(ColorPickerView.WHEEL_TYPE wheelType) { + switch (wheelType) { + case CIRCLE: + return new SimpleColorWheelRenderer(); + case FLOWER: + return new FlowerColorWheelRenderer(); + } + throw new IllegalArgumentException("wrong WHEEL_TYPE"); + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/builder/PaintBuilder.java b/colorpicker/src/main/java/com/flask/colorpicker/builder/PaintBuilder.java new file mode 100644 index 0000000000..a8bb6a5f0c --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/builder/PaintBuilder.java @@ -0,0 +1,82 @@ +package com.flask.colorpicker.builder; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Shader; + +public class PaintBuilder { + public static PaintHolder newPaint() { + return new PaintHolder(); + } + + public static class PaintHolder { + private Paint paint; + + private PaintHolder() { + this.paint = new Paint(Paint.ANTI_ALIAS_FLAG); + } + + public PaintHolder color(int color) { + this.paint.setColor(color); + return this; + } + + public PaintHolder antiAlias(boolean flag) { + this.paint.setAntiAlias(flag); + return this; + } + + public PaintHolder style(Paint.Style style) { + this.paint.setStyle(style); + return this; + } + + public PaintHolder mode(PorterDuff.Mode mode) { + this.paint.setXfermode(new PorterDuffXfermode(mode)); + return this; + } + + public PaintHolder stroke(float width) { + this.paint.setStrokeWidth(width); + return this; + } + + public PaintHolder xPerMode(PorterDuff.Mode mode) { + this.paint.setXfermode(new PorterDuffXfermode(mode)); + return this; + } + + public PaintHolder shader(Shader shader) { + this.paint.setShader(shader); + return this; + } + + public Paint build() { + return this.paint; + } + } + + public static Shader createAlphaPatternShader(int size) { + size /= 2; + size = Math.max(8, size * 2); + return new BitmapShader(createAlphaBackgroundPattern(size), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); + } + + private static Bitmap createAlphaBackgroundPattern(int size) { + Paint alphaPatternPaint = PaintBuilder.newPaint().build(); + Bitmap bm = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bm); + int s = Math.round(size / 2f); + for (int i = 0; i < 2; i++) + for (int j = 0; j < 2; j++) { + if ((i + j) % 2 == 0) alphaPatternPaint.setColor(0xffffffff); + else alphaPatternPaint.setColor(0xffd0d0d0); + c.drawRect(i * s, j * s, (i + 1) * s, (j + 1) * s, alphaPatternPaint); + } + return bm; + } +} diff --git a/colorpicker/src/main/java/com/flask/colorpicker/renderer/AbsColorWheelRenderer.java b/colorpicker/src/main/java/com/flask/colorpicker/renderer/AbsColorWheelRenderer.java new file mode 100644 index 0000000000..9ce0f980a0 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/renderer/AbsColorWheelRenderer.java @@ -0,0 +1,34 @@ +package com.flask.colorpicker.renderer; + +import com.flask.colorpicker.ColorCircle; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AbsColorWheelRenderer implements ColorWheelRenderer { + protected ColorWheelRenderOption colorWheelRenderOption; + protected List colorCircleList = new ArrayList<>(); + + public void initWith(ColorWheelRenderOption colorWheelRenderOption) { + this.colorWheelRenderOption = colorWheelRenderOption; + this.colorCircleList.clear(); + } + + @Override + public ColorWheelRenderOption getRenderOption() { + if (colorWheelRenderOption == null) colorWheelRenderOption = new ColorWheelRenderOption(); + return colorWheelRenderOption; + } + + public List getColorCircleList() { + return colorCircleList; + } + + protected int getAlphaValueAsInt() { + return Math.round(colorWheelRenderOption.alpha * 255); + } + + protected int calcTotalCount(float radius, float size) { + return Math.max(1, (int) ((1f - GAP_PERCENTAGE) * Math.PI / (Math.asin(size / radius)) + 0.5f)); + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/renderer/ColorWheelRenderOption.java b/colorpicker/src/main/java/com/flask/colorpicker/renderer/ColorWheelRenderOption.java new file mode 100644 index 0000000000..6a8c7eb1c0 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/renderer/ColorWheelRenderOption.java @@ -0,0 +1,10 @@ +package com.flask.colorpicker.renderer; + +import android.graphics.Canvas; + +public class ColorWheelRenderOption { + public int density; + public float maxRadius; + public float cSize, strokeWidth, alpha, lightness; + public Canvas targetCanvas; +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/renderer/ColorWheelRenderer.java b/colorpicker/src/main/java/com/flask/colorpicker/renderer/ColorWheelRenderer.java new file mode 100644 index 0000000000..180fcda25c --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/renderer/ColorWheelRenderer.java @@ -0,0 +1,17 @@ +package com.flask.colorpicker.renderer; + +import com.flask.colorpicker.ColorCircle; + +import java.util.List; + +public interface ColorWheelRenderer { + float GAP_PERCENTAGE = 0.025f; + + void draw(); + + ColorWheelRenderOption getRenderOption(); + + void initWith(ColorWheelRenderOption colorWheelRenderOption); + + List getColorCircleList(); +} diff --git a/colorpicker/src/main/java/com/flask/colorpicker/renderer/FlowerColorWheelRenderer.java b/colorpicker/src/main/java/com/flask/colorpicker/renderer/FlowerColorWheelRenderer.java new file mode 100644 index 0000000000..02927af606 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/renderer/FlowerColorWheelRenderer.java @@ -0,0 +1,50 @@ +package com.flask.colorpicker.renderer; + +import android.graphics.Color; +import android.graphics.Paint; + +import com.flask.colorpicker.ColorCircle; +import com.flask.colorpicker.builder.PaintBuilder; + +public class FlowerColorWheelRenderer extends AbsColorWheelRenderer { + private Paint selectorFill = PaintBuilder.newPaint().build(); + private float[] hsv = new float[3]; + private float sizeJitter = 1.2f; + + @Override + public void draw() { + final int setSize = colorCircleList.size(); + int currentCount = 0; + float half = colorWheelRenderOption.targetCanvas.getWidth() / 2f; + int density = colorWheelRenderOption.density; + float strokeWidth = colorWheelRenderOption.strokeWidth; + float maxRadius = colorWheelRenderOption.maxRadius; + float cSize = colorWheelRenderOption.cSize; + + for (int i = 0; i < density; i++) { + float p = (float) i / (density - 1); // 0~1 + float jitter = (i - density / 2f) / density; // -0.5 ~ 0.5 + float radius = maxRadius * p; + float size = Math.max(1.5f + strokeWidth, cSize + (i == 0 ? 0 : cSize * sizeJitter * jitter)); + int total = Math.min(calcTotalCount(radius, size), density * 2); + + for (int j = 0; j < total; j++) { + double angle = Math.PI * 2 * j / total + (Math.PI / total) * ((i + 1) % 2); + float x = half + (float) (radius * Math.cos(angle)); + float y = half + (float) (radius * Math.sin(angle)); + hsv[0] = (float) (angle * 180 / Math.PI); + hsv[1] = radius / maxRadius; + hsv[2] = colorWheelRenderOption.lightness; + selectorFill.setColor(Color.HSVToColor(hsv)); + selectorFill.setAlpha(getAlphaValueAsInt()); + + colorWheelRenderOption.targetCanvas.drawCircle(x, y, size - strokeWidth, selectorFill); + + if (currentCount >= setSize) { + colorCircleList.add(new ColorCircle(x, y, hsv)); + } else colorCircleList.get(currentCount).set(x, y, hsv); + currentCount++; + } + } + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/renderer/SimpleColorWheelRenderer.java b/colorpicker/src/main/java/com/flask/colorpicker/renderer/SimpleColorWheelRenderer.java new file mode 100644 index 0000000000..46a3769d34 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/renderer/SimpleColorWheelRenderer.java @@ -0,0 +1,46 @@ +package com.flask.colorpicker.renderer; + +import android.graphics.Color; +import android.graphics.Paint; + +import com.flask.colorpicker.ColorCircle; +import com.flask.colorpicker.builder.PaintBuilder; + +public class SimpleColorWheelRenderer extends AbsColorWheelRenderer { + private Paint selectorFill = PaintBuilder.newPaint().build(); + private float[] hsv = new float[3]; + + @Override + public void draw() { + final int setSize = colorCircleList.size(); + int currentCount = 0; + float half = colorWheelRenderOption.targetCanvas.getWidth() / 2f; + int density = colorWheelRenderOption.density; + float maxRadius = colorWheelRenderOption.maxRadius; + + for (int i = 0; i < density; i++) { + float p = (float) i / (density - 1); // 0~1 + float radius = maxRadius * p; + float size = colorWheelRenderOption.cSize; + int total = calcTotalCount(radius, size); + + for (int j = 0; j < total; j++) { + double angle = Math.PI * 2 * j / total + (Math.PI / total) * ((i + 1) % 2); + float x = half + (float) (radius * Math.cos(angle)); + float y = half + (float) (radius * Math.sin(angle)); + hsv[0] = (float) (angle * 180 / Math.PI); + hsv[1] = radius / maxRadius; + hsv[2] = colorWheelRenderOption.lightness; + selectorFill.setColor(Color.HSVToColor(hsv)); + selectorFill.setAlpha(getAlphaValueAsInt()); + + colorWheelRenderOption.targetCanvas.drawCircle(x, y, size - colorWheelRenderOption.strokeWidth, selectorFill); + + if (currentCount >= setSize) + colorCircleList.add(new ColorCircle(x, y, hsv)); + else colorCircleList.get(currentCount).set(x, y, hsv); + currentCount++; + } + } + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/slider/AbsCustomSlider.java b/colorpicker/src/main/java/com/flask/colorpicker/slider/AbsCustomSlider.java new file mode 100644 index 0000000000..76edec4e4e --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/slider/AbsCustomSlider.java @@ -0,0 +1,189 @@ +package com.flask.colorpicker.slider; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import androidx.annotation.DimenRes; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import com.flask.colorpicker.R; + +public abstract class AbsCustomSlider extends View { + protected Bitmap bitmap; + protected Canvas bitmapCanvas; + protected Bitmap bar; + protected Canvas barCanvas; + protected OnValueChangedListener onValueChangedListener; + protected int barOffsetX; + protected int handleRadius = 20; + protected int barHeight = 5; + protected float value = 1; + protected boolean showBorder = false; + + private boolean inVerticalOrientation = false; + + public AbsCustomSlider(Context context) { + super(context); + init(context, null); + } + + public AbsCustomSlider(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public AbsCustomSlider(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + TypedArray styledAttrs = context.getTheme().obtainStyledAttributes( + attrs, R.styleable.AbsCustomSlider, 0, 0); + try { + inVerticalOrientation = styledAttrs.getBoolean( + R.styleable.AbsCustomSlider_inVerticalOrientation, inVerticalOrientation); + } finally { + styledAttrs.recycle(); + } + } + + protected void updateBar() { + handleRadius = getDimension(R.dimen.default_slider_handler_radius); + barHeight = getDimension(R.dimen.default_slider_bar_height); + barOffsetX = handleRadius; + + if (bar == null) + createBitmaps(); + drawBar(barCanvas); + invalidate(); + } + + protected void createBitmaps() { + int width; + int height; + if (inVerticalOrientation) { + width = getHeight(); + height = getWidth(); + } else { + width = getWidth(); + height = getHeight(); + } + + bar = Bitmap.createBitmap(Math.max(width - barOffsetX * 2, 1), barHeight, Bitmap.Config.ARGB_8888); + barCanvas = new Canvas(bar); + + if (bitmap == null || bitmap.getWidth() != width || bitmap.getHeight() != height) { + if (bitmap != null) bitmap.recycle(); + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmapCanvas = new Canvas(bitmap); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int width; + int height; + if (inVerticalOrientation) { + width = getHeight(); + height = getWidth(); + + canvas.rotate(-90); + canvas.translate(-width, 0); + } else { + width = getWidth(); + height = getHeight(); + } + + if (bar != null && bitmapCanvas != null) { + bitmapCanvas.drawColor(0, PorterDuff.Mode.CLEAR); + bitmapCanvas.drawBitmap(bar, barOffsetX, (height - bar.getHeight()) / 2, null); + + float x = handleRadius + value * (width - handleRadius * 2); + float y = height / 2f; + drawHandle(bitmapCanvas, x, y); + canvas.drawBitmap(bitmap, 0, 0, null); + } + } + + protected abstract void drawBar(Canvas barCanvas); + + protected abstract void onValueChanged(float value); + + protected abstract void drawHandle(Canvas canvas, float x, float y); + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateBar(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int width = 0; + if (widthMode == MeasureSpec.UNSPECIFIED) + width = widthMeasureSpec; + else if (widthMode == MeasureSpec.AT_MOST) + width = MeasureSpec.getSize(widthMeasureSpec); + else if (widthMode == MeasureSpec.EXACTLY) + width = MeasureSpec.getSize(widthMeasureSpec); + + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int height = 0; + if (heightMode == MeasureSpec.UNSPECIFIED) + height = heightMeasureSpec; + else if (heightMode == MeasureSpec.AT_MOST) + height = MeasureSpec.getSize(heightMeasureSpec); + else if (heightMode == MeasureSpec.EXACTLY) + height = MeasureSpec.getSize(heightMeasureSpec); + + setMeasuredDimension(width, height); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: { + if (bar != null) { + if (inVerticalOrientation) { + value = 1 - (event.getY() - barOffsetX) / bar.getWidth(); + } else { + value = (event.getX() - barOffsetX) / bar.getWidth(); + } + value = Math.max(0, Math.min(value, 1)); + onValueChanged(value); + invalidate(); + } + break; + } + case MotionEvent.ACTION_UP: { + onValueChanged(value); + if (onValueChangedListener != null) + onValueChangedListener.onValueChanged(value); + invalidate(); + } + } + return true; + } + + protected int getDimension(@DimenRes int id) { + return getResources().getDimensionPixelSize(id); + } + + public void setShowBorder(boolean showBorder) { + this.showBorder = showBorder; + } + + public void setOnValueChangedListener(OnValueChangedListener onValueChangedListener) { + this.onValueChangedListener = onValueChangedListener; + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/slider/AlphaSlider.java b/colorpicker/src/main/java/com/flask/colorpicker/slider/AlphaSlider.java new file mode 100644 index 0000000000..cf3db827e3 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/slider/AlphaSlider.java @@ -0,0 +1,100 @@ +package com.flask.colorpicker.slider; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.util.AttributeSet; + +import com.flask.colorpicker.ColorPickerView; +import com.flask.colorpicker.Utils; +import com.flask.colorpicker.builder.PaintBuilder; + +public class AlphaSlider extends AbsCustomSlider { + public int color; + private Paint alphaPatternPaint = PaintBuilder.newPaint().build(); + private Paint barPaint = PaintBuilder.newPaint().build(); + private Paint solid = PaintBuilder.newPaint().build(); + private Paint clearingStroke = PaintBuilder.newPaint().color(0xffffffff).xPerMode(PorterDuff.Mode.CLEAR).build(); + + private Paint clearStroke = PaintBuilder.newPaint().build(); + private Bitmap clearBitmap; + private Canvas clearBitmapCanvas; + + private ColorPickerView colorPicker; + + public AlphaSlider(Context context) { + super(context); + } + + public AlphaSlider(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AlphaSlider(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void createBitmaps() { + super.createBitmaps(); + alphaPatternPaint.setShader(PaintBuilder.createAlphaPatternShader(barHeight * 2)); + clearBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); + clearBitmapCanvas = new Canvas(clearBitmap); + } + + @Override + protected void drawBar(Canvas barCanvas) { + int width = barCanvas.getWidth(); + int height = barCanvas.getHeight(); + + barCanvas.drawRect(0, 0, width, height, alphaPatternPaint); + int l = Math.max(2, width / 256); + for (int x = 0; x <= width; x += l) { + float alpha = (float) x / (width - 1); + barPaint.setColor(color); + barPaint.setAlpha(Math.round(alpha * 255)); + barCanvas.drawRect(x, 0, x + l, height, barPaint); + } + } + + @Override + protected void onValueChanged(float value) { + if (colorPicker != null) + colorPicker.setAlphaValue(value); + } + + @Override + protected void drawHandle(Canvas canvas, float x, float y) { + solid.setColor(color); + solid.setAlpha(Math.round(value * 255)); + if (showBorder) canvas.drawCircle(x, y, handleRadius, clearingStroke); + if (value < 1) { + // this fixes the same artifact issue from ColorPickerView + // happens when alpha pattern is drawn underneath a circle with the same size + clearBitmapCanvas.drawColor(0, PorterDuff.Mode.CLEAR); + clearBitmapCanvas.drawCircle(x, y, handleRadius * 0.75f + 4, alphaPatternPaint); + clearBitmapCanvas.drawCircle(x, y, handleRadius * 0.75f + 4, solid); + + clearStroke = PaintBuilder.newPaint().color(0xffffffff).style(Paint.Style.STROKE).stroke(6).xPerMode(PorterDuff.Mode.CLEAR).build(); + clearBitmapCanvas.drawCircle(x, y, handleRadius * 0.75f + (clearStroke.getStrokeWidth() / 2), clearStroke); + canvas.drawBitmap(clearBitmap, 0, 0, null); + } else { + canvas.drawCircle(x, y, handleRadius * 0.75f, solid); + } + } + + public void setColorPicker(ColorPickerView colorPicker) { + this.colorPicker = colorPicker; + } + + public void setColor(int color) { + this.color = color; + this.value = Utils.getAlphaPercent(color); + if (bar != null) { + updateBar(); + invalidate(); + } + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/slider/LightnessSlider.java b/colorpicker/src/main/java/com/flask/colorpicker/slider/LightnessSlider.java new file mode 100644 index 0000000000..58de0b2192 --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/slider/LightnessSlider.java @@ -0,0 +1,74 @@ +package com.flask.colorpicker.slider; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.util.AttributeSet; + +import com.flask.colorpicker.ColorPickerView; +import com.flask.colorpicker.Utils; +import com.flask.colorpicker.builder.PaintBuilder; + +public class LightnessSlider extends AbsCustomSlider { + private int color; + private Paint barPaint = PaintBuilder.newPaint().build(); + private Paint solid = PaintBuilder.newPaint().build(); + private Paint clearingStroke = PaintBuilder.newPaint().color(0xffffffff).xPerMode(PorterDuff.Mode.CLEAR).build(); + + private ColorPickerView colorPicker; + + public LightnessSlider(Context context) { + super(context); + } + + public LightnessSlider(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LightnessSlider(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void drawBar(Canvas barCanvas) { + int width = barCanvas.getWidth(); + int height = barCanvas.getHeight(); + + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + int l = Math.max(2, width / 256); + for (int x = 0; x <= width; x += l) { + hsv[2] = (float) x / (width - 1); + barPaint.setColor(Color.HSVToColor(hsv)); + barCanvas.drawRect(x, 0, x + l, height, barPaint); + } + } + + @Override + protected void onValueChanged(float value) { + if (colorPicker != null) + colorPicker.setLightness(value); + } + + @Override + protected void drawHandle(Canvas canvas, float x, float y) { + solid.setColor(Utils.colorAtLightness(color, value)); + if (showBorder) canvas.drawCircle(x, y, handleRadius, clearingStroke); + canvas.drawCircle(x, y, handleRadius * 0.75f, solid); + } + + public void setColorPicker(ColorPickerView colorPicker) { + this.colorPicker = colorPicker; + } + + public void setColor(int color) { + this.color = color; + this.value = Utils.lightnessOfColor(color); + if (bar != null) { + updateBar(); + invalidate(); + } + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/flask/colorpicker/slider/OnValueChangedListener.java b/colorpicker/src/main/java/com/flask/colorpicker/slider/OnValueChangedListener.java new file mode 100644 index 0000000000..68b263a83c --- /dev/null +++ b/colorpicker/src/main/java/com/flask/colorpicker/slider/OnValueChangedListener.java @@ -0,0 +1,5 @@ +package com.flask.colorpicker.slider; + +public interface OnValueChangedListener { + void onValueChanged(float value); +} \ No newline at end of file diff --git a/colorpicker/src/main/res/layout/color_edit.xml b/colorpicker/src/main/res/layout/color_edit.xml new file mode 100644 index 0000000000..53707c89f3 --- /dev/null +++ b/colorpicker/src/main/res/layout/color_edit.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/colorpicker/src/main/res/layout/color_preview.xml b/colorpicker/src/main/res/layout/color_preview.xml new file mode 100644 index 0000000000..22c055147f --- /dev/null +++ b/colorpicker/src/main/res/layout/color_preview.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/colorpicker/src/main/res/layout/color_selector.xml b/colorpicker/src/main/res/layout/color_selector.xml new file mode 100644 index 0000000000..b9ef85a7e3 --- /dev/null +++ b/colorpicker/src/main/res/layout/color_selector.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/colorpicker/src/main/res/layout/color_widget.xml b/colorpicker/src/main/res/layout/color_widget.xml new file mode 100644 index 0000000000..9d0f6f7218 --- /dev/null +++ b/colorpicker/src/main/res/layout/color_widget.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/colorpicker/src/main/res/values/attrs.xml b/colorpicker/src/main/res/values/attrs.xml new file mode 100644 index 0000000000..45887f990a --- /dev/null +++ b/colorpicker/src/main/res/values/attrs.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/colorpicker/src/main/res/values/dimens.xml b/colorpicker/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..d3737543e7 --- /dev/null +++ b/colorpicker/src/main/res/values/dimens.xml @@ -0,0 +1,10 @@ + + 36dp + 24dp + 4dp + 10dp + 24dp + 40dp + 36dp + 20dp + diff --git a/colorpicker/src/main/res/values/styles.xml b/colorpicker/src/main/res/values/styles.xml new file mode 100644 index 0000000000..afdeddf13b --- /dev/null +++ b/colorpicker/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index fd78ac7fec..247746043c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ -include ':app', ':openpgp-api' +include ':app', ':colorpicker', ':openpgp-api' +project(':colorpicker').projectDir = new File('colorpicker') project(':openpgp-api').projectDir = new File('openpgp-api')