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.cyanogenmod.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.text.TextUtils;
28 import android.util.AttributeSet;
29 import android.view.MotionEvent;
30 import android.view.View;
33 * Displays a color picker to the user and allow them to select a color. A
34 * slider for the alpha channel is also available. Enable it by setting
35 * setAlphaSliderVisible(boolean) to true.
37 * @author Daniel Nilsson
39 public class ColorPickerView extends View {
41 public interface OnColorChangedListener {
42 public void onColorChanged(int color);
45 private final static int PANEL_SAT_VAL = 0;
47 private final static int PANEL_HUE = 1;
49 private final static int PANEL_ALPHA = 2;
52 * The width in pixels of the border surrounding all color panels.
54 private final static float BORDER_WIDTH_PX = 1;
57 * The width in dp of the hue panel.
59 private float HUE_PANEL_WIDTH = 30f;
62 * The height in dp of the alpha panel
64 private float ALPHA_PANEL_HEIGHT = 20f;
67 * The distance in dp between the different color panels.
69 private float PANEL_SPACING = 10f;
72 * The radius in dp of the color palette tracker circle.
74 private float PALETTE_CIRCLE_TRACKER_RADIUS = 5f;
77 * The dp which the tracker of the hue or alpha panel will extend outside of
80 private float RECTANGLE_TRACKER_OFFSET = 2f;
82 private static float mDensity = 1f;
84 private OnColorChangedListener mListener;
86 private Paint mSatValPaint;
88 private Paint mSatValTrackerPaint;
90 private Paint mHuePaint;
92 private Paint mHueTrackerPaint;
94 private Paint mAlphaPaint;
96 private Paint mAlphaTextPaint;
98 private Paint mBorderPaint;
100 private Shader mValShader;
102 private Shader mSatShader;
104 private Shader mHueShader;
106 private Shader mAlphaShader;
108 private int mAlpha = 0xff;
110 private float mHue = 360f;
112 private float mSat = 0f;
114 private float mVal = 0f;
116 private String mAlphaSliderText = "Alpha";
118 private int mSliderTrackerColor = 0xff1c1c1c;
120 private int mBorderColor = 0xff6E6E6E;
122 private boolean mShowAlphaPanel = false;
125 * To remember which panel that has the "focus" when processing hardware
128 private int mLastTouchedPanel = PANEL_SAT_VAL;
131 * Offset from the edge we must have or else the finger tracker will get
132 * clipped when it is drawn outside of the view.
134 private float mDrawingOffset;
137 * Distance form the edges of the view of where we are allowed to draw.
139 private RectF mDrawingRect;
141 private RectF mSatValRect;
143 private RectF mHueRect;
145 private RectF mAlphaRect;
147 private AlphaPatternDrawable mAlphaPattern;
149 private Point mStartTouchPoint = null;
151 public ColorPickerView(final Context context) {
155 public ColorPickerView(final Context context, final AttributeSet attrs) {
156 this(context, attrs, 0);
159 public ColorPickerView(final Context context, final AttributeSet attrs, final int defStyle) {
160 super(context, attrs, defStyle);
164 private void init() {
165 mDensity = getContext().getResources().getDisplayMetrics().density;
166 PALETTE_CIRCLE_TRACKER_RADIUS *= mDensity;
167 RECTANGLE_TRACKER_OFFSET *= mDensity;
168 HUE_PANEL_WIDTH *= mDensity;
169 ALPHA_PANEL_HEIGHT *= mDensity;
170 PANEL_SPACING = PANEL_SPACING * mDensity;
172 mDrawingOffset = calculateRequiredOffset();
176 // Needed for receiving track ball motion events.
178 setFocusableInTouchMode(true);
181 private void initPaintTools() {
183 mSatValPaint = new Paint();
184 mSatValTrackerPaint = new Paint();
185 mHuePaint = new Paint();
186 mHueTrackerPaint = new Paint();
187 mAlphaPaint = new Paint();
188 mAlphaTextPaint = new Paint();
189 mBorderPaint = new Paint();
191 mSatValTrackerPaint.setStyle(Style.STROKE);
192 mSatValTrackerPaint.setStrokeWidth(2f * mDensity);
193 mSatValTrackerPaint.setAntiAlias(true);
195 mHueTrackerPaint.setColor(mSliderTrackerColor);
196 mHueTrackerPaint.setStyle(Style.STROKE);
197 mHueTrackerPaint.setStrokeWidth(2f * mDensity);
198 mHueTrackerPaint.setAntiAlias(true);
200 mAlphaTextPaint.setColor(0xff1c1c1c);
201 mAlphaTextPaint.setTextSize(14f * mDensity);
202 mAlphaTextPaint.setAntiAlias(true);
203 mAlphaTextPaint.setTextAlign(Align.CENTER);
204 mAlphaTextPaint.setFakeBoldText(true);
208 private float calculateRequiredOffset() {
209 float offset = Math.max(PALETTE_CIRCLE_TRACKER_RADIUS, RECTANGLE_TRACKER_OFFSET);
210 offset = Math.max(offset, BORDER_WIDTH_PX * mDensity);
212 return offset * 1.5f;
215 private int[] buildHueColorArray() {
217 final int[] hue = new int[361];
220 for (int i = hue.length - 1; i >= 0; i--, count++) {
221 hue[count] = Color.HSVToColor(new float[] {
230 protected void onDraw(final Canvas canvas) {
232 if (mDrawingRect.width() <= 0 || mDrawingRect.height() <= 0) {
236 drawSatValPanel(canvas);
237 drawHuePanel(canvas);
238 drawAlphaPanel(canvas);
242 private void drawSatValPanel(final Canvas canvas) {
244 final RectF rect = mSatValRect;
246 if (BORDER_WIDTH_PX > 0) {
247 mBorderPaint.setColor(mBorderColor);
248 canvas.drawRect(mDrawingRect.left, mDrawingRect.top, rect.right + BORDER_WIDTH_PX,
249 rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
252 if (mValShader == null) {
253 mValShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom,
254 0xffffffff, 0xff000000, TileMode.CLAMP);
257 final int rgb = Color.HSVToColor(new float[] {
261 mSatShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb,
263 final ComposeShader mShader = new ComposeShader(mValShader, mSatShader,
264 PorterDuff.Mode.MULTIPLY);
265 mSatValPaint.setShader(mShader);
267 canvas.drawRect(rect, mSatValPaint);
269 final Point p = satValToPoint(mSat, mVal);
271 mSatValTrackerPaint.setColor(0xff000000);
272 canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS - 1f * mDensity,
273 mSatValTrackerPaint);
275 mSatValTrackerPaint.setColor(0xffdddddd);
276 canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS, mSatValTrackerPaint);
280 private void drawHuePanel(final Canvas canvas) {
282 final RectF rect = mHueRect;
284 if (BORDER_WIDTH_PX > 0) {
285 mBorderPaint.setColor(mBorderColor);
286 canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right
287 + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
290 if (mHueShader == null) {
291 mHueShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom,
292 buildHueColorArray(), null, TileMode.CLAMP);
293 mHuePaint.setShader(mHueShader);
296 canvas.drawRect(rect, mHuePaint);
298 final float rectHeight = 4 * mDensity / 2;
300 final Point p = hueToPoint(mHue);
302 final RectF r = new RectF();
303 r.left = rect.left - RECTANGLE_TRACKER_OFFSET;
304 r.right = rect.right + RECTANGLE_TRACKER_OFFSET;
305 r.top = p.y - rectHeight;
306 r.bottom = p.y + rectHeight;
308 canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
312 private void drawAlphaPanel(final Canvas canvas) {
314 if (!mShowAlphaPanel || mAlphaRect == null || mAlphaPattern == null) {
318 final RectF rect = mAlphaRect;
320 if (BORDER_WIDTH_PX > 0) {
321 mBorderPaint.setColor(mBorderColor);
322 canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right
323 + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint);
326 mAlphaPattern.draw(canvas);
328 final float[] hsv = new float[] {
331 final int color = Color.HSVToColor(hsv);
332 final int acolor = Color.HSVToColor(0, hsv);
334 mAlphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, color, acolor,
337 mAlphaPaint.setShader(mAlphaShader);
339 canvas.drawRect(rect, mAlphaPaint);
341 if (!TextUtils.isEmpty(mAlphaSliderText)) {
342 canvas.drawText(mAlphaSliderText, rect.centerX(), rect.centerY() + 4 * mDensity,
346 final float rectWidth = 4 * mDensity / 2;
348 final Point p = alphaToPoint(mAlpha);
350 final RectF r = new RectF();
351 r.left = p.x - rectWidth;
352 r.right = p.x + rectWidth;
353 r.top = rect.top - RECTANGLE_TRACKER_OFFSET;
354 r.bottom = rect.bottom + RECTANGLE_TRACKER_OFFSET;
356 canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint);
360 private Point hueToPoint(final float hue) {
362 final RectF rect = mHueRect;
363 final float height = rect.height();
365 final Point p = new Point();
367 p.y = (int)(height - hue * height / 360f + rect.top);
368 p.x = (int)rect.left;
373 private Point satValToPoint(final float sat, final float val) {
375 final RectF rect = mSatValRect;
376 final float height = rect.height();
377 final float width = rect.width();
379 final Point p = new Point();
381 p.x = (int)(sat * width + rect.left);
382 p.y = (int)((1f - val) * height + rect.top);
387 private Point alphaToPoint(final int alpha) {
389 final RectF rect = mAlphaRect;
390 final float width = rect.width();
392 final Point p = new Point();
394 p.x = (int)(width - alpha * width / 0xff + rect.left);
401 private float[] pointToSatVal(float x, float y) {
403 final RectF rect = mSatValRect;
404 final float[] result = new float[2];
406 final float width = rect.width();
407 final float height = rect.height();
411 } else if (x > rect.right) {
419 } else if (y > rect.bottom) {
425 result[0] = 1.f / width * x;
426 result[1] = 1.f - 1.f / height * y;
431 private float pointToHue(float y) {
433 final RectF rect = mHueRect;
435 final float height = rect.height();
439 } else if (y > rect.bottom) {
445 return 360f - y * 360f / height;
448 private int pointToAlpha(int x) {
450 final RectF rect = mAlphaRect;
451 final int width = (int)rect.width();
455 } else if (x > rect.right) {
458 x = x - (int)rect.left;
461 return 0xff - x * 0xff / width;
466 public boolean onTrackballEvent(final MotionEvent event) {
468 final float x = event.getX();
469 final float y = event.getY();
471 boolean update = false;
473 if (event.getAction() == MotionEvent.ACTION_MOVE) {
475 switch (mLastTouchedPanel) {
482 sat = mSat + x / 50f;
483 val = mVal - y / 50f;
487 } else if (sat > 1f) {
493 } else if (val > 1f) {
506 float hue = mHue - y * 10f;
510 } else if (hue > 360f) {
522 if (!mShowAlphaPanel || mAlphaRect == null) {
526 int alpha = (int)(mAlpha - x * 10);
530 } else if (alpha > 0xff) {
546 if (mListener != null) {
547 mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
556 return super.onTrackballEvent(event);
560 public boolean onTouchEvent(final MotionEvent event) {
562 boolean update = false;
564 switch (event.getAction()) {
566 case MotionEvent.ACTION_DOWN:
568 mStartTouchPoint = new Point((int)event.getX(), (int)event.getY());
570 update = moveTrackersIfNeeded(event);
574 case MotionEvent.ACTION_MOVE:
576 update = moveTrackersIfNeeded(event);
580 case MotionEvent.ACTION_UP:
582 mStartTouchPoint = null;
584 update = moveTrackersIfNeeded(event);
592 if (mListener != null) {
593 mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
602 return super.onTouchEvent(event);
605 private boolean moveTrackersIfNeeded(final MotionEvent event) {
607 if (mStartTouchPoint == null) {
611 boolean update = false;
613 final int startX = mStartTouchPoint.x;
614 final int startY = mStartTouchPoint.y;
616 if (mHueRect.contains(startX, startY)) {
617 mLastTouchedPanel = PANEL_HUE;
619 mHue = pointToHue(event.getY());
622 } else if (mSatValRect.contains(startX, startY)) {
624 mLastTouchedPanel = PANEL_SAT_VAL;
626 final float[] result = pointToSatVal(event.getX(), event.getY());
632 } else if (mAlphaRect != null && mAlphaRect.contains(startX, startY)) {
634 mLastTouchedPanel = PANEL_ALPHA;
636 mAlpha = pointToAlpha((int)event.getX());
645 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
650 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
651 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
653 int widthAllowed = MeasureSpec.getSize(widthMeasureSpec);
654 int heightAllowed = MeasureSpec.getSize(heightMeasureSpec);
656 widthAllowed = chooseWidth(widthMode, widthAllowed);
657 heightAllowed = chooseHeight(heightMode, heightAllowed);
659 if (!mShowAlphaPanel) {
660 height = (int)(widthAllowed - PANEL_SPACING - HUE_PANEL_WIDTH);
662 // If calculated height (based on the width) is more than the
664 if (height > heightAllowed) {
665 height = heightAllowed;
666 width = (int)(height + PANEL_SPACING + HUE_PANEL_WIDTH);
668 width = widthAllowed;
672 width = (int)(heightAllowed - ALPHA_PANEL_HEIGHT + HUE_PANEL_WIDTH);
674 if (width > widthAllowed) {
675 width = widthAllowed;
676 height = (int)(widthAllowed - HUE_PANEL_WIDTH + ALPHA_PANEL_HEIGHT);
678 height = heightAllowed;
683 setMeasuredDimension(width, height);
686 private int chooseWidth(final int mode, final int size) {
687 if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
689 } else { // (mode == MeasureSpec.UNSPECIFIED)
690 return getPrefferedWidth();
694 private int chooseHeight(final int mode, final int size) {
695 if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
697 } else { // (mode == MeasureSpec.UNSPECIFIED)
698 return getPrefferedHeight();
702 private int getPrefferedWidth() {
704 int width = getPrefferedHeight();
706 if (mShowAlphaPanel) {
707 width -= PANEL_SPACING + ALPHA_PANEL_HEIGHT;
710 return (int)(width + HUE_PANEL_WIDTH + PANEL_SPACING);
714 private int getPrefferedHeight() {
716 int height = (int)(200 * mDensity);
718 if (mShowAlphaPanel) {
719 height += PANEL_SPACING + ALPHA_PANEL_HEIGHT;
726 protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
727 super.onSizeChanged(w, h, oldw, oldh);
729 mDrawingRect = new RectF();
730 mDrawingRect.left = mDrawingOffset + getPaddingLeft();
731 mDrawingRect.right = w - mDrawingOffset - getPaddingRight();
732 mDrawingRect.top = mDrawingOffset + getPaddingTop();
733 mDrawingRect.bottom = h - mDrawingOffset - getPaddingBottom();
740 private void setUpSatValRect() {
742 final RectF dRect = mDrawingRect;
743 float panelSide = dRect.height() - BORDER_WIDTH_PX * 2;
745 if (mShowAlphaPanel) {
746 panelSide -= PANEL_SPACING + ALPHA_PANEL_HEIGHT;
749 final float left = dRect.left + BORDER_WIDTH_PX;
750 final float top = dRect.top + BORDER_WIDTH_PX;
751 final float bottom = top + panelSide;
752 final float right = left + panelSide;
754 mSatValRect = new RectF(left, top, right, bottom);
757 private void setUpHueRect() {
758 final RectF dRect = mDrawingRect;
760 final float left = dRect.right - HUE_PANEL_WIDTH + BORDER_WIDTH_PX;
761 final float top = dRect.top + BORDER_WIDTH_PX;
762 final float bottom = dRect.bottom - BORDER_WIDTH_PX
763 - (mShowAlphaPanel ? PANEL_SPACING + ALPHA_PANEL_HEIGHT : 0);
764 final float right = dRect.right - BORDER_WIDTH_PX;
766 mHueRect = new RectF(left, top, right, bottom);
769 private void setUpAlphaRect() {
771 if (!mShowAlphaPanel) {
775 final RectF dRect = mDrawingRect;
777 final float left = dRect.left + BORDER_WIDTH_PX;
778 final float top = dRect.bottom - ALPHA_PANEL_HEIGHT + BORDER_WIDTH_PX;
779 final float bottom = dRect.bottom - BORDER_WIDTH_PX;
780 final float right = dRect.right - BORDER_WIDTH_PX;
782 mAlphaRect = new RectF(left, top, right, bottom);
784 mAlphaPattern = new AlphaPatternDrawable((int)(5 * mDensity));
785 mAlphaPattern.setBounds(Math.round(mAlphaRect.left), Math.round(mAlphaRect.top),
786 Math.round(mAlphaRect.right), Math.round(mAlphaRect.bottom));
791 * Set a OnColorChangedListener to get notified when the color selected by
792 * the user has changed.
796 public void setOnColorChangedListener(final OnColorChangedListener listener) {
797 mListener = listener;
801 * Set the color of the border surrounding all panels.
805 public void setBorderColor(final int color) {
806 mBorderColor = color;
811 * Get the color of the border surrounding all panels.
813 public int getBorderColor() {
818 * Get the current color this view is showing.
820 * @return the current color.
822 public int getColor() {
823 return Color.HSVToColor(mAlpha, new float[] {
829 * Set the color the view should show.
831 * @param color The color that should be selected.
833 public void setColor(final int color) {
834 setColor(color, false);
838 * Set the color this view should show.
840 * @param color The color that should be selected.
841 * @param callback If you want to get a callback to your
842 * OnColorChangedListener.
844 public void setColor(final int color, final boolean callback) {
846 final int alpha = Color.alpha(color);
847 final int red = Color.red(color);
848 final int blue = Color.blue(color);
849 final int green = Color.green(color);
851 final float[] hsv = new float[3];
853 Color.RGBToHSV(red, green, blue, hsv);
860 if (callback && mListener != null) {
861 mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[] {
870 * Get the drawing offset of the color picker view. The drawing offset is
871 * the distance from the side of a panel to the side of the view minus the
872 * padding. Useful if you want to have your own panel below showing the
873 * currently selected color and want to align it perfectly.
875 * @return The offset in pixels.
877 public float getDrawingOffset() {
878 return mDrawingOffset;
882 * Set if the user is allowed to adjust the alpha panel. Default is false.
883 * If it is set to false no alpha will be set.
887 public void setAlphaSliderVisible(final boolean visible) {
889 if (mShowAlphaPanel != visible) {
890 mShowAlphaPanel = visible;
893 * Reset all shader to force a recreation. Otherwise they will not
894 * look right after the size of the view has changed.
906 public void setSliderTrackerColor(final int color) {
907 mSliderTrackerColor = color;
909 mHueTrackerPaint.setColor(mSliderTrackerColor);
914 public int getSliderTrackerColor() {
915 return mSliderTrackerColor;
919 * Set the text that should be shown in the alpha slider. Set to null to
922 * @param res string resource id.
924 public void setAlphaSliderText(final int res) {
925 final String text = getContext().getString(res);
926 setAlphaSliderText(text);
930 * Set the text that should be shown in the alpha slider. Set to null to
933 * @param text Text that should be shown.
935 public void setAlphaSliderText(final String text) {
936 mAlphaSliderText = text;
941 * Get the current value of the text that will be shown in the alpha slider.
945 public String getAlphaSliderText() {
946 return mAlphaSliderText;