/* * Copyright (C) 2010 Daniel Nilsson * * 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 com.fanfou.app.opensource.preferences.colorpicker; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ComposeShader; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Shader.TileMode; import android.text.TextUtils; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; /** * Displays a color picker to the user and allow them to select a color. A * slider for the alpha channel is also available. Enable it by setting * setAlphaSliderVisible(boolean) to true. * * @author Daniel Nilsson */ public class ColorPickerView extends View { public interface OnColorChangedListener { public void onColorChanged(int color); } private final static int PANEL_SAT_VAL = 0; private final static int PANEL_HUE = 1; private final static int PANEL_ALPHA = 2; /** * The width in pixels of the border surrounding all color panels. */ private final static float BORDER_WIDTH_PX = 1; /** * The width in dp of the hue panel. */ private float HUE_PANEL_WIDTH = 30f; /** * The height in dp of the alpha panel */ private float ALPHA_PANEL_HEIGHT = 20f; /** * The distance in dp between the different color panels. */ private float PANEL_SPACING = 10f; /** * The radius in dp of the color palette tracker circle. */ private float PALETTE_CIRCLE_TRACKER_RADIUS = 5f; /** * The dp which the tracker of the hue or alpha panel will extend outside of * its bounds. */ private float RECTANGLE_TRACKER_OFFSET = 2f; private float mDensity = 1f; private OnColorChangedListener mListener; private Paint mSatValPaint; private Paint mSatValTrackerPaint; private Paint mHuePaint; private Paint mHueTrackerPaint; private Paint mAlphaPaint; private Paint mAlphaTextPaint; private Paint mBorderPaint; private Shader mValShader; private Shader mSatShader; private Shader mHueShader; private Shader mAlphaShader; private int mAlpha = 0xff; private float mHue = 360f; private float mSat = 0f; private float mVal = 0f; private String mAlphaSliderText = ""; private int mSliderTrackerColor = 0xff1c1c1c; private int mBorderColor = 0xff6E6E6E; private boolean mShowAlphaPanel = false; /* * To remember which panel that has the "focus" when processing hardware * button data. */ private int mLastTouchedPanel = ColorPickerView.PANEL_SAT_VAL; /** * Offset from the edge we must have or else the finger tracker will get * clipped when it is drawn outside of the view. */ private float mDrawingOffset; /* * Distance form the edges of the view of where we are allowed to draw. */ private RectF mDrawingRect; private RectF mSatValRect; private RectF mHueRect; private RectF mAlphaRect; private AlphaPatternDrawable mAlphaPattern; private Point mStartTouchPoint = null; public ColorPickerView(final Context context) { this(context, null); } public ColorPickerView(final Context context, final AttributeSet attrs) { this(context, attrs, 0); } public ColorPickerView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); init(); } private Point alphaToPoint(final int alpha) { final RectF rect = this.mAlphaRect; final float width = rect.width(); final Point p = new Point(); p.x = (int) ((width - ((alpha * width) / 0xff)) + rect.left); p.y = (int) rect.top; return p; } private int[] buildHueColorArray() { final int[] hue = new int[361]; int count = 0; for (int i = hue.length - 1; i >= 0; i--, count++) { hue[count] = Color.HSVToColor(new float[] { i, 1f, 1f }); } return hue; } private float calculateRequiredOffset() { float offset = Math.max(this.PALETTE_CIRCLE_TRACKER_RADIUS, this.RECTANGLE_TRACKER_OFFSET); offset = Math.max(offset, ColorPickerView.BORDER_WIDTH_PX * this.mDensity); return offset * 1.5f; } private int chooseHeight(final int mode, final int size) { if ((mode == MeasureSpec.AT_MOST) || (mode == MeasureSpec.EXACTLY)) { return size; } else { // (mode == MeasureSpec.UNSPECIFIED) return getPrefferedHeight(); } } private int chooseWidth(final int mode, final int size) { if ((mode == MeasureSpec.AT_MOST) || (mode == MeasureSpec.EXACTLY)) { return size; } else { // (mode == MeasureSpec.UNSPECIFIED) return getPrefferedWidth(); } } private void drawAlphaPanel(final Canvas canvas) { if (!this.mShowAlphaPanel || (this.mAlphaRect == null) || (this.mAlphaPattern == null)) { return; } final RectF rect = this.mAlphaRect; if (ColorPickerView.BORDER_WIDTH_PX > 0) { this.mBorderPaint.setColor(this.mBorderColor); canvas.drawRect(rect.left - ColorPickerView.BORDER_WIDTH_PX, rect.top - ColorPickerView.BORDER_WIDTH_PX, rect.right + ColorPickerView.BORDER_WIDTH_PX, rect.bottom + ColorPickerView.BORDER_WIDTH_PX, this.mBorderPaint); } this.mAlphaPattern.draw(canvas); final float[] hsv = new float[] { this.mHue, this.mSat, this.mVal }; final int color = Color.HSVToColor(hsv); final int acolor = Color.HSVToColor(0, hsv); this.mAlphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, color, acolor, TileMode.CLAMP); this.mAlphaPaint.setShader(this.mAlphaShader); canvas.drawRect(rect, this.mAlphaPaint); if (!TextUtils.isEmpty(this.mAlphaSliderText)) { canvas.drawText(this.mAlphaSliderText, rect.centerX(), rect.centerY() + (4 * this.mDensity), this.mAlphaTextPaint); } final float rectWidth = (4 * this.mDensity) / 2; final Point p = alphaToPoint(this.mAlpha); final RectF r = new RectF(); r.left = p.x - rectWidth; r.right = p.x + rectWidth; r.top = rect.top - this.RECTANGLE_TRACKER_OFFSET; r.bottom = rect.bottom + this.RECTANGLE_TRACKER_OFFSET; canvas.drawRoundRect(r, 2, 2, this.mHueTrackerPaint); } private void drawHuePanel(final Canvas canvas) { final RectF rect = this.mHueRect; if (ColorPickerView.BORDER_WIDTH_PX > 0) { this.mBorderPaint.setColor(this.mBorderColor); canvas.drawRect(rect.left - ColorPickerView.BORDER_WIDTH_PX, rect.top - ColorPickerView.BORDER_WIDTH_PX, rect.right + ColorPickerView.BORDER_WIDTH_PX, rect.bottom + ColorPickerView.BORDER_WIDTH_PX, this.mBorderPaint); } if (this.mHueShader == null) { this.mHueShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, buildHueColorArray(), null, TileMode.CLAMP); this.mHuePaint.setShader(this.mHueShader); } canvas.drawRect(rect, this.mHuePaint); final float rectHeight = (4 * this.mDensity) / 2; final Point p = hueToPoint(this.mHue); final RectF r = new RectF(); r.left = rect.left - this.RECTANGLE_TRACKER_OFFSET; r.right = rect.right + this.RECTANGLE_TRACKER_OFFSET; r.top = p.y - rectHeight; r.bottom = p.y + rectHeight; canvas.drawRoundRect(r, 2, 2, this.mHueTrackerPaint); } private void drawSatValPanel(final Canvas canvas) { final RectF rect = this.mSatValRect; if (ColorPickerView.BORDER_WIDTH_PX > 0) { this.mBorderPaint.setColor(this.mBorderColor); canvas.drawRect(this.mDrawingRect.left, this.mDrawingRect.top, rect.right + ColorPickerView.BORDER_WIDTH_PX, rect.bottom + ColorPickerView.BORDER_WIDTH_PX, this.mBorderPaint); } if (this.mValShader == null) { this.mValShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, 0xffffffff, 0xff000000, TileMode.CLAMP); } final int rgb = Color.HSVToColor(new float[] { this.mHue, 1f, 1f }); this.mSatShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb, TileMode.CLAMP); final ComposeShader mShader = new ComposeShader(this.mValShader, this.mSatShader, PorterDuff.Mode.MULTIPLY); this.mSatValPaint.setShader(mShader); canvas.drawRect(rect, this.mSatValPaint); final Point p = satValToPoint(this.mSat, this.mVal); this.mSatValTrackerPaint.setColor(0xff000000); canvas.drawCircle(p.x, p.y, this.PALETTE_CIRCLE_TRACKER_RADIUS - (1f * this.mDensity), this.mSatValTrackerPaint); this.mSatValTrackerPaint.setColor(0xffdddddd); canvas.drawCircle(p.x, p.y, this.PALETTE_CIRCLE_TRACKER_RADIUS, this.mSatValTrackerPaint); } /** * Get the current value of the text that will be shown in the alpha slider. * * @return */ public String getAlphaSliderText() { return this.mAlphaSliderText; } /** * Get the color of the border surrounding all panels. */ public int getBorderColor() { return this.mBorderColor; } /** * Get the current color this view is showing. * * @return the current color. */ public int getColor() { return Color.HSVToColor(this.mAlpha, new float[] { this.mHue, this.mSat, this.mVal }); } /** * Get the drawing offset of the color picker view. The drawing offset is * the distance from the side of a panel to the side of the view minus the * padding. Useful if you want to have your own panel below showing the * currently selected color and want to align it perfectly. * * @return The offset in pixels. */ public float getDrawingOffset() { return this.mDrawingOffset; } private int getPrefferedHeight() { int height = (int) (200 * this.mDensity); if (this.mShowAlphaPanel) { height += this.PANEL_SPACING + this.ALPHA_PANEL_HEIGHT; } return height; } private int getPrefferedWidth() { int width = getPrefferedHeight(); if (this.mShowAlphaPanel) { width -= (this.PANEL_SPACING + this.ALPHA_PANEL_HEIGHT); } return (int) (width + this.HUE_PANEL_WIDTH + this.PANEL_SPACING); } public int getSliderTrackerColor() { return this.mSliderTrackerColor; } private Point hueToPoint(final float hue) { final RectF rect = this.mHueRect; final float height = rect.height(); final Point p = new Point(); p.y = (int) ((height - ((hue * height) / 360f)) + rect.top); p.x = (int) rect.left; return p; } private void init() { this.mDensity = getContext().getResources().getDisplayMetrics().density; this.PALETTE_CIRCLE_TRACKER_RADIUS *= this.mDensity; this.RECTANGLE_TRACKER_OFFSET *= this.mDensity; this.HUE_PANEL_WIDTH *= this.mDensity; this.ALPHA_PANEL_HEIGHT *= this.mDensity; this.PANEL_SPACING = this.PANEL_SPACING * this.mDensity; this.mDrawingOffset = calculateRequiredOffset(); initPaintTools(); // Needed for receiving trackball motion events. setFocusable(true); setFocusableInTouchMode(true); } private void initPaintTools() { this.mSatValPaint = new Paint(); this.mSatValTrackerPaint = new Paint(); this.mHuePaint = new Paint(); this.mHueTrackerPaint = new Paint(); this.mAlphaPaint = new Paint(); this.mAlphaTextPaint = new Paint(); this.mBorderPaint = new Paint(); this.mSatValTrackerPaint.setStyle(Style.STROKE); this.mSatValTrackerPaint.setStrokeWidth(2f * this.mDensity); this.mSatValTrackerPaint.setAntiAlias(true); this.mHueTrackerPaint.setColor(this.mSliderTrackerColor); this.mHueTrackerPaint.setStyle(Style.STROKE); this.mHueTrackerPaint.setStrokeWidth(2f * this.mDensity); this.mHueTrackerPaint.setAntiAlias(true); this.mAlphaTextPaint.setColor(0xff1c1c1c); this.mAlphaTextPaint.setTextSize(14f * this.mDensity); this.mAlphaTextPaint.setAntiAlias(true); this.mAlphaTextPaint.setTextAlign(Align.CENTER); this.mAlphaTextPaint.setFakeBoldText(true); } private boolean moveTrackersIfNeeded(final MotionEvent event) { if (this.mStartTouchPoint == null) { return false; } boolean update = false; final int startX = this.mStartTouchPoint.x; final int startY = this.mStartTouchPoint.y; if (this.mHueRect.contains(startX, startY)) { this.mLastTouchedPanel = ColorPickerView.PANEL_HUE; this.mHue = pointToHue(event.getY()); update = true; } else if (this.mSatValRect.contains(startX, startY)) { this.mLastTouchedPanel = ColorPickerView.PANEL_SAT_VAL; final float[] result = pointToSatVal(event.getX(), event.getY()); this.mSat = result[0]; this.mVal = result[1]; update = true; } else if ((this.mAlphaRect != null) && this.mAlphaRect.contains(startX, startY)) { this.mLastTouchedPanel = ColorPickerView.PANEL_ALPHA; this.mAlpha = pointToAlpha((int) event.getX()); update = true; } return update; } @Override protected void onDraw(final Canvas canvas) { if ((this.mDrawingRect.width() <= 0) || (this.mDrawingRect.height() <= 0)) { return; } drawSatValPanel(canvas); drawHuePanel(canvas); drawAlphaPanel(canvas); } @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { int width = 0; int height = 0; final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthAllowed = MeasureSpec.getSize(widthMeasureSpec); int heightAllowed = MeasureSpec.getSize(heightMeasureSpec); widthAllowed = chooseWidth(widthMode, widthAllowed); heightAllowed = chooseHeight(heightMode, heightAllowed); if (!this.mShowAlphaPanel) { height = (int) (widthAllowed - this.PANEL_SPACING - this.HUE_PANEL_WIDTH); // If calculated height (based on the width) is more than the // allowed height. if ((height > heightAllowed) || getTag().equals("landscape")) { height = heightAllowed; width = (int) (height + this.PANEL_SPACING + this.HUE_PANEL_WIDTH); } else { width = widthAllowed; } } else { width = (int) ((heightAllowed - this.ALPHA_PANEL_HEIGHT) + this.HUE_PANEL_WIDTH); if (width > widthAllowed) { width = widthAllowed; height = (int) ((widthAllowed - this.HUE_PANEL_WIDTH) + this.ALPHA_PANEL_HEIGHT); } else { height = heightAllowed; } } setMeasuredDimension(width, height); } @Override protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { super.onSizeChanged(w, h, oldw, oldh); this.mDrawingRect = new RectF(); this.mDrawingRect.left = this.mDrawingOffset + getPaddingLeft(); this.mDrawingRect.right = w - this.mDrawingOffset - getPaddingRight(); this.mDrawingRect.top = this.mDrawingOffset + getPaddingTop(); this.mDrawingRect.bottom = h - this.mDrawingOffset - getPaddingBottom(); setUpSatValRect(); setUpHueRect(); setUpAlphaRect(); } @Override public boolean onTouchEvent(final MotionEvent event) { boolean update = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: this.mStartTouchPoint = new Point((int) event.getX(), (int) event.getY()); update = moveTrackersIfNeeded(event); break; case MotionEvent.ACTION_MOVE: update = moveTrackersIfNeeded(event); break; case MotionEvent.ACTION_UP: this.mStartTouchPoint = null; update = moveTrackersIfNeeded(event); break; } if (update) { if (this.mListener != null) { this.mListener.onColorChanged(Color.HSVToColor(this.mAlpha, new float[] { this.mHue, this.mSat, this.mVal })); } invalidate(); return true; } return super.onTouchEvent(event); } @Override public boolean onTrackballEvent(final MotionEvent event) { final float x = event.getX(); final float y = event.getY(); boolean update = false; if (event.getAction() == MotionEvent.ACTION_MOVE) { switch (this.mLastTouchedPanel) { case PANEL_SAT_VAL: float sat, val; sat = this.mSat + (x / 50f); val = this.mVal - (y / 50f); if (sat < 0f) { sat = 0f; } else if (sat > 1f) { sat = 1f; } if (val < 0f) { val = 0f; } else if (val > 1f) { val = 1f; } this.mSat = sat; this.mVal = val; update = true; break; case PANEL_HUE: float hue = this.mHue - (y * 10f); if (hue < 0f) { hue = 0f; } else if (hue > 360f) { hue = 360f; } this.mHue = hue; update = true; break; case PANEL_ALPHA: if (!this.mShowAlphaPanel || (this.mAlphaRect == null)) { update = false; } else { int alpha = (int) (this.mAlpha - (x * 10)); if (alpha < 0) { alpha = 0; } else if (alpha > 0xff) { alpha = 0xff; } this.mAlpha = alpha; update = true; } break; } } if (update) { if (this.mListener != null) { this.mListener.onColorChanged(Color.HSVToColor(this.mAlpha, new float[] { this.mHue, this.mSat, this.mVal })); } invalidate(); return true; } return super.onTrackballEvent(event); } private int pointToAlpha(int x) { final RectF rect = this.mAlphaRect; final int width = (int) rect.width(); if (x < rect.left) { x = 0; } else if (x > rect.right) { x = width; } else { x = x - (int) rect.left; } return 0xff - ((x * 0xff) / width); } private float pointToHue(float y) { final RectF rect = this.mHueRect; final float height = rect.height(); if (y < rect.top) { y = 0f; } else if (y > rect.bottom) { y = height; } else { y = y - rect.top; } return 360f - ((y * 360f) / height); } private float[] pointToSatVal(float x, float y) { final RectF rect = this.mSatValRect; final float[] result = new float[2]; final float width = rect.width(); final float height = rect.height(); if (x < rect.left) { x = 0f; } else if (x > rect.right) { x = width; } else { x = x - rect.left; } if (y < rect.top) { y = 0f; } else if (y > rect.bottom) { y = height; } else { y = y - rect.top; } result[0] = (1.f / width) * x; result[1] = 1.f - ((1.f / height) * y); return result; } private Point satValToPoint(final float sat, final float val) { final RectF rect = this.mSatValRect; final float height = rect.height(); final float width = rect.width(); final Point p = new Point(); p.x = (int) ((sat * width) + rect.left); p.y = (int) (((1f - val) * height) + rect.top); return p; } /** * Set the text that should be shown in the alpha slider. Set to null to * disable text. * * @param res * string resource id. */ public void setAlphaSliderText(final int res) { final String text = getContext().getString(res); setAlphaSliderText(text); } /** * Set the text that should be shown in the alpha slider. Set to null to * disable text. * * @param text * Text that should be shown. */ public void setAlphaSliderText(final String text) { this.mAlphaSliderText = text; invalidate(); } /** * Set if the user is allowed to adjust the alpha panel. Default is false. * If it is set to false no alpha will be set. * * @param visible */ public void setAlphaSliderVisible(final boolean visible) { if (this.mShowAlphaPanel != visible) { this.mShowAlphaPanel = visible; /* * Reset all shader to force a recreation. Otherwise they will not * look right after the size of the view has changed. */ this.mValShader = null; this.mSatShader = null; this.mHueShader = null; this.mAlphaShader = null; ; requestLayout(); } } /** * Set the color of the border surrounding all panels. * * @param color */ public void setBorderColor(final int color) { this.mBorderColor = color; invalidate(); } /** * Set the color the view should show. * * @param color * The color that should be selected. */ public void setColor(final int color) { setColor(color, false); } /** * Set the color this view should show. * * @param color * The color that should be selected. * @param callback * If you want to get a callback to your OnColorChangedListener. */ public void setColor(final int color, final boolean callback) { final int alpha = Color.alpha(color); final int red = Color.red(color); final int blue = Color.blue(color); final int green = Color.green(color); final float[] hsv = new float[3]; Color.RGBToHSV(red, green, blue, hsv); this.mAlpha = alpha; this.mHue = hsv[0]; this.mSat = hsv[1]; this.mVal = hsv[2]; if (callback && (this.mListener != null)) { this.mListener.onColorChanged(Color.HSVToColor(this.mAlpha, new float[] { this.mHue, this.mSat, this.mVal })); } invalidate(); } /** * Set a OnColorChangedListener to get notified when the color selected by * the user has changed. * * @param listener */ public void setOnColorChangedListener(final OnColorChangedListener listener) { this.mListener = listener; } public void setSliderTrackerColor(final int color) { this.mSliderTrackerColor = color; this.mHueTrackerPaint.setColor(this.mSliderTrackerColor); invalidate(); } private void setUpAlphaRect() { if (!this.mShowAlphaPanel) { return; } final RectF dRect = this.mDrawingRect; final float left = dRect.left + ColorPickerView.BORDER_WIDTH_PX; final float top = (dRect.bottom - this.ALPHA_PANEL_HEIGHT) + ColorPickerView.BORDER_WIDTH_PX; final float bottom = dRect.bottom - ColorPickerView.BORDER_WIDTH_PX; final float right = dRect.right - ColorPickerView.BORDER_WIDTH_PX; this.mAlphaRect = new RectF(left, top, right, bottom); this.mAlphaPattern = new AlphaPatternDrawable((int) (5 * this.mDensity)); this.mAlphaPattern.setBounds(Math.round(this.mAlphaRect.left), Math.round(this.mAlphaRect.top), Math.round(this.mAlphaRect.right), Math.round(this.mAlphaRect.bottom)); } private void setUpHueRect() { final RectF dRect = this.mDrawingRect; final float left = (dRect.right - this.HUE_PANEL_WIDTH) + ColorPickerView.BORDER_WIDTH_PX; final float top = dRect.top + ColorPickerView.BORDER_WIDTH_PX; final float bottom = dRect.bottom - ColorPickerView.BORDER_WIDTH_PX - (this.mShowAlphaPanel ? (this.PANEL_SPACING + this.ALPHA_PANEL_HEIGHT) : 0); final float right = dRect.right - ColorPickerView.BORDER_WIDTH_PX; this.mHueRect = new RectF(left, top, right, bottom); } private void setUpSatValRect() { final RectF dRect = this.mDrawingRect; float panelSide = dRect.height() - (ColorPickerView.BORDER_WIDTH_PX * 2); if (this.mShowAlphaPanel) { panelSide -= this.PANEL_SPACING + this.ALPHA_PANEL_HEIGHT; } final float left = dRect.left + ColorPickerView.BORDER_WIDTH_PX; final float top = dRect.top + ColorPickerView.BORDER_WIDTH_PX; final float bottom = top + panelSide; final float right = left + panelSide; this.mSatValRect = new RectF(left, top, right, bottom); } }