2 * Copyright (C) 2010 Daniel Nilsson Copyright (C) 2012 THe CyanogenMod Project
3 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 * use this file except in compliance with the License. You may obtain a copy of
5 * the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
6 * applicable law or agreed to in writing, software distributed under the
7 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
8 * OF ANY KIND, either express or implied. See the License for the specific
9 * language governing permissions and limitations under the License.
12 package com.cyngn.eleven.widgets;
14 import android.content.Context;
15 import android.graphics.Canvas;
16 import android.graphics.Color;
17 import android.graphics.ComposeShader;
18 import android.graphics.LinearGradient;
19 import android.graphics.Paint;
20 import android.graphics.Paint.Align;
21 import android.graphics.Paint.Style;
22 import android.graphics.Point;
23 import android.graphics.PorterDuff;
24 import android.graphics.RectF;
25 import android.graphics.Shader;
26 import android.graphics.Shader.TileMode;
27 import android.util.AttributeSet;
28 import android.view.MotionEvent;
29 import android.view.View;
32 * Displays a color picker to the user and allow them to select a color. A
33 * slider for the alpha channel is also available. Enable it by setting
34 * setAlphaSliderVisible(boolean) to true.
36 * @author Daniel Nilsson
38 public class ColorPickerView extends View {
40 public interface OnColorChangedListener {
41 public void onColorChanged(int color);
44 private final static int PANEL_SAT_VAL = 0;
46 private final static int PANEL_HUE = 1;
48 private final static int PANEL_ALPHA = 2;
51 * The width in pixels of the border surrounding all color panels.
53 private final static float BORDER_WIDTH_PX = 1;
56 * The width in dp of the hue panel.
58 private float HUE_PANEL_WIDTH = 30f;
61 * The height in dp of the alpha panel
63 private float ALPHA_PANEL_HEIGHT = 20f;
66 * The distance in dp between the different color panels.
68 private float PANEL_SPACING = 10f;
71 * The radius in dp of the color palette tracker circle.
73 private float PALETTE_CIRCLE_TRACKER_RADIUS = 5f;
76 * The dp which the tracker of the hue or alpha panel will extend outside of
79 private float RECTANGLE_TRACKER_OFFSET = 2f;
81 private static float mDensity = 1f;
83 private OnColorChangedListener mListener;
85 private Paint mSatValPaint;
87 private Paint mSatValTrackerPaint;
89 private Paint mHuePaint;
91 private Paint mHueTrackerPaint;
93 private Paint mAlphaPaint;
95 private Paint mAlphaTextPaint;
97 private Paint mBorderPaint;
99 private Shader mValShader;
101 private Shader mSatShader;
103 private Shader mHueShader;
105 private Shader mAlphaShader;
107 private int mAlpha = 0xff;
109 private float mHue = 360f;
111 private float mSat = 0f;
113 private float mVal = 0f;
115 private String mAlphaSliderText = "Alpha";
117 private int mSliderTrackerColor = 0xff1c1c1c;
119 private int mBorderColor = 0xff6E6E6E;
121 private boolean mShowAlphaPanel = false;
124 * To remember which panel that has the "focus" when processing hardware
127 private int mLastTouchedPanel = PANEL_SAT_VAL;
130 * Offset from the edge we must have or else the finger tracker will get
131 * clipped when it is drawn outside of the view.
133 private float mDrawingOffset;
136 * Distance form the edges of the view of where we are allowed to draw.
138 private RectF mDrawingRect;
140 private RectF mSatValRect;
142 private RectF mHueRect;
144 private RectF mAlphaRect;
146 private AlphaPatternDrawable mAlphaPattern;
148 private Point mStartTouchPoint = null;
150 public ColorPickerView(final Context context) {
154 public ColorPickerView(final Context context, final AttributeSet attrs) {
155 this(context, attrs, 0);
158 public ColorPickerView(final Context context, final AttributeSet attrs, final int defStyle) {
159 super(context, attrs, defStyle);
163 private void init() {
164 mDensity = getContext().getResources().getDisplayMetrics().density;
165 PALETTE_CIRCLE_TRACKER_RADIUS *= mDensity;
166 RECTANGLE_TRACKER_OFFSET *= mDensity;
167 HUE_PANEL_WIDTH *= mDensity;
168 ALPHA_PANEL_HEIGHT *= mDensity;
169 PANEL_SPACING = PANEL_SPACING * mDensity;
171 mDrawingOffset = calculateRequiredOffset();
175 // Needed for receiving track ball motion events.
177 setFocusableInTouchMode(true);
180 private void initPaintTools() {
182 mSatValPaint = new Paint();
183 mSatValTrackerPaint = new Paint();
184 mHuePaint = new Paint();
185 mHueTrackerPaint = new Paint();
186 mAlphaPaint = new Paint();
187 mAlphaTextPaint = new Paint();
188 mBorderPaint = new Paint();
190 mSatValTrackerPaint.setStyle(Style.STROKE);
191 mSatValTrackerPaint.setStrokeWidth(2f * mDensity);
192 mSatValTrackerPaint.setAntiAlias(true);
194 mHueTrackerPaint.setColor(mSliderTrackerColor);
195 mHueTrackerPaint.setStyle(Style.STROKE);
196 mHueTrackerPaint.setStrokeWidth(2f * mDensity);
197 mHueTrackerPaint.setAntiAlias(true);
199 mAlphaTextPaint.setColor(0xff1c1c1c);
200 mAlphaTextPaint.setTextSize(14f * mDensity);
201 mAlphaTextPaint.setAntiAlias(true);
202 mAlphaTextPaint.setTextAlign(Align.CENTER);
203 mAlphaTextPaint.setFakeBoldText(true);
207 private float calculateRequiredOffset() {
208 float offset = Math.max(PALETTE_CIRCLE_TRACKER_RADIUS, RECTANGLE_TRACKER_OFFSET);
209 offset = Math.max(offset, BORDER_WIDTH_PX * mDensity);
211 return offset * 1.5f;
214 private int[] buildHueColorArray() {
216 final int[] hue = new int[361];
219 for (int i = hue.length - 1; i >= 0; i--, count++) {
220 hue[count] = Color.HSVToColor(new float[] {
229 protected void onDraw(final Canvas canvas) {
231 if (mDrawingRect.width() <= 0 || mDrawingRect.height() <= 0) {
235 drawSatValPanel(canvas);
236 drawHuePanel(canvas);
237 drawAlphaPanel(canvas);
241 private void drawSatValPanel(final Canvas canvas) {
243 final RectF rect = mSatValRect;
245 if (BORDER_WIDTH_PX > 0) {
246 mBorderPaint.setColor(mBorderColor);
247 canvas.drawRect(mDrawingRect.left, mDrawingRect.top, rect.right + BORDER_WIDTH_PX,
248 rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
251 if (mValShader == null) {
252 mValShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom,
253 0xffffffff, 0xff000000, TileMode.CLAMP);
256 final int rgb = Color.HSVToColor(new float[] {
260 mSatShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb,
262 final ComposeShader mShader = new ComposeShader(mValShader, mSatShader,
263 PorterDuff.Mode.MULTIPLY);
264 mSatValPaint.setShader(mShader);
266 canvas.drawRect(rect, mSatValPaint);
268 final Point p = satValToPoint(mSat, mVal);
270 mSatValTrackerPaint.setColor(0xff000000);
271 canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS - 1f * mDensity,
272 mSatValTrackerPaint);
274 mSatValTrackerPaint.setColor(0xffdddddd);
275 canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS, mSatValTrackerPaint);
279 private void drawHuePanel(final Canvas canvas) {
281 final RectF rect = mHueRect;
283 if (BORDER_WIDTH_PX > 0) {
284 mBorderPaint.setColor(mBorderColor);
285 canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right
286 + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
289 if (mHueShader == null) {
290 mHueShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom,
291 buildHueColorArray(), null, TileMode.CLAMP);
292 mHuePaint.setShader(mHueShader);
295 canvas.drawRect(rect, mHuePaint);
297 final float rectHeight = 4 * mDensity / 2;
299 final Point p = hueToPoint(mHue);
301 final RectF r = new RectF();
302 r.left = rect.left - RECTANGLE_TRACKER_OFFSET;
303 r.right = rect.right + RECTANGLE_TRACKER_OFFSET;
304 r.top = p.y - rectHeight;
305 r.bottom = p.y + rectHeight;
307 canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
311 private void drawAlphaPanel(final Canvas canvas) {
313 if (!mShowAlphaPanel || mAlphaRect == null || mAlphaPattern == null) {
317 final RectF rect = mAlphaRect;
319 if (BORDER_WIDTH_PX > 0) {
320 mBorderPaint.setColor(mBorderColor);
321 canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right
322 + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
325 mAlphaPattern.draw(canvas);
327 final float[] hsv = new float[] {
330 final int color = Color.HSVToColor(hsv);
331 final int acolor = Color.HSVToColor(0, hsv);
333 mAlphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, color, acolor,
336 mAlphaPaint.setShader(mAlphaShader);
338 canvas.drawRect(rect, mAlphaPaint);
340 if (mAlphaSliderText != null && mAlphaSliderText != "") {
341 canvas.drawText(mAlphaSliderText, rect.centerX(), rect.centerY() + 4 * mDensity,
345 final float rectWidth = 4 * mDensity / 2;
347 final Point p = alphaToPoint(mAlpha);
349 final RectF r = new RectF();
350 r.left = p.x - rectWidth;
351 r.right = p.x + rectWidth;
352 r.top = rect.top - RECTANGLE_TRACKER_OFFSET;
353 r.bottom = rect.bottom + RECTANGLE_TRACKER_OFFSET;
355 canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
359 private Point hueToPoint(final float hue) {
361 final RectF rect = mHueRect;
362 final float height = rect.height();
364 final Point p = new Point();
366 p.y = (int)(height - hue * height / 360f + rect.top);
367 p.x = (int)rect.left;
372 private Point satValToPoint(final float sat, final float val) {
374 final RectF rect = mSatValRect;
375 final float height = rect.height();
376 final float width = rect.width();
378 final Point p = new Point();
380 p.x = (int)(sat * width + rect.left);
381 p.y = (int)((1f - val) * height + rect.top);
386 private Point alphaToPoint(final int alpha) {
388 final RectF rect = mAlphaRect;
389 final float width = rect.width();
391 final Point p = new Point();
393 p.x = (int)(width - alpha * width / 0xff + rect.left);
400 private float[] pointToSatVal(float x, float y) {
402 final RectF rect = mSatValRect;
403 final float[] result = new float[2];
405 final float width = rect.width();
406 final float height = rect.height();
410 } else if (x > rect.right) {
418 } else if (y > rect.bottom) {
424 result[0] = 1.f / width * x;
425 result[1] = 1.f - 1.f / height * y;
430 private float pointToHue(float y) {
432 final RectF rect = mHueRect;
434 final float height = rect.height();
438 } else if (y > rect.bottom) {
444 return 360f - y * 360f / height;
447 private int pointToAlpha(int x) {
449 final RectF rect = mAlphaRect;
450 final int width = (int)rect.width();
454 } else if (x > rect.right) {
457 x = x - (int)rect.left;
460 return 0xff - x * 0xff / width;
465 public boolean onTrackballEvent(final MotionEvent event) {
467 final float x = event.getX();
468 final float y = event.getY();
470 boolean update = false;
472 if (event.getAction() == MotionEvent.ACTION_MOVE) {
474 switch (mLastTouchedPanel) {
481 sat = mSat + x / 50f;
482 val = mVal - y / 50f;
486 } else if (sat > 1f) {
492 } else if (val > 1f) {
505 float hue = mHue - y * 10f;
509 } else if (hue > 360f) {
521 if (!mShowAlphaPanel || mAlphaRect == null) {
525 int alpha = (int)(mAlpha - x * 10);
529 } else if (alpha > 0xff) {
545 if (mListener != null) {
546 mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
555 return super.onTrackballEvent(event);
559 public boolean onTouchEvent(final MotionEvent event) {
561 boolean update = false;
563 switch (event.getAction()) {
565 case MotionEvent.ACTION_DOWN:
567 mStartTouchPoint = new Point((int)event.getX(), (int)event.getY());
569 update = moveTrackersIfNeeded(event);
573 case MotionEvent.ACTION_MOVE:
575 update = moveTrackersIfNeeded(event);
579 case MotionEvent.ACTION_UP:
581 mStartTouchPoint = null;
583 update = moveTrackersIfNeeded(event);
591 if (mListener != null) {
592 mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
601 return super.onTouchEvent(event);
604 private boolean moveTrackersIfNeeded(final MotionEvent event) {
606 if (mStartTouchPoint == null) {
610 boolean update = false;
612 final int startX = mStartTouchPoint.x;
613 final int startY = mStartTouchPoint.y;
615 if (mHueRect.contains(startX, startY)) {
616 mLastTouchedPanel = PANEL_HUE;
618 mHue = pointToHue(event.getY());
621 } else if (mSatValRect.contains(startX, startY)) {
623 mLastTouchedPanel = PANEL_SAT_VAL;
625 final float[] result = pointToSatVal(event.getX(), event.getY());
631 } else if (mAlphaRect != null && mAlphaRect.contains(startX, startY)) {
633 mLastTouchedPanel = PANEL_ALPHA;
635 mAlpha = pointToAlpha((int)event.getX());
644 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
649 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
650 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
652 int widthAllowed = MeasureSpec.getSize(widthMeasureSpec);
653 int heightAllowed = MeasureSpec.getSize(heightMeasureSpec);
655 widthAllowed = chooseWidth(widthMode, widthAllowed);
656 heightAllowed = chooseHeight(heightMode, heightAllowed);
658 if (!mShowAlphaPanel) {
659 height = (int)(widthAllowed - PANEL_SPACING - HUE_PANEL_WIDTH);
661 // If calculated height (based on the width) is more than the
663 if (height > heightAllowed) {
664 height = heightAllowed;
665 width = (int)(height + PANEL_SPACING + HUE_PANEL_WIDTH);
667 width = widthAllowed;
671 width = (int)(heightAllowed - ALPHA_PANEL_HEIGHT + HUE_PANEL_WIDTH);
673 if (width > widthAllowed) {
674 width = widthAllowed;
675 height = (int)(widthAllowed - HUE_PANEL_WIDTH + ALPHA_PANEL_HEIGHT);
677 height = heightAllowed;
682 setMeasuredDimension(width, height);
685 private int chooseWidth(final int mode, final int size) {
686 if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
688 } else { // (mode == MeasureSpec.UNSPECIFIED)
689 return getPrefferedWidth();
693 private int chooseHeight(final int mode, final int size) {
694 if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
696 } else { // (mode == MeasureSpec.UNSPECIFIED)
697 return getPrefferedHeight();
701 private int getPrefferedWidth() {
703 int width = getPrefferedHeight();
705 if (mShowAlphaPanel) {
706 width -= PANEL_SPACING + ALPHA_PANEL_HEIGHT;
709 return (int)(width + HUE_PANEL_WIDTH + PANEL_SPACING);
713 private int getPrefferedHeight() {
715 int height = (int)(200 * mDensity);
717 if (mShowAlphaPanel) {
718 height += PANEL_SPACING + ALPHA_PANEL_HEIGHT;
725 protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
726 super.onSizeChanged(w, h, oldw, oldh);
728 mDrawingRect = new RectF();
729 mDrawingRect.left = mDrawingOffset + getPaddingLeft();
730 mDrawingRect.right = w - mDrawingOffset - getPaddingRight();
731 mDrawingRect.top = mDrawingOffset + getPaddingTop();
732 mDrawingRect.bottom = h - mDrawingOffset - getPaddingBottom();
739 private void setUpSatValRect() {
741 final RectF dRect = mDrawingRect;
742 float panelSide = dRect.height() - BORDER_WIDTH_PX * 2;
744 if (mShowAlphaPanel) {
745 panelSide -= PANEL_SPACING + ALPHA_PANEL_HEIGHT;
748 final float left = dRect.left + BORDER_WIDTH_PX;
749 final float top = dRect.top + BORDER_WIDTH_PX;
750 final float bottom = top + panelSide;
751 final float right = left + panelSide;
753 mSatValRect = new RectF(left, top, right, bottom);
756 private void setUpHueRect() {
757 final RectF dRect = mDrawingRect;
759 final float left = dRect.right - HUE_PANEL_WIDTH + BORDER_WIDTH_PX;
760 final float top = dRect.top + BORDER_WIDTH_PX;
761 final float bottom = dRect.bottom - BORDER_WIDTH_PX
762 - (mShowAlphaPanel ? PANEL_SPACING + ALPHA_PANEL_HEIGHT : 0);
763 final float right = dRect.right - BORDER_WIDTH_PX;
765 mHueRect = new RectF(left, top, right, bottom);
768 private void setUpAlphaRect() {
770 if (!mShowAlphaPanel) {
774 final RectF dRect = mDrawingRect;
776 final float left = dRect.left + BORDER_WIDTH_PX;
777 final float top = dRect.bottom - ALPHA_PANEL_HEIGHT + BORDER_WIDTH_PX;
778 final float bottom = dRect.bottom - BORDER_WIDTH_PX;
779 final float right = dRect.right - BORDER_WIDTH_PX;
781 mAlphaRect = new RectF(left, top, right, bottom);
783 mAlphaPattern = new AlphaPatternDrawable((int)(5 * mDensity));
784 mAlphaPattern.setBounds(Math.round(mAlphaRect.left), Math.round(mAlphaRect.top),
785 Math.round(mAlphaRect.right), Math.round(mAlphaRect.bottom));
790 * Set a OnColorChangedListener to get notified when the color selected by
791 * the user has changed.
795 public void setOnColorChangedListener(final OnColorChangedListener listener) {
796 mListener = listener;
800 * Set the color of the border surrounding all panels.
804 public void setBorderColor(final int color) {
805 mBorderColor = color;
810 * Get the color of the border surrounding all panels.
812 public int getBorderColor() {
817 * Get the current color this view is showing.
819 * @return the current color.
821 public int getColor() {
822 return Color.HSVToColor(mAlpha, new float[] {
828 * Set the color the view should show.
830 * @param color The color that should be selected.
832 public void setColor(final int color) {
833 setColor(color, false);
837 * Set the color this view should show.
839 * @param color The color that should be selected.
840 * @param callback If you want to get a callback to your
841 * OnColorChangedListener.
843 public void setColor(final int color, final boolean callback) {
845 final int alpha = Color.alpha(color);
846 final int red = Color.red(color);
847 final int blue = Color.blue(color);
848 final int green = Color.green(color);
850 final float[] hsv = new float[3];
852 Color.RGBToHSV(red, green, blue, hsv);
859 if (callback && mListener != null) {
860 mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
869 * Get the drawing offset of the color picker view. The drawing offset is
870 * the distance from the side of a panel to the side of the view minus the
871 * padding. Useful if you want to have your own panel below showing the
872 * currently selected color and want to align it perfectly.
874 * @return The offset in pixels.
876 public float getDrawingOffset() {
877 return mDrawingOffset;
881 * Set if the user is allowed to adjust the alpha panel. Default is false.
882 * If it is set to false no alpha will be set.
886 public void setAlphaSliderVisible(final boolean visible) {
888 if (mShowAlphaPanel != visible) {
889 mShowAlphaPanel = visible;
892 * Reset all shader to force a recreation. Otherwise they will not
893 * look right after the size of the view has changed.
905 public void setSliderTrackerColor(final int color) {
906 mSliderTrackerColor = color;
908 mHueTrackerPaint.setColor(mSliderTrackerColor);
913 public int getSliderTrackerColor() {
914 return mSliderTrackerColor;
918 * Set the text that should be shown in the alpha slider. Set to null to
921 * @param res string resource id.
923 public void setAlphaSliderText(final int res) {
924 final String text = getContext().getString(res);
925 setAlphaSliderText(text);
929 * Set the text that should be shown in the alpha slider. Set to null to
932 * @param text Text that should be shown.
934 public void setAlphaSliderText(final String text) {
935 mAlphaSliderText = text;
940 * Get the current value of the text that will be shown in the alpha slider.
944 public String getAlphaSliderText() {
945 return mAlphaSliderText;