• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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