diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/AlphaView.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/AlphaView.java new file mode 100644 index 000000000..e99aff955 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/AlphaView.java @@ -0,0 +1,53 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import android.content.Context; +import android.graphics.Bitmap; + +public class AlphaView extends SliderViewBase implements ColorObserver { + + private ObservableColor observableColor = new ObservableColor(0); + + public AlphaView(Context context) { + super(context); + } + + public void observeColor(ObservableColor observableColor) { + this.observableColor = observableColor; + observableColor.addObserver(this); + } + + @Override + public void updateColor(ObservableColor observableColor) { + setPos((float)observableColor.getAlpha()/0xff); + updateBitmap(); + invalidate(); + } + + @Override + protected void notifyListener(float currentPos) { + observableColor.updateAlpha((int)(currentPos * 0xff), this); + } + + @Override + protected int getPointerColor(float currentPos) { + float solidColorLightness = observableColor.getLightness(); + float posLightness = 1 + currentPos * (solidColorLightness - 1); + return posLightness > 0.5f ? 0xff000000 : 0xffffffff; + } + + @Override + protected Bitmap makeBitmap(int w, int h) { + final boolean isWide = w > h; + final int n = Math.max(w, h); + int color = observableColor.getColor(); + int[] colors = new int[n]; + for (int i = 0; i < n; ++i) { + float alpha = isWide ? (float)i / n : 1 - (float)i / n; + colors[i] = color & 0xffffff | (int)(alpha * 0xff) << 24; + } + final int bmpWidth = isWide ? w : 1; + final int bmpHeight = isWide ? 1 : h; + return Bitmap.createBitmap(colors, bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888); + } + +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ColorObserver.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ColorObserver.java new file mode 100644 index 000000000..b660d98a0 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ColorObserver.java @@ -0,0 +1,6 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +public interface ColorObserver { + + void updateColor(ObservableColor observableColor); +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ColorPickerView.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ColorPickerView.java new file mode 100644 index 000000000..78546aee7 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ColorPickerView.java @@ -0,0 +1,148 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import static ru.vtosters.lite.utils.AndroidUtils.dp2px; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.InputFilter; +import android.text.InputType; +import android.text.method.DigitsKeyListener; +import android.view.Gravity; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import ru.vtosters.lite.utils.LayoutUtils; + +public class ColorPickerView extends FrameLayout { + + private AlphaView alphaView; + private EditText hexEdit; + private ObservableColor observableColor = new ObservableColor(0); + private SwatchView swatchView; + + public ColorPickerView(Context context) { + super(context); + + var container = new LinearLayout(context); + container.setOrientation(LinearLayout.VERTICAL); + container.setPadding(dp2px(16), dp2px(16), dp2px(16), dp2px(16)); + container.setGravity(Gravity.CENTER); + this.addView(container, LayoutUtils.createFrame(-2, -2)); + + var colorPickerFrame = new FrameLayout(context); + var colorPickerFrameParams = LayoutUtils.createLinear(-2, -2); + colorPickerFrameParams.weight = 1.0f; + container.addView(colorPickerFrame, colorPickerFrameParams); + + swatchView = new SwatchView(context); + colorPickerFrame.addView(swatchView, LayoutUtils.createFrame(-2, -2)); + + HueSatView hueSatView = new HueSatView(context); + colorPickerFrame.addView(hueSatView, LayoutUtils.createFrame(-2, -2)); + + ValueView valueView = new ValueView(context); + var valueViewParams = LayoutUtils.createLinear(-1, dp2px(36)); + valueViewParams.topMargin = dp2px(16); + container.addView(valueView, valueViewParams); + + alphaView = new AlphaView(context); + var alphaViewParams = LayoutUtils.createLinear(-1, dp2px(36)); + valueViewParams.topMargin = dp2px(16); + container.addView(alphaView, alphaViewParams); + + hexEdit = new EditText(context); + hexEdit.setText("00000000"); + hexEdit.setKeyListener(DigitsKeyListener.getInstance("0123456789ABCDEFabcdef")); + hexEdit.setGravity(Gravity.CENTER_HORIZONTAL); + hexEdit.setFilters(new InputFilter[]{ new InputFilter.LengthFilter(4) }); + hexEdit.setBackground(null); + hexEdit.setTypeface(Typeface.MONOSPACE); + hexEdit.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + hexEdit.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + container.addView(hexEdit, LayoutUtils.createLinear(-1, -2)); + + swatchView.observeColor(observableColor); + hueSatView.observeColor(observableColor); + valueView.observeColor(observableColor); + alphaView.observeColor(observableColor); + HexEdit.setUpListeners(hexEdit, observableColor); + + showAlpha(true); + showHex(true); + showPreview(true); + + // We get all our state saved and restored for free, + // thanks to the EditText and its listeners! + } + + /** Returns the color selected by the user */ + public int getColor() { + return observableColor.getColor(); + } + + /** + * Sets the original color swatch and the current color to the specified value.
+ * Using dark colors may lead to a loss of color information caused by the ARGB color space. + * Use {@link #setColor(int, float, float, float)} instead to avoid this. + */ + public void setColor(int color) { + setOriginalColor(color); + setCurrentColor(color); + } + + /** + * Sets the original color swatch and the current color to the specified value. + * + * + */ public void setColor(int alpha, float hue, float sat, float bri) { + setOriginalColor(alpha, hue, sat, bri); + setCurrentColor(alpha, hue, sat, bri); + } + + /** Sets the original color swatch without changing the current color. */ + public void setOriginalColor(int color) { + swatchView.setOriginalColor(color); + } + + /** Sets the original color swatch without changing the current color. */ + public void setOriginalColor(int alpha, float hue, float sat, float bri) { + int color = Color.HSVToColor(alpha, new float[]{hue, sat, bri}); + swatchView.setOriginalColor(color); + } + + /** Updates the current color without changing the original color swatch. */ + public void setCurrentColor(int color) { + observableColor.updateColor(color, null); + } + + /** Updates the current color without changing the original color swatch. */ + public void setCurrentColor(int alpha, float hue, float sat, float bri) { + observableColor.updateColor(alpha, hue, sat, bri, null); + } + + public void showAlpha(boolean showAlpha) { + alphaView.setVisibility(showAlpha ? View.VISIBLE : View.GONE); + HexEdit.setShowAlphaDigits(hexEdit, showAlpha); + } + + public void addColorObserver(ColorObserver observer) { + observableColor.addObserver(observer); + } + + public void showHex(boolean showHex) { + hexEdit.setVisibility(showHex ? View.VISIBLE : View.GONE); + } + + public void showPreview(boolean showPreview) { + swatchView.setVisibility(showPreview ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/HexEdit.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/HexEdit.java new file mode 100644 index 000000000..e485009eb --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/HexEdit.java @@ -0,0 +1,96 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import android.graphics.Color; +import android.text.Editable; +import android.text.InputFilter; +import android.text.Spanned; +import android.text.TextWatcher; +import android.widget.EditText; + +class HexEdit { + + private static InputFilter[] withoutAlphaDigits = {new ColorPasteLengthFilter()}; + private static InputFilter[] withAlphaDigits = {new InputFilter.LengthFilter(8)}; + + public static void setUpListeners(final EditText hexEdit, final ObservableColor observableColor) { + + class MultiObserver implements ColorObserver, TextWatcher { + + @Override + public void updateColor(ObservableColor observableColor) { + final String colorString = formatColor(observableColor.getColor()); + // Prevent onTextChanged getting called when we update text programmatically + hexEdit.removeTextChangedListener(this); + hexEdit.setText(colorString); + hexEdit.addTextChangedListener(this); + } + + private String formatColor(int color) { + return shouldTrimAlphaDigits() + ? String.format("%06x", color & 0x00ffffff) + : String.format("%08x", color); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + int color = parseHexColor(s); + if (shouldTrimAlphaDigits()) color = color | 0xff000000; + observableColor.updateColor(color, this); + } + + @Override + public void afterTextChanged(Editable s) { + } + + private boolean shouldTrimAlphaDigits() { + return hexEdit.getFilters() == withoutAlphaDigits; + } + } + + final MultiObserver multiObserver = new MultiObserver(); + hexEdit.addTextChangedListener(multiObserver); + observableColor.addObserver(multiObserver); + setShowAlphaDigits(hexEdit, true); + } + + private static int parseHexColor(CharSequence s) { + try { + return (int)(Long.parseLong(s.toString(), 16) & 0xffffffffL); + } + catch (NumberFormatException e) { + return Color.GRAY; + } + } + + public static void setShowAlphaDigits(final EditText hexEdit, boolean showAlphaDigits) { + hexEdit.setFilters(showAlphaDigits ? withAlphaDigits : withoutAlphaDigits); + hexEdit.setText(hexEdit.getText()); // trigger a reformat of text + } + + + private static class ColorPasteLengthFilter implements InputFilter { + + private static final int MAX_LENGTH = 6; + private static final int PASTED_LEN = 8; + private final InputFilter sixDigitFilter = new InputFilter.LengthFilter(MAX_LENGTH); + + public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { + // If 8 digits have been pasted, replacing all source, trim alpha digits. + // Otherwise standard LengthFilter behavior. + final int srcLength = end - start; + final int dstSelLength = dend - dstart; + if (srcLength == PASTED_LEN && dstSelLength == dest.length()) { + // Discard alpha digits: + return source.subSequence(PASTED_LEN - MAX_LENGTH, PASTED_LEN); + } + else { + return sixDigitFilter.filter(source, start, end, dest, dstart, dend); + } + } + } + +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/HueSatView.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/HueSatView.java new file mode 100644 index 000000000..6a83f79a0 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/HueSatView.java @@ -0,0 +1,205 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.MotionEvent; + +public class HueSatView extends SquareView implements ColorObserver { + + private Paint borderPaint; + private Paint pointerPaint; + private Path pointerPath; + private Path borderPath; + private Rect viewRect = new Rect(); + private int w; + private int h; + + // Bitmap is generated once and shared across instances. + private static Bitmap bitmap; + + private final PointF pointer = new PointF(); + private ObservableColor observableColor = new ObservableColor(0); + + public HueSatView(Context context) { + super(context); + + borderPaint = Resources.makeLinePaint(); + pointerPaint = Resources.makeLinePaint(); + pointerPaint.setColor(0xff000000); + pointerPath = Resources.makePointerPath(); + borderPath = new Path(); + if (bitmap == null) { + bitmap = makeBitmap(optimalBitmapSize()); + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + // Need clipPath() and setLayerType()... + throw new UnsupportedOperationException("Android API 10 and below is not supported."); + } + else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + // clipPath() is only supported on a software layer. + setLayerType(LAYER_TYPE_SOFTWARE, null); + } + } + + private int optimalBitmapSize() { + final int scale = 2; + final int maxBitmapSize = 128; + final DisplayMetrics dm = getResources().getDisplayMetrics(); + return Math.min(maxBitmapSize, Math.min(dm.widthPixels, dm.heightPixels) / scale); + } + + public void observeColor(ObservableColor observableColor) { + this.observableColor = observableColor; + observableColor.addObserver(this); + } + + @Override + public void updateColor(ObservableColor observableColor) { + setPointer(pointer, observableColor.getHue(), observableColor.getSat(), w); + optimisePointerColor(); + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + this.w = w; + this.h = h; + viewRect.set(0, 0, w, h); + + float inset = borderPaint.getStrokeWidth() / 2; + makeBorderPath(borderPath, w, h, inset); + + // Sets pointer position + updateColor(observableColor); + } + + private static void makeBorderPath(Path borderPath, int w, int h, float inset) { + w -= inset; + h -= inset; + borderPath.reset(); + borderPath.moveTo(w, inset); + borderPath.lineTo(w, h); + borderPath.lineTo(inset, h); + borderPath.addArc(new RectF(inset, inset, 2 * w, 2 * h), 180, 270); + borderPath.close(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + boolean withinPicker = clamp(pointer, event.getX(), event.getY(), true); + if (withinPicker) update(); + return withinPicker; + case MotionEvent.ACTION_MOVE: + clamp(pointer, event.getX(), event.getY(), false); + update(); + getParent().requestDisallowInterceptTouchEvent(true); + return true; + } + return super.onTouchEvent(event); + } + + private void update() { + observableColor.updateHueSat( + hueForPos(pointer.x, pointer.y, w), + satForPos(pointer.x, pointer.y, w), + this); + optimisePointerColor(); + invalidate(); + } + + private boolean clamp(PointF pointer, float x, float y, boolean rejectOutside) { + x = Math.min(x, w); + y = Math.min(y, h); + final float dx = w - x; + final float dy = h - y; + final float r = (float)Math.sqrt(dx * dx + dy * dy); + boolean outside = r > w; + if (!outside || !rejectOutside) { + if (outside) { + x = w - dx * w / r; + y = w - dy * w / r; + } + pointer.set(x, y); + } + return !outside; + } + + private void optimisePointerColor() { + pointerPaint.setColor( + observableColor.getLightnessWithValue(1) > 0.5 + ? 0xff000000 : 0xffffffff); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.save(); + + canvas.clipPath(borderPath); + canvas.drawBitmap(bitmap, null, viewRect, null); + canvas.translate(pointer.x, pointer.y); + canvas.drawPath(pointerPath, pointerPaint); + + canvas.restore(); + + canvas.drawPath(borderPath, borderPaint); + } + + private static Bitmap makeBitmap(int radiusPx) { + int[] colors = new int[radiusPx * radiusPx]; + float[] hsv = new float[]{0f, 0f, 1f}; + for (int y = 0; y < radiusPx; ++y) { + for (int x = 0; x < radiusPx; ++x) { + final int i = x + y * radiusPx; + final float sat = satForPos(x, y, radiusPx); + final float arcBleed = 2f / radiusPx; // extend curved edge pixels just outside clip area. + if (sat <= 1 + arcBleed) { + hsv[0] = hueForPos(x, y, radiusPx); + hsv[1] = sat; + colors[i] = Color.HSVToColor(0xff, hsv); + } + } + } + return Bitmap.createBitmap(colors, radiusPx, radiusPx, Bitmap.Config.ARGB_8888); + } + + private static float hueForPos(float x, float y, float radiusPx) { + final double r = radiusPx - 1; // gives values 0...1 inclusive + final double dx = (r - x) / r; + final double dy = (r - y) / r; + final double angle = Math.atan2(dy, dx); + final double hue = 360 * angle / (Math.PI / 2); + return (float)hue; + } + + private static float satForPos(float x, float y, float radiusPx) { + final double r = radiusPx - 1; // gives values 0...1 inclusive + final double dx = (r - x) / r; + final double dy = (r - y) / r; + final double sat = dx * dx + dy * dy; // leave it squared -- exaggerates pale colors + return (float)sat; + } + + private static void setPointer(PointF pointer, float hue, float sat, float radiusPx) { + final float r = radiusPx - 1; // for values 0...1 inclusive + final double distance = r * Math.sqrt(sat); + final double angle = hue / 360 * Math.PI / 2; + final double dx = distance * Math.cos(angle); + final double dy = distance * Math.sin(angle); + pointer.set(r - (float)dx, r - (float)dy); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ObservableColor.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ObservableColor.java new file mode 100644 index 000000000..8cf482ec1 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ObservableColor.java @@ -0,0 +1,97 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import android.graphics.Color; + +import java.util.ArrayList; +import java.util.List; + +public class ObservableColor { + + // Store as HSV & A, otherwise round-trip to int causes color drift. + private final float[] hsv = {0, 0, 0}; + private int alpha; + private final List observers = new ArrayList(); + + public ObservableColor(int color) { + Color.colorToHSV(color, hsv); + alpha = Color.alpha(color); + } + + public void getHsv(float hsvOut[]) { + hsvOut[0] = hsv[0]; + hsvOut[1] = hsv[1]; + hsvOut[2] = hsv[2]; + } + + public int getColor() { + return Color.HSVToColor(alpha, hsv); + } + + public float getHue() { + return hsv[0]; + } + + public float getSat() { + return hsv[1]; + } + + public float getValue() { + return hsv[2]; + } + + public int getAlpha() { + return alpha; + } + + public float getLightness() { + return getLightnessWithValue(hsv[2]); + } + + public float getLightnessWithValue(float value) { + float[] hsV = {hsv[0], hsv[1], value}; + final int color = Color.HSVToColor(hsV); + return (Color.red(color) * 0.2126f + Color.green(color) * 0.7152f + Color.blue(color) * 0.0722f)/0xff; + } + + public void addObserver(ColorObserver observer) { + observers.add(observer); + } + + public void updateHueSat(float hue, float sat, ColorObserver sender) { + hsv[0] = hue; + hsv[1] = sat; + notifyOtherObservers(sender); + } + + public void updateValue(float value, ColorObserver sender) { + hsv[2] = value; + notifyOtherObservers(sender); + } + + public void updateAlpha(int alpha, ColorObserver sender) { + this.alpha = alpha; + notifyOtherObservers(sender); + } + + public void updateColor(int color, ColorObserver sender) { + Color.colorToHSV(color, hsv); + alpha = Color.alpha(color); + notifyOtherObservers(sender); + } + + public void updateColor(int alpha, float hue, float sat, float bri, ColorObserver sender) { + this.alpha = alpha; + hsv[0] = hue; + hsv[1] = sat; + hsv[2] = bri; + notifyOtherObservers(sender); + } + + private void notifyOtherObservers(ColorObserver sender) { + for (ColorObserver observer : observers) { + if (observer != sender) { + observer.updateColor(this); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/Resources.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/Resources.java new file mode 100644 index 000000000..df66096d3 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/Resources.java @@ -0,0 +1,46 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import static ru.vtosters.lite.utils.AndroidUtils.dp2px; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapShader; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Shader; + +import ru.vtosters.lite.utils.AndroidUtils; + +class Resources { + + private static final float LINE_WIDTH_DIP = 1.5f; + private static final float POINTER_RADIUS_DIP = 7; + private static final int VIEW_OUTLINE_COLOR = 0xff808080; + + public static Paint makeLinePaint() { + Paint paint = new Paint(); + paint.setColor(VIEW_OUTLINE_COLOR); + paint.setStrokeWidth(dp2px(LINE_WIDTH_DIP)); + paint.setStyle(Paint.Style.STROKE); + paint.setAntiAlias(true); + return paint; + } + + public static Paint makeCheckerPaint(Context context) { + Paint paint = new Paint(); + final Bitmap checkerBmp = BitmapFactory.decodeResource(context.getResources(), AndroidUtils.getIdentifier("checker_background", "drawable")); + paint.setShader(new BitmapShader(checkerBmp, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); + paint.setStrokeWidth(dp2px(LINE_WIDTH_DIP)); + paint.setStyle(Paint.Style.FILL); + paint.setAntiAlias(true); + return paint; + } + + public static Path makePointerPath() { + Path pointerPath = new Path(); + final float radiusPx = dp2px(POINTER_RADIUS_DIP); + pointerPath.addCircle(0, 0, radiusPx, Path.Direction.CW); + return pointerPath; + } +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/SliderViewBase.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/SliderViewBase.java new file mode 100644 index 000000000..90e28b064 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/SliderViewBase.java @@ -0,0 +1,112 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.MotionEvent; +import android.view.View; + +public abstract class SliderViewBase extends View { + + private final Paint borderPaint; + private final Paint checkerPaint; + private final Rect viewRect = new Rect(); + private int w; + private int h; + private final Path borderPath; + private Bitmap bitmap; + private final Path pointerPath; + private final Paint pointerPaint; + + private float currentPos; + + public SliderViewBase(Context context) { + super(context); + checkerPaint = Resources.makeCheckerPaint(context); + borderPaint = Resources.makeLinePaint(); + pointerPaint = Resources.makeLinePaint(); + pointerPath = Resources.makePointerPath(); + borderPath = new Path(); + } + + protected abstract void notifyListener(float currentPos); + + protected abstract Bitmap makeBitmap(int w, int h); + + protected abstract int getPointerColor(float currentPos); + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + this.w = w; + this.h = h; + viewRect.set(0, 0, w, h); + float inset = borderPaint.getStrokeWidth() / 2; + borderPath.reset(); + borderPath.addRect(new RectF(inset, inset, w - inset, h - inset), Path.Direction.CW); + updateBitmap(); + } + + protected void setPos(float pos) { + currentPos = pos; + optimisePointerColor(); + } + + protected void updateBitmap() { + if (w > 0 && h > 0) { + bitmap = makeBitmap(w, h); + optimisePointerColor(); + } + // else not ready yet + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + currentPos = valueForTouchPos(event.getX(), event.getY()); + optimisePointerColor(); + notifyListener(currentPos); + invalidate(); + getParent().requestDisallowInterceptTouchEvent(true); + return true; + } + return super.onTouchEvent(event); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawPath(borderPath, checkerPaint); + canvas.drawBitmap(bitmap, null, viewRect, null); + canvas.drawPath(borderPath, borderPaint); + + canvas.save(); + if (isWide()) { + canvas.translate(w * currentPos, h / 2); + } + else { + canvas.translate(w / 2, h * (1 - currentPos)); + } + canvas.drawPath(pointerPath, pointerPaint); + canvas.restore(); + } + + private boolean isWide() { + return w > h; + } + + private float valueForTouchPos(float x, float y) { + final float val = isWide() ? x / w : 1 - y / h; + return Math.max(0, Math.min(1, val)); + } + + private void optimisePointerColor() { + pointerPaint.setColor(getPointerColor(currentPos)); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/SquareView.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/SquareView.java new file mode 100644 index 000000000..813aaee6c --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/SquareView.java @@ -0,0 +1,38 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import static ru.vtosters.lite.utils.AndroidUtils.dp2px; + +import android.content.Context; +import android.view.View; + +class SquareView extends View { + private static final int MIN_SIZE_DIP = 200; + + private final int minSizePx; + + public SquareView(Context context) { + super(context); + minSizePx = dp2px(MIN_SIZE_DIP); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Constrain to square + final int w = MeasureSpec.getSize(widthMeasureSpec); + final int h = MeasureSpec.getSize(heightMeasureSpec); + final int modeW = MeasureSpec.getMode(widthMeasureSpec); + final int modeH = MeasureSpec.getMode(heightMeasureSpec); + int size = minSizePx; + if (modeW == MeasureSpec.UNSPECIFIED) { + size = h; + } + else if (modeH == MeasureSpec.UNSPECIFIED) { + size = w; + } + else { + size = Math.min(w, h); + } + size = Math.max(size, minSizePx); + setMeasuredDimension(size, size); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/SwatchView.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/SwatchView.java new file mode 100644 index 000000000..e2c9cf909 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/SwatchView.java @@ -0,0 +1,125 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; + +import ru.vtosters.lite.utils.AndroidUtils; + +public class SwatchView extends SquareView implements ColorObserver { + + private final Paint borderPaint; + private final Path borderPath; + private final Paint checkerPaint; + private final Path oldFillPath; + private final Path newFillPath; + private final Paint oldFillPaint; + private final Paint newFillPaint; + private final float radialMarginPx; + + public SwatchView(Context context) { + super(context); + + radialMarginPx = AndroidUtils.dp2px(16); + + borderPaint = Resources.makeLinePaint(); + checkerPaint = Resources.makeCheckerPaint(context); + oldFillPaint = new Paint(); + newFillPaint = new Paint(); + + borderPath = new Path(); + oldFillPath = new Path(); + newFillPath = new Path(); + } + + void setOriginalColor(int color) { + oldFillPaint.setColor(color); + invalidate(); + } + + void observeColor(ObservableColor observableColor) { + observableColor.addObserver(this); + } + + @Override + public void updateColor(ObservableColor observableColor) { + newFillPaint.setColor(observableColor.getColor()); + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + // We expect to be stacked behind a square HueSatView and fill the top left corner. + + final float inset = borderPaint.getStrokeWidth() / 2; + final float r = Math.min(w, h); + + // Trim the corners of the swatch is where it is radialMarginPx thick. + // find how long the outeredges are: + final float margin = radialMarginPx; + final float diagonal = r + 2 * margin; + final float opp = (float)Math.sqrt(diagonal * diagonal - r * r); + final float edgeLen = r - opp; + + // Arc angles for drawing CCW + final float outerAngle = (float)Math.toDegrees(Math.atan2(opp, r)); + final float startAngle = 270 - outerAngle; + // Sweep angle for each half of the swatch: + final float sweepAngle = outerAngle - 45; + // Sweep angle for the smooth corners: + final float cornerSweepAngle = 90 - outerAngle; + + // Outer border: + beginBorder(borderPath, inset, edgeLen, margin, cornerSweepAngle); + mainArc(borderPath, r, margin, startAngle, 2 * sweepAngle); + endBorder(borderPath, inset, edgeLen, margin, cornerSweepAngle); + + // Old fill shape: + oldFillPath.reset(); + oldFillPath.moveTo(inset, inset); + mainArc(oldFillPath, r, margin, 225, sweepAngle); + endBorder(oldFillPath, inset, edgeLen, margin, cornerSweepAngle); + + // New fill shape: + beginBorder(newFillPath, inset, edgeLen, margin, cornerSweepAngle); + mainArc(newFillPath, r, margin, startAngle, sweepAngle); + newFillPath.lineTo(inset, inset); + newFillPath.close(); + } + + private static void beginBorder(Path path, float inset, float edgeLen, float cornerRadius, float cornerSweepAngle) { + path.reset(); + path.moveTo(inset, inset); + cornerArc(path, edgeLen, inset, cornerRadius-inset, 0, cornerSweepAngle); + } + + private static void endBorder(Path path, float inset, float edgeLen, float cornerRadius, float cornerSweepAngle) { + cornerArc(path, inset, edgeLen, cornerRadius - inset, 90 - cornerSweepAngle, cornerSweepAngle); + path.lineTo(inset, inset); + path.close(); + } + + private static void cornerArc(Path path, float cx, float cy, float r, float startAngle, float sweepAngle) { + final RectF ovalRect = new RectF(-r, -r, r, r); + ovalRect.offset(cx, cy); + path.arcTo(ovalRect, startAngle, sweepAngle); + } + + private static void mainArc(Path path, float viewSize, float margin, float startAngle, float sweepAngle) { + float r = viewSize + margin; + final RectF ovalRect = new RectF(-r, -r, r, r); + ovalRect.offset(viewSize, viewSize); + path.arcTo(ovalRect, startAngle, sweepAngle); + } + + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawPath(borderPath, checkerPaint); + canvas.drawPath(oldFillPath, oldFillPaint); + canvas.drawPath(newFillPath, newFillPaint); + canvas.drawPath(borderPath, borderPaint); + } + +} diff --git a/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ValueView.java b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ValueView.java new file mode 100644 index 000000000..ad4444f15 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/views/rarepebble/ValueView.java @@ -0,0 +1,56 @@ +package ru.vtosters.lite.ui.views.rarepebble; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; + +public class ValueView extends SliderViewBase implements ColorObserver { + + private ObservableColor observableColor = new ObservableColor(0); + + public ValueView(Context context) { + super(context); + } + + public void observeColor(ObservableColor observableColor) { + this.observableColor = observableColor; + observableColor.addObserver(this); + } + + @Override + public void updateColor(ObservableColor observableColor) { + setPos(this.observableColor.getValue()); + updateBitmap(); + invalidate(); + } + + @Override + protected void notifyListener(float currentPos) { + observableColor.updateValue(currentPos, this); + } + + @Override + protected int getPointerColor(float currentPos) { + float brightColorLightness = observableColor.getLightness(); + float posLightness = currentPos * brightColorLightness; + return posLightness > 0.5f ? 0xff000000 : 0xffffffff; + } + + protected Bitmap makeBitmap(int w, int h) { + final boolean isWide = w > h; + final int n = Math.max(w, h); + int[] colors = new int[n]; + + float[] hsv = new float[]{0, 0, 0}; + observableColor.getHsv(hsv); + + for (int i = 0; i < n; ++i) { + hsv[2] = isWide ? (float)i / n : 1 - (float)i / n; + colors[i] = Color.HSVToColor(hsv); + } + final int bmpWidth = isWide ? w : 1; + final int bmpHeight = isWide ? 1 : h; + return Bitmap.createBitmap(colors, bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888); + } + +} \ No newline at end of file diff --git a/smali/res/drawable/checker_background.png b/smali/res/drawable/checker_background.png new file mode 100644 index 000000000..284f90695 Binary files /dev/null and b/smali/res/drawable/checker_background.png differ