/*
* Copyright 2012 Lars Werkman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.ttbox.androgister.ui.admin.tag.holocolorpicker;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import eu.ttbox.androgister.R;
/**
* Displays a holo-themed color picker.
*
* <p>
* Use {@link #getColor()} to retrieve the selected color. <br>
* Use {@link #addSVBar(SVBar)} to add a Saturation/Value Bar. <br>
* Use {@link #addOpacityBar(OpacityBar)} to add a Opacity Bar.
* </p>
* <p>
* {@link https://github.com/LarsWerkman/HoloColorPicker/}
* </p>
*/
public class ColorPicker extends View {
/*
* Constants used to save/restore the instance state.
*/
private static final String STATE_PARENT = "parent";
private static final String STATE_ANGLE = "angle";
private static final String STATE_OLD_COLOR = "color";
/**
* Colors to construct the color wheel using {@link SweepGradient}.
*
* <p>
* Note: The algorithm in {@link #normalizeColor(int)} highly depends on
* these exact values. Be aware that {@link #setColor(int)} might break if
* you change this array.
* </p>
*/
private static final int[] COLORS = new int[] { 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000 };
/**
* {@code Paint} instance used to draw the color wheel.
*/
private Paint mColorWheelPaint;
/**
* {@code Paint} instance used to draw the pointer's "halo".
*/
private Paint mPointerHaloPaint;
/**
* {@code Paint} instance used to draw the pointer (the selected color).
*/
private Paint mPointerColor;
/**
* The width of the color wheel thickness.
*/
private int mColorWheelThickness;
/**
* The radius of the color wheel.
*/
private int mColorWheelRadius;
private int mPreferredColorWheelRadius;
/**
* The radius of the center circle inside the color wheel.
*/
private int mColorCenterRadius;
private int mPreferredColorCenterRadius;
/**
* The radius of the halo of the center circle inside the color wheel.
*/
private int mColorCenterHaloRadius;
private int mPreferredColorCenterHaloRadius;
/**
* The radius of the pointer.
*/
private int mColorPointerRadius;
/**
* The radius of the halo of the pointer.
*/
private int mColorPointerHaloRadius;
/**
* The rectangle enclosing the color wheel.
*/
private RectF mColorWheelRectangle = new RectF();
/**
* The rectangle enclosing the center inside the color wheel.
*/
private RectF mCenterRectangle = new RectF();
/**
* {@code true} if the user clicked on the pointer to start the move mode. <br>
* {@code false} once the user stops touching the screen.
*
* @see #onTouchEvent(MotionEvent)
*/
private boolean mUserIsMovingPointer = false;
/**
* The ARGB value of the currently selected color.
*/
private int mColor;
/**
* The ARGB value of the center with the old selected color.
*/
private int mCenterOldColor;
/**
* The ARGB value of the center with the new selected color.
*/
private int mCenterNewColor;
/**
* Number of pixels the origin of this view is moved in X- and Y-direction.
*
* <p>
* We use the center of this (quadratic) View as origin of our internal
* coordinate system. Android uses the upper left corner as origin for the
* View-specific coordinate system. So this is the value we use to translate
* from one coordinate system to the other.
* </p>
*
* <p>
* Note: (Re)calculated in {@link #onMeasure(int, int)}.
* </p>
*
* @see #onDraw(Canvas)
*/
private float mTranslationOffset;
/**
* The pointer's position expressed as angle (in rad).
*/
private float mAngle;
/**
* {@code Paint} instance used to draw the center with the old selected
* color.
*/
private Paint mCenterOldPaint;
/**
* {@code Paint} instance used to draw the center with the new selected
* color.
*/
private Paint mCenterNewPaint;
/**
* {@code Paint} instance used to draw the halo of the center selected
* colors.
*/
private Paint mCenterHaloPaint;
/**
* An array of floats that can be build into a {@code Color} <br>
* Where we can extract the Saturation and Value from.
*/
private float[] mHSV = new float[3];
/**
* {@code SVBar} instance used to control the Saturation/Value bar.
*/
private SVBar mSVbar = null;
/**
* {@code OpacityBar} instance used to control the Opacity bar.
*/
private OpacityBar mOpacityBar = null;
/**
* {@code onColorChangedListener} instance of the onColorChangedListener
*/
private OnColorChangedListener onColorChangedListener;
public ColorPicker(Context context) {
super(context);
init(null, 0);
}
public ColorPicker(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0);
}
public ColorPicker(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
/**
* An interface that is called whenever the color is changed. Currently it
* is always called when the color is changes.
*
* @author lars
*
*/
public interface OnColorChangedListener {
public void onColorChanged(int color);
}
/**
* Set a onColorChangedListener
*
* @param {@code OnColorChangedListener}
*/
public void setOnColorChangedListener(OnColorChangedListener listener) {
this.onColorChangedListener = listener;
}
/**
* Gets the onColorChangedListener
*
* @return {@code OnColorChangedListener}
*/
public OnColorChangedListener getOnColorChangedListener() {
return this.onColorChangedListener;
}
private void init(AttributeSet attrs, int defStyle) {
final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPicker, defStyle, 0);
final Resources b = getContext().getResources();
mColorWheelThickness = a.getDimensionPixelSize(R.styleable.ColorPicker_color_wheel_thickness, b.getDimensionPixelSize(R.dimen.color_wheel_thickness));
mColorWheelRadius = a.getDimensionPixelSize(R.styleable.ColorPicker_color_wheel_radius, b.getDimensionPixelSize(R.dimen.color_wheel_radius));
mPreferredColorWheelRadius = mColorWheelRadius;
mColorCenterRadius = a.getDimensionPixelSize(R.styleable.ColorPicker_color_center_radius, b.getDimensionPixelSize(R.dimen.color_center_radius));
mPreferredColorCenterRadius = mColorCenterRadius;
mColorCenterHaloRadius = a.getDimensionPixelSize(R.styleable.ColorPicker_color_center_halo_radius, b.getDimensionPixelSize(R.dimen.color_center_halo_radius));
mPreferredColorCenterHaloRadius = mColorCenterHaloRadius;
mColorPointerRadius = a.getDimensionPixelSize(R.styleable.ColorPicker_color_pointer_radius, b.getDimensionPixelSize(R.dimen.color_pointer_radius));
mColorPointerHaloRadius = a.getDimensionPixelSize(R.styleable.ColorPicker_color_pointer_halo_radius, b.getDimensionPixelSize(R.dimen.color_pointer_halo_radius));
a.recycle();
mAngle = (float) (-Math.PI / 2);
Shader s = new SweepGradient(0, 0, COLORS, null);
mColorWheelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mColorWheelPaint.setShader(s);
mColorWheelPaint.setStyle(Paint.Style.STROKE);
mColorWheelPaint.setStrokeWidth(mColorWheelThickness);
mPointerHaloPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPointerHaloPaint.setColor(Color.BLACK);
mPointerHaloPaint.setAlpha(0x50);
mPointerColor = new Paint(Paint.ANTI_ALIAS_FLAG);
mPointerColor.setColor(calculateColor(mAngle));
mCenterNewPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCenterNewPaint.setColor(calculateColor(mAngle));
mCenterNewPaint.setStyle(Paint.Style.FILL);
mCenterOldPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCenterOldPaint.setColor(calculateColor(mAngle));
mCenterOldPaint.setStyle(Paint.Style.FILL);
mCenterHaloPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCenterHaloPaint.setColor(Color.BLACK);
mCenterHaloPaint.setAlpha(0x00);
}
@Override
protected void onDraw(Canvas canvas) {
// All of our positions are using our internal coordinate system.
// Instead of translating
// them we let Canvas do the work for us.
canvas.translate(mTranslationOffset, mTranslationOffset);
// Draw the color wheel.
canvas.drawOval(mColorWheelRectangle, mColorWheelPaint);
float[] pointerPosition = calculatePointerPosition(mAngle);
// Draw the pointer's "halo"
canvas.drawCircle(pointerPosition[0], pointerPosition[1], mColorPointerHaloRadius, mPointerHaloPaint);
// Draw the pointer (the currently selected color) slightly smaller on
// top.
canvas.drawCircle(pointerPosition[0], pointerPosition[1], mColorPointerRadius, mPointerColor);
// Draw the halo of the center colors.
canvas.drawCircle(0, 0, mColorCenterHaloRadius, mCenterHaloPaint);
// Draw the old selected color in the center.
canvas.drawArc(mCenterRectangle, 90, 180, true, mCenterOldPaint);
// Draw the new selected color in the center.
canvas.drawArc(mCenterRectangle, 270, 180, true, mCenterNewPaint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int intrinsicSize = 2 * (mPreferredColorWheelRadius + mColorPointerHaloRadius);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(intrinsicSize, widthSize);
} else {
width = intrinsicSize;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(intrinsicSize, heightSize);
} else {
height = intrinsicSize;
}
int min = Math.min(width, height);
setMeasuredDimension(min, min);
mTranslationOffset = min * 0.5f;
// fill the rectangle instances.
mColorWheelRadius = min / 2 - mColorWheelThickness - (mColorPointerHaloRadius / 2);
mColorWheelRectangle.set(-mColorWheelRadius, -mColorWheelRadius, mColorWheelRadius, mColorWheelRadius);
mColorCenterRadius = (int) ((float) mPreferredColorCenterRadius * ((float) mColorWheelRadius / (float) mPreferredColorWheelRadius));
mColorCenterHaloRadius = (int) ((float) mPreferredColorCenterHaloRadius * ((float) mColorWheelRadius / (float) mPreferredColorWheelRadius));
mCenterRectangle.set(-mColorCenterRadius, -mColorCenterRadius, mColorCenterRadius, mColorCenterRadius);
}
private int ave(int s, int d, float p) {
return s + java.lang.Math.round(p * (d - s));
}
/**
* Calculate the color using the supplied angle.
*
* @param angle
* The selected color's position expressed as angle (in rad).
*
* @return The ARGB value of the color on the color wheel at the specified
* angle.
*/
private int calculateColor(float angle) {
float unit = (float) (angle / (2 * Math.PI));
if (unit < 0) {
unit += 1;
}
if (unit <= 0) {
mColor = COLORS[0];
return COLORS[0];
}
if (unit >= 1) {
mColor = COLORS[COLORS.length - 1];
return COLORS[COLORS.length - 1];
}
float p = unit * (COLORS.length - 1);
int i = (int) p;
p -= i;
int c0 = COLORS[i];
int c1 = COLORS[i + 1];
int a = ave(Color.alpha(c0), Color.alpha(c1), p);
int r = ave(Color.red(c0), Color.red(c1), p);
int g = ave(Color.green(c0), Color.green(c1), p);
int b = ave(Color.blue(c0), Color.blue(c1), p);
mColor = Color.argb(a, r, g, b);
return Color.argb(a, r, g, b);
}
/**
* Get the currently selected color.
*
* @return The ARGB value of the currently selected color.
*/
public int getColor() {
return mCenterNewColor;
}
/**
* Set the color to be highlighted by the pointer. </br> </br> If the
* instances {@code SVBar} and the {@code OpacityBar} aren't null the color
* will also be set to them
*
* @param color
* The RGB value of the color to highlight. If this is not a
* color displayed on the color wheel a very simple algorithm is
* used to map it to the color wheel. The resulting color often
* won't look close to the original color. This is especially
* true for shades of grey. You have been warned!
*/
public void setColor(int color) {
mAngle = colorToAngle(color);
mPointerColor.setColor(calculateColor(mAngle));
// check of the instance isn't null
if (mOpacityBar != null) {
// set the value of the opacity
mOpacityBar.setColor(mColor);
mOpacityBar.setOpacity(Color.alpha(color));
}
// check if the instance isn't null
if (mSVbar != null) {
// the array mHSV will be filled with the HSV values of the color.
Color.colorToHSV(color, mHSV);
mSVbar.setColor(mColor);
// because of the design of the Saturation/Value bar,
// we can only use Saturation or Value every time.
// Here will be checked which we shall use.
if (mHSV[1] < mHSV[2]) {
mSVbar.setSaturation(mHSV[1]);
} else { // if (mHSV[1] > mHSV[2]) {
mSVbar.setValue(mHSV[2]);
}
}
invalidate();
}
/**
* Convert a color to an angle.
*
* @param color
* The RGB value of the color to "find" on the color wheel.
* {@link #normalizeColor(int)} will be used to map this color to
* one on the color wheel if necessary.
*
* @return The angle (in rad) the "normalized" color is displayed on the
* color wheel.
*/
private float colorToAngle(int color) {
int[] colorInfo = normalizeColor(color);
int normColor = colorInfo[0];
int colorMask = colorInfo[1];
int shiftValue = colorInfo[2];
int anchorColor = (normColor & ~colorMask);
// Find the "anchor" color in the COLORS array
for (int i = 0; i < COLORS.length - 1; i++) {
if (COLORS[i] == anchorColor) {
int nextValue = COLORS[i + 1];
double value;
double decimals = ((normColor >> shiftValue) & 0xFF) / 255D;
// Find out if the gradient our color belongs to goes from the
// element just found to
// the next element in the array.
if ((nextValue & colorMask) != (anchorColor & colorMask)) {
// Compute value depending of the gradient direction
if (nextValue < anchorColor) {
value = i + 1 - decimals;
} else {
value = i + decimals;
}
} else {
// It's a gradient from this element to the previous element
// in the array.
// Wrap to the end of the array if the "anchor" color is the
// first element.
int index = (i == 0) ? COLORS.length - 1 : i;
int prevValue = COLORS[index - 1];
// Compute value depending of the gradient direction
if (prevValue < anchorColor) {
value = index - 1 + decimals;
} else {
value = index - decimals;
}
}
// Calculate the angle in rad (from -PI to PI)
float angle = (float) (2 * Math.PI * value / (COLORS.length - 1));
if (angle > Math.PI) {
angle -= 2 * Math.PI;
}
return angle;
}
}
// This shouldn't happen
return 0;
}
/**
* "Normalize" the supplied color.
*
* <p>
* This will set the lowest value of R,G,B to 0, the highest to 255, and
* will keep the middle value.<br>
* For values close to those on the color wheel this will result in close
* matches. For other values, especially shades of grey this will produce
* funny results.
* </p>
*
* @param color
* The color to "normalize".
*
* @return An {@code int} array with the following contents:
* <ol>
* <li>The ARGB value of the "normalized" color.</li>
* <li>A mask with all bits {@code 0} but those for the byte
* representing the "middle value" that remains unchanged in the
* "normalized" color.</li>
* <li>The number of bits the "normalized" color has to be shifted
* to the right so the "middle value" is in the lower 8 bits.</li>
* </ol>
*/
private int[] normalizeColor(int color) {
// Remove S and V from color
Color.colorToHSV(color, mHSV);
mHSV[1] = 1f;
mHSV[2] = 1f;
color = Color.HSVToColor(mHSV);
// Normalize
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
int newRed = red;
int newGreen = green;
int newBlue = blue;
int maskRed = 0;
int maskGreen = 0;
int maskBlue = 0;
int shiftValue;
if (red <= green && red <= blue) {
// Red is the smallest component
newRed = 0;
if (green > blue) {
// Green is the largest component
shiftValue = 0;
maskBlue = 0xFF;
newGreen = 0xFF;
} else {
// We make blue the largest component
shiftValue = 8;
maskGreen = 0xFF;
newBlue = 0xFF;
}
} else if (green <= red && green <= blue) {
// Green is the smallest component
newGreen = 0;
if (red > blue) {
// Red is the largest component
shiftValue = 0;
maskBlue = 0xFF;
newRed = 0xFF;
} else {
// We make blue the largest component
shiftValue = 16;
maskRed = 0xFF;
newBlue = 0xFF;
}
} else {
// We make blue the smallest component
newBlue = 0;
if (red > green) {
// Red is the largest component
shiftValue = 8;
maskGreen = 0xFF;
newRed = 0xFF;
} else {
// We make green the largest component
shiftValue = 16;
maskRed = 0xFF;
newGreen = 0xFF;
}
}
int normColor = Color.argb(255, newRed, newGreen, newBlue);
int colorMask = Color.argb(0, maskRed, maskGreen, maskBlue);
return new int[] { normColor, colorMask, shiftValue };
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Convert coordinates to our internal coordinate system
float x = event.getX() - mTranslationOffset;
float y = event.getY() - mTranslationOffset;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Check whether the user pressed on the pointer.
float[] pointerPosition = calculatePointerPosition(mAngle);
if (x >= (pointerPosition[0] - mColorPointerHaloRadius) && x <= (pointerPosition[0] + mColorPointerHaloRadius) && y >= (pointerPosition[1] - mColorPointerHaloRadius)
&& y <= (pointerPosition[1] + mColorPointerHaloRadius)) {
mUserIsMovingPointer = true;
invalidate();
}
// Check wheter the user pressed on the center.
if (x >= -mColorCenterRadius && x <= mColorCenterRadius && y >= -mColorCenterRadius && y <= mColorCenterRadius) {
mCenterHaloPaint.setAlpha(0x50);
setColor(getOldCenterColor());
mCenterNewPaint.setColor(getOldCenterColor());
invalidate();
}
break;
case MotionEvent.ACTION_MOVE:
if (mUserIsMovingPointer) {
mAngle = (float) java.lang.Math.atan2(y, x);
mPointerColor.setColor(calculateColor(mAngle));
// Check wheter there is a Saturation/Value bar or/and
// OpacityBar connected.
if (mSVbar != null && mOpacityBar == null) {
mSVbar.setColor(mColor);
} else if (mSVbar == null && mOpacityBar != null) {
mOpacityBar.setColor(mColor);
} else if (mSVbar != null && mOpacityBar != null) {
mOpacityBar.setColor(mColor);
mSVbar.setColor(mColor);
} else {
setNewCenterColor(mCenterNewColor = calculateColor(mAngle));
}
invalidate();
}
break;
case MotionEvent.ACTION_UP:
mUserIsMovingPointer = false;
mCenterHaloPaint.setAlpha(0x00);
invalidate();
break;
}
getParent().requestDisallowInterceptTouchEvent(true);
return true;
}
/**
* Calculate the pointer's coordinates on the color wheel using the supplied
* angle.
*
* @param angle
* The position of the pointer expressed as angle (in rad).
*
* @return The coordinates of the pointer's center in our internal
* coordinate system.
*/
private float[] calculatePointerPosition(float angle) {
float x = (float) (mColorWheelRadius * Math.cos(angle));
float y = (float) (mColorWheelRadius * Math.sin(angle));
return new float[] { x, y };
}
/**
* Add a Saturation/Value bar to the color wheel.
*
* @param bar
* The instance of the Saturation/Value bar.
*/
public void addSVBar(SVBar bar) {
mSVbar = bar;
// Give an instance of the color picker to the Saturation/Value bar.
mSVbar.setColorPicker(this);
mSVbar.setColor(mColor);
}
/**
* Add a Opacity bar to the color wheel.
*
* @param bar
* The instance of the Opacity bar.
*/
public void addOpacityBar(OpacityBar bar) {
mOpacityBar = bar;
// Give an instance of the color picker to the Opacity bar.
mOpacityBar.setColorPicker(this);
mOpacityBar.setColor(mColor);
}
/**
* Change the color of the center which indicates the new color.
*
* @param color
* int of the color.
*/
public void setNewCenterColor(int color) {
mCenterNewColor = color;
mCenterNewPaint.setColor(color);
if (mCenterOldColor == 0) {
mCenterOldColor = color;
mCenterOldPaint.setColor(color);
}
if (onColorChangedListener != null) {
onColorChangedListener.onColorChanged(color);
}
invalidate();
}
/**
* Change the color of the center which indicates the old color.
*
* @param color
* int of the color.
*/
public void setOldCenterColor(int color) {
mCenterOldColor = color;
mCenterOldPaint.setColor(color);
invalidate();
}
public int getOldCenterColor() {
return mCenterOldColor;
}
/**
* Used to change the color of the {@code OpacityBar} used by the
* {@code SVBar} if there is an change in color.
*
* @param color
* int of the color used to change the opacity bar color.
*/
public void changeOpacityBarColor(int color) {
if (mOpacityBar != null) {
mOpacityBar.setColor(color);
}
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
Bundle state = new Bundle();
state.putParcelable(STATE_PARENT, superState);
state.putFloat(STATE_ANGLE, mAngle);
state.putInt(STATE_OLD_COLOR, mCenterOldColor);
return state;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
Bundle savedState = (Bundle) state;
Parcelable superState = savedState.getParcelable(STATE_PARENT);
super.onRestoreInstanceState(superState);
mAngle = savedState.getFloat(STATE_ANGLE);
setOldCenterColor(savedState.getInt(STATE_OLD_COLOR));
mPointerColor.setColor(calculateColor(mAngle));
}
}