add color picker by Martin Stone
Add own modification of HSV-Alpha Color Picker for Android https://github.com/martin-stone/hsv-alpha-color-picker-android
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.vtosters.lite.ui.views.rarepebble;
|
||||
|
||||
public interface ColorObserver {
|
||||
|
||||
void updateColor(ObservableColor observableColor);
|
||||
}
|
||||
@@ -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.<br>
|
||||
* 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.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Alpha [0...255]</li>
|
||||
* <li>Hue [0...360)</li>
|
||||
* <li>Saturation [0...1]</li>
|
||||
* <li>Brightness [0...1]</li>
|
||||
* </ul>
|
||||
*/ 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<ColorObserver> observers = new ArrayList<ColorObserver>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 397 B |
Reference in New Issue
Block a user