1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.photoeditor.actions; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.RectF; 27 import android.graphics.drawable.Drawable; 28 import android.util.AttributeSet; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.animation.AnimationUtils; 32 33 import com.android.photoeditor.R; 34 35 /** 36 * Wheel that has a draggable thumb to set and get the predefined color set. 37 */ 38 class ColorWheel extends View { 39 40 /** 41 * Listens to color changes. 42 */ 43 public interface OnColorChangeListener { 44 onColorChanged(int color, boolean fromUser)45 void onColorChanged(int color, boolean fromUser); 46 } 47 48 private static final float MATH_PI = (float) Math.PI; 49 private static final float MATH_HALF_PI = MATH_PI / 2; 50 51 // All angles used in this object are defined between PI and -PI. 52 private static final float ANGLE_SPANNED = MATH_PI * 4 / 3; 53 private static final float ANGLE_BEGIN = ANGLE_SPANNED / 2.0f; 54 private static final float DEGREES_BEGIN = 360 - (float) Math.toDegrees(ANGLE_BEGIN); 55 private static final float STROKE_WIDTH = 3.0f; 56 57 private static final float THUMB_RADIUS_RATIO = 0.363f; 58 private static final float INNER_RADIUS_RATIO = 0.173f; 59 60 private static final int PADDING = 4; 61 private static final int COLOR_METER_THICKNESS = 18; 62 63 private final Drawable thumb; 64 private final Paint fillPaint; 65 private final Paint strokePaint; 66 private final int thumbSize; 67 private final int borderColor; 68 private final int[] colorsDefined; 69 private final float radiantInterval; 70 private Bitmap background; 71 private int thumbRadius; 72 private int innerRadius; 73 private int centerXY; 74 private int colorIndex; 75 private float angle; 76 private boolean dragThumb; 77 private OnColorChangeListener listener; 78 ColorWheel(Context context, AttributeSet attrs)79 public ColorWheel(Context context, AttributeSet attrs) { 80 super(context, attrs); 81 82 Resources resources = context.getResources(); 83 thumbSize = (int) resources.getDimension(R.dimen.wheel_thumb_size); 84 85 // Set the number of total colors and compute the radiant interval between colors. 86 TypedArray colors = resources.obtainTypedArray(R.array.color_picker_wheel_colors); 87 colorsDefined = new int[colors.length()]; 88 for (int c = 0; c < colors.length(); c++) { 89 colorsDefined[c] = colors.getColor(c, 0x000000); 90 } 91 colors.recycle(); 92 93 radiantInterval = ANGLE_SPANNED / colorsDefined.length; 94 95 thumb = resources.getDrawable(R.drawable.wheel_knot_selector); 96 borderColor = resources.getColor(R.color.color_picker_border_color); 97 98 fillPaint = new Paint(); 99 fillPaint.setAntiAlias(true); 100 fillPaint.setStyle(Paint.Style.FILL); 101 strokePaint = new Paint(); 102 strokePaint.setAntiAlias(true); 103 strokePaint.setStrokeWidth(STROKE_WIDTH); 104 strokePaint.setStyle(Paint.Style.STROKE); 105 } 106 setColorIndex(int colorIndex)107 public void setColorIndex(int colorIndex) { 108 if (updateColorIndex(colorIndex, false)) { 109 updateThumbPositionByColorIndex(); 110 } 111 } 112 getColor()113 public int getColor() { 114 return colorsDefined[colorIndex]; 115 } 116 setOnColorChangeListener(OnColorChangeListener listener)117 public void setOnColorChangeListener(OnColorChangeListener listener) { 118 this.listener = listener; 119 } 120 121 @Override setVisibility(int visibility)122 public void setVisibility(int visibility) { 123 super.setVisibility(visibility); 124 125 startAnimation(AnimationUtils.loadAnimation(getContext(), 126 (visibility == VISIBLE) ? R.anim.wheel_show : R.anim.wheel_hide)); 127 } 128 129 @Override onSizeChanged(int w, int h, int oldw, int oldh)130 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 131 super.onSizeChanged(w, h, oldw, oldh); 132 133 int wheelSize = Math.min(w, h) - PADDING; 134 thumbRadius = (int) (wheelSize * THUMB_RADIUS_RATIO); 135 innerRadius = (int) (wheelSize * INNER_RADIUS_RATIO); 136 137 // The wheel would be centered at (centerXY, centerXY) and have outer-radius centerXY. 138 centerXY = wheelSize / 2; 139 updateThumbPositionByColorIndex(); 140 } 141 prepareBackground()142 private Bitmap prepareBackground() { 143 int diameter = centerXY * 2; 144 Bitmap bitmap = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888); 145 Canvas canvas = new Canvas(bitmap); 146 147 // the colors to be selected. 148 float radiantDegrees = (float) Math.toDegrees(radiantInterval); 149 RectF drawBound = new RectF(0, 0, diameter, diameter); 150 for (int c = 0; c < colorsDefined.length; c++) { 151 fillPaint.setColor(colorsDefined[c]); 152 canvas.drawArc(drawBound, DEGREES_BEGIN + radiantDegrees * c, 153 radiantDegrees, true, fillPaint); 154 } 155 156 // clear the inner area. 157 fillPaint.setColor(Color.BLACK); 158 fillPaint.setAlpha(160); 159 canvas.drawCircle(centerXY, centerXY, centerXY - COLOR_METER_THICKNESS, fillPaint); 160 161 // the border for the inner ball 162 fillPaint.setColor(borderColor); 163 canvas.drawCircle(centerXY, centerXY, innerRadius + STROKE_WIDTH, fillPaint); 164 165 return bitmap; 166 } 167 drawBackground(Canvas canvas)168 private void drawBackground(Canvas canvas) { 169 if (background == null) { 170 background = prepareBackground(); 171 } 172 canvas.drawBitmap(background, 0, 0, fillPaint); 173 } 174 drawHighlighter(Canvas canvas)175 private void drawHighlighter(Canvas canvas) { 176 strokePaint.setColor(borderColor); 177 int diameter = centerXY * 2; 178 RectF drawBound = new RectF(0, 0, diameter, diameter); 179 float radiantDegrees = (float) Math.toDegrees(radiantInterval); 180 float startAngle = DEGREES_BEGIN + radiantDegrees * colorIndex; 181 canvas.drawArc(drawBound, startAngle, radiantDegrees, false, strokePaint); 182 drawBound.inset(COLOR_METER_THICKNESS, COLOR_METER_THICKNESS); 183 canvas.drawArc(drawBound, startAngle, radiantDegrees, false, strokePaint); 184 185 float lineAngle = ANGLE_BEGIN - radiantInterval * colorIndex; 186 float cosAngle = (float) Math.cos(lineAngle); 187 float sinAngle = (float) Math.sin(lineAngle); 188 int innerRadius = centerXY - COLOR_METER_THICKNESS; 189 canvas.drawLine(centerXY + centerXY * cosAngle, centerXY - centerXY * sinAngle, 190 centerXY + innerRadius * cosAngle, 191 centerXY - innerRadius * sinAngle, strokePaint); 192 193 lineAngle -= radiantInterval; 194 cosAngle = (float) Math.cos(lineAngle); 195 sinAngle = (float) Math.sin(lineAngle); 196 canvas.drawLine(centerXY + centerXY * cosAngle, centerXY - centerXY * sinAngle, 197 centerXY + innerRadius * cosAngle, 198 centerXY - innerRadius * sinAngle, strokePaint); 199 } 200 drawInnerCircle(Canvas canvas)201 private void drawInnerCircle(Canvas canvas) { 202 fillPaint.setColor(colorsDefined[colorIndex]); 203 canvas.drawCircle(centerXY, centerXY, innerRadius, fillPaint); 204 } 205 drawThumb(Canvas canvas)206 private void drawThumb(Canvas canvas) { 207 int thumbX = (int) (thumbRadius * Math.cos(angle) + centerXY); 208 int thumbY = (int) (centerXY - thumbRadius * Math.sin(angle)); 209 int halfSize = thumbSize / 2; 210 thumb.setBounds(thumbX - halfSize, thumbY - halfSize, thumbX + halfSize, thumbY + halfSize); 211 thumb.draw(canvas); 212 } 213 214 @Override onDraw(Canvas canvas)215 protected void onDraw(Canvas canvas) { 216 super.onDraw(canvas); 217 218 drawBackground(canvas); 219 drawInnerCircle(canvas); 220 drawHighlighter(canvas); 221 drawThumb(canvas); 222 } 223 updateAngle(float x, float y)224 private boolean updateAngle(float x, float y) { 225 float angle; 226 if (x == 0) { 227 if (y >= 0) { 228 angle = MATH_HALF_PI; 229 } else { 230 angle = -MATH_HALF_PI; 231 } 232 } else { 233 angle = (float) Math.atan((double) y / x); 234 } 235 236 if (angle >= 0 && x < 0) { 237 angle = angle - MATH_PI; 238 } else if (angle < 0 && x < 0) { 239 angle = MATH_PI + angle; 240 } 241 242 if (angle > ANGLE_BEGIN || angle <= ANGLE_BEGIN - ANGLE_SPANNED) { 243 return false; 244 } 245 246 this.angle = angle; 247 return true; 248 } 249 250 @Override onTouchEvent(MotionEvent ev)251 public boolean onTouchEvent(MotionEvent ev) { 252 super.onTouchEvent(ev); 253 254 if (isEnabled()) { 255 switch (ev.getAction()) { 256 case MotionEvent.ACTION_DOWN: 257 updateThumbState( 258 isHittingThumbArea(ev.getX() - centerXY, centerXY - ev.getY())); 259 break; 260 261 case MotionEvent.ACTION_MOVE: 262 final float x = ev.getX() - centerXY; 263 final float y = centerXY - ev.getY(); 264 if (!dragThumb && !updateThumbState(isHittingThumbArea(x, y))) { 265 // The thumb wasn't dragged and isn't being dragged, either. 266 break; 267 } 268 269 if (updateAngle(x, y)) { 270 int index = (int) ((ANGLE_BEGIN - angle) / radiantInterval); 271 if (updateColorIndex(index, true)) { 272 updateThumbPositionByColorIndex(); 273 } 274 } 275 break; 276 277 case MotionEvent.ACTION_CANCEL: 278 case MotionEvent.ACTION_UP: 279 updateThumbState(false); 280 break; 281 } 282 } 283 return true; 284 } 285 286 /** 287 * Returns true if the user is hitting the correct thumb area. 288 */ isHittingThumbArea(float x, float y)289 private boolean isHittingThumbArea(float x, float y) { 290 final float radius = (float) Math.sqrt((x * x) + (y * y)); 291 return (radius > innerRadius) && (radius < centerXY); 292 } 293 294 updateColorIndex(int index, boolean fromUser)295 private boolean updateColorIndex(int index, boolean fromUser) { 296 if (index < 0 || index >= colorsDefined.length) { 297 return false; 298 } 299 if (colorIndex != index) { 300 colorIndex = index; 301 302 if (listener != null) { 303 listener.onColorChanged(colorsDefined[colorIndex], fromUser); 304 } 305 return true; 306 } 307 return false; 308 } 309 310 /** 311 * Set the thumb position according to the selected color. 312 * The thumb will always be placed in the middle of the selected color. 313 */ updateThumbPositionByColorIndex()314 private void updateThumbPositionByColorIndex() { 315 angle = ANGLE_BEGIN - (colorIndex + 0.5f) * radiantInterval; 316 invalidate(); 317 } 318 updateThumbState(boolean dragThumb)319 private boolean updateThumbState(boolean dragThumb) { 320 if (this.dragThumb == dragThumb) { 321 // The state hasn't been changed; no need for updates. 322 return false; 323 } 324 325 this.dragThumb = dragThumb; 326 thumb.setState(dragThumb ? PRESSED_ENABLED_STATE_SET : ENABLED_STATE_SET); 327 invalidate(); 328 return true; 329 } 330 } 331