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.
+ *
+ *
+ * - Alpha [0...255]
+ * - Hue [0...360)
+ * - Saturation [0...1]
+ * - Brightness [0...1]
+ *
+ */ 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