blob: d2f68d07fbbd5cd8d12662a5c6fff6ea595f9eca [file] [log] [blame]
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001/*
2 * Copyright (C) 2013 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
17package android.widget;
18
19import android.animation.Animator;
20import android.animation.AnimatorSet;
21import android.animation.Keyframe;
22import android.animation.ObjectAnimator;
23import android.animation.PropertyValuesHolder;
24import android.animation.ValueAnimator;
25import android.annotation.SuppressLint;
26import android.content.Context;
Alan Viverette60727e02014-07-28 16:56:32 -070027import android.content.res.ColorStateList;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070028import android.content.res.Resources;
29import android.content.res.TypedArray;
30import android.graphics.Canvas;
31import android.graphics.Color;
32import android.graphics.Paint;
33import android.graphics.Typeface;
34import android.graphics.RectF;
35import android.os.Bundle;
36import android.text.format.DateUtils;
37import android.text.format.Time;
38import android.util.AttributeSet;
39import android.util.Log;
Alan Viverette51344782014-07-16 17:39:27 -070040import android.util.TypedValue;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070041import android.view.HapticFeedbackConstants;
42import android.view.MotionEvent;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.accessibility.AccessibilityEvent;
46import android.view.accessibility.AccessibilityNodeInfo;
Alan Viveretteeb1d3792014-06-03 18:36:03 -070047
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070048import com.android.internal.R;
49
50import java.text.DateFormatSymbols;
51import java.util.ArrayList;
52import java.util.Calendar;
53import java.util.Locale;
54
55/**
56 * View to show a clock circle picker (with one or two picking circles)
57 *
58 * @hide
59 */
60public class RadialTimePickerView extends View implements View.OnTouchListener {
61 private static final String TAG = "ClockView";
62
63 private static final boolean DEBUG = false;
64
65 private static final int DEBUG_COLOR = 0x20FF0000;
66 private static final int DEBUG_TEXT_COLOR = 0x60FF0000;
67 private static final int DEBUG_STROKE_WIDTH = 2;
68
69 private static final int HOURS = 0;
70 private static final int MINUTES = 1;
71 private static final int HOURS_INNER = 2;
72 private static final int AMPM = 3;
73
74 private static final int SELECTOR_CIRCLE = 0;
75 private static final int SELECTOR_DOT = 1;
76 private static final int SELECTOR_LINE = 2;
77
78 private static final int AM = 0;
79 private static final int PM = 1;
80
81 // Opaque alpha level
82 private static final int ALPHA_OPAQUE = 255;
83
84 // Transparent alpha level
85 private static final int ALPHA_TRANSPARENT = 0;
86
87 // Alpha level of color for selector.
Alan Viverette518ff0d2014-08-15 14:20:35 -070088 private static final int ALPHA_SELECTOR = 60; // was 51
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070089
90 // Alpha level of color for selected circle.
91 private static final int ALPHA_AMPM_SELECTED = ALPHA_SELECTOR;
92
93 // Alpha level of color for pressed circle.
Alan Viveretteeb1d3792014-06-03 18:36:03 -070094 private static final int ALPHA_AMPM_PRESSED = 255; // was 175
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070095
96 private static final float COSINE_30_DEGREES = ((float) Math.sqrt(3)) * 0.5f;
97 private static final float SINE_30_DEGREES = 0.5f;
98
99 private static final int DEGREES_FOR_ONE_HOUR = 30;
100 private static final int DEGREES_FOR_ONE_MINUTE = 6;
101
102 private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
103 private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
104 private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
105
106 private static final int CENTER_RADIUS = 2;
107
Alan Viverette518ff0d2014-08-15 14:20:35 -0700108 private static final int[] STATE_SET_SELECTED = new int[] {R.attr.state_selected};
Alan Viverette60727e02014-07-28 16:56:32 -0700109
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700110 private static int[] sSnapPrefer30sMap = new int[361];
111
112 private final String[] mHours12Texts = new String[12];
113 private final String[] mOuterHours24Texts = new String[12];
114 private final String[] mInnerHours24Texts = new String[12];
115 private final String[] mMinutesTexts = new String[12];
116
117 private final String[] mAmPmText = new String[2];
118
119 private final Paint[] mPaint = new Paint[2];
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700120 private final int[] mColor = new int[2];
121 private final IntHolder[] mAlpha = new IntHolder[2];
122
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700123 private final Paint mPaintCenter = new Paint();
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700124
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700125 private final Paint[][] mPaintSelector = new Paint[2][3];
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700126 private final int[][] mColorSelector = new int[2][3];
127 private final IntHolder[][] mAlphaSelector = new IntHolder[2][3];
128
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700129 private final Paint mPaintAmPmText = new Paint();
130 private final Paint[] mPaintAmPmCircle = new Paint[2];
131
132 private final Paint mPaintBackground = new Paint();
133 private final Paint mPaintDisabled = new Paint();
134 private final Paint mPaintDebug = new Paint();
135
136 private Typeface mTypeface;
137
138 private boolean mIs24HourMode;
139 private boolean mShowHours;
140 private boolean mIsOnInnerCircle;
141
142 private int mXCenter;
143 private int mYCenter;
144
145 private float[] mCircleRadius = new float[3];
146
147 private int mMinHypotenuseForInnerNumber;
148 private int mMaxHypotenuseForOuterNumber;
149 private int mHalfwayHypotenusePoint;
150
151 private float[] mTextSize = new float[2];
152 private float mInnerTextSize;
153
154 private float[][] mTextGridHeights = new float[2][7];
155 private float[][] mTextGridWidths = new float[2][7];
156
157 private float[] mInnerTextGridHeights = new float[7];
158 private float[] mInnerTextGridWidths = new float[7];
159
160 private String[] mOuterTextHours;
161 private String[] mInnerTextHours;
162 private String[] mOuterTextMinutes;
163
164 private float[] mCircleRadiusMultiplier = new float[2];
165 private float[] mNumbersRadiusMultiplier = new float[3];
166
167 private float[] mTextSizeMultiplier = new float[3];
168
169 private float[] mAnimationRadiusMultiplier = new float[3];
170
171 private float mTransitionMidRadiusMultiplier;
172 private float mTransitionEndRadiusMultiplier;
173
174 private AnimatorSet mTransition;
175 private InvalidateUpdateListener mInvalidateUpdateListener = new InvalidateUpdateListener();
176
177 private int[] mLineLength = new int[3];
178 private int[] mSelectionRadius = new int[3];
179 private float mSelectionRadiusMultiplier;
180 private int[] mSelectionDegrees = new int[3];
181
182 private int mAmPmCircleRadius;
183 private float mAmPmYCenter;
184
185 private float mAmPmCircleRadiusMultiplier;
186 private int mAmPmTextColor;
187
188 private float mLeftIndicatorXCenter;
189 private float mRightIndicatorXCenter;
190
191 private int mAmPmUnselectedColor;
192 private int mAmPmSelectedColor;
193
194 private int mAmOrPm;
195 private int mAmOrPmPressed;
196
Alan Viverette51344782014-07-16 17:39:27 -0700197 private int mDisabledAlpha;
198
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700199 private RectF mRectF = new RectF();
200 private boolean mInputEnabled = true;
201 private OnValueSelectedListener mListener;
202
203 private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>();
204 private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>();
205
206 public interface OnValueSelectedListener {
207 void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
208 }
209
210 static {
211 // Prepare mapping to snap touchable degrees to selectable degrees.
212 preparePrefer30sMap();
213 }
214
215 /**
216 * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger
217 * selectable area to each of the 12 visible values, such that the ratio of space apportioned
218 * to a visible value : space apportioned to a non-visible value will be 14 : 4.
219 * E.g. the output of 30 degrees should have a higher range of input associated with it than
220 * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock
221 * circle (5 on the minutes, 1 or 13 on the hours).
222 */
223 private static void preparePrefer30sMap() {
224 // We'll split up the visible output and the non-visible output such that each visible
225 // output will correspond to a range of 14 associated input degrees, and each non-visible
226 // output will correspond to a range of 4 associate input degrees, so visible numbers
227 // are more than 3 times easier to get than non-visible numbers:
228 // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc.
229 //
230 // If an output of 30 degrees should correspond to a range of 14 associated degrees, then
231 // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should
232 // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you
233 // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this
234 // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the
235 // ability to aggressively prefer the visible values by a factor of more than 3:1, which
236 // greatly contributes to the selectability of these values.
237
238 // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}.
239 int snappedOutputDegrees = 0;
240 // Count of how many inputs we've designated to the specified output.
241 int count = 1;
242 // How many input we expect for a specified output. This will be 14 for output divisible
243 // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so
244 // the caller can decide which they need.
245 int expectedCount = 8;
246 // Iterate through the input.
247 for (int degrees = 0; degrees < 361; degrees++) {
248 // Save the input-output mapping.
249 sSnapPrefer30sMap[degrees] = snappedOutputDegrees;
250 // If this is the last input for the specified output, calculate the next output and
251 // the next expected count.
252 if (count == expectedCount) {
253 snappedOutputDegrees += 6;
254 if (snappedOutputDegrees == 360) {
255 expectedCount = 7;
256 } else if (snappedOutputDegrees % 30 == 0) {
257 expectedCount = 14;
258 } else {
259 expectedCount = 4;
260 }
261 count = 1;
262 } else {
263 count++;
264 }
265 }
266 }
267
268 /**
269 * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees,
270 * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be
271 * weighted heavier than the degrees corresponding to non-visible numbers.
272 * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the
273 * mapping.
274 */
275 private static int snapPrefer30s(int degrees) {
276 if (sSnapPrefer30sMap == null) {
277 return -1;
278 }
279 return sSnapPrefer30sMap[degrees];
280 }
281
282 /**
283 * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all
284 * multiples of 30), where the input will be "snapped" to the closest visible degrees.
285 * @param degrees The input degrees
286 * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may
287 * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force
288 * strictly lower, and 0 to snap to the closer one.
289 * @return output degrees, will be a multiple of 30
290 */
291 private static int snapOnly30s(int degrees, int forceHigherOrLower) {
292 final int stepSize = DEGREES_FOR_ONE_HOUR;
293 int floor = (degrees / stepSize) * stepSize;
294 final int ceiling = floor + stepSize;
295 if (forceHigherOrLower == 1) {
296 degrees = ceiling;
297 } else if (forceHigherOrLower == -1) {
298 if (degrees == floor) {
299 floor -= stepSize;
300 }
301 degrees = floor;
302 } else {
303 if ((degrees - floor) < (ceiling - degrees)) {
304 degrees = floor;
305 } else {
306 degrees = ceiling;
307 }
308 }
309 return degrees;
310 }
311
312 public RadialTimePickerView(Context context, AttributeSet attrs) {
313 this(context, attrs, R.attr.timePickerStyle);
314 }
315
316 public RadialTimePickerView(Context context, AttributeSet attrs, int defStyle) {
317 super(context, attrs);
318
Alan Viverette51344782014-07-16 17:39:27 -0700319 // Pull disabled alpha from theme.
320 final TypedValue outValue = new TypedValue();
321 context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true);
322 mDisabledAlpha = (int) (outValue.getFloat() * 255 + 0.5f);
323
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700324 // process style attributes
Alan Viverette51344782014-07-16 17:39:27 -0700325 final Resources res = getResources();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700326 final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker,
327 defStyle, 0);
328
Alan Viverette60727e02014-07-28 16:56:32 -0700329 ColorStateList amPmBackgroundColor = a.getColorStateList(
330 R.styleable.TimePicker_amPmBackgroundColor);
331 if (amPmBackgroundColor == null) {
332 amPmBackgroundColor = res.getColorStateList(
333 R.color.timepicker_default_ampm_unselected_background_color_material);
334 }
335
336 // Obtain the backup selected color. If the background color state
337 // list doesn't have a state for selected, we'll use this color.
338 final int amPmSelectedColor = a.getColor(R.styleable.TimePicker_amPmSelectedBackgroundColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700339 res.getColor(R.color.timepicker_default_ampm_selected_background_color_material));
Alan Viverette518ff0d2014-08-15 14:20:35 -0700340 amPmBackgroundColor = ColorStateList.addFirstIfMissing(
341 amPmBackgroundColor, R.attr.state_selected, amPmSelectedColor);
342
Alan Viverette60727e02014-07-28 16:56:32 -0700343 mAmPmSelectedColor = amPmBackgroundColor.getColorForState(
344 STATE_SET_SELECTED, amPmSelectedColor);
345 mAmPmUnselectedColor = amPmBackgroundColor.getDefaultColor();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700346
347 mAmPmTextColor = a.getColor(R.styleable.TimePicker_amPmTextColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700348 res.getColor(R.color.timepicker_default_text_color_material));
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700349
350 mTypeface = Typeface.create("sans-serif", Typeface.NORMAL);
351
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700352 // Initialize all alpha values to opaque.
353 for (int i = 0; i < mAlpha.length; i++) {
354 mAlpha[i] = new IntHolder(ALPHA_OPAQUE);
355 }
356 for (int i = 0; i < mAlphaSelector.length; i++) {
357 for (int j = 0; j < mAlphaSelector[i].length; j++) {
358 mAlphaSelector[i][j] = new IntHolder(ALPHA_OPAQUE);
359 }
360 }
361
362 final int numbersTextColor = a.getColor(R.styleable.TimePicker_numbersTextColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700363 res.getColor(R.color.timepicker_default_text_color_material));
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700364
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700365 mPaint[HOURS] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700366 mPaint[HOURS].setAntiAlias(true);
367 mPaint[HOURS].setTextAlign(Paint.Align.CENTER);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700368 mColor[HOURS] = numbersTextColor;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700369
370 mPaint[MINUTES] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700371 mPaint[MINUTES].setAntiAlias(true);
372 mPaint[MINUTES].setTextAlign(Paint.Align.CENTER);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700373 mColor[MINUTES] = numbersTextColor;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700374
375 mPaintCenter.setColor(numbersTextColor);
376 mPaintCenter.setAntiAlias(true);
377 mPaintCenter.setTextAlign(Paint.Align.CENTER);
378
379 mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700380 mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700381 mColorSelector[HOURS][SELECTOR_CIRCLE] = a.getColor(
382 R.styleable.TimePicker_numbersSelectorColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700383 R.color.timepicker_default_selector_color_material);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700384
385 mPaintSelector[HOURS][SELECTOR_DOT] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700386 mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700387 mColorSelector[HOURS][SELECTOR_DOT] = a.getColor(
388 R.styleable.TimePicker_numbersSelectorColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700389 R.color.timepicker_default_selector_color_material);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700390
391 mPaintSelector[HOURS][SELECTOR_LINE] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700392 mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true);
393 mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700394 mColorSelector[HOURS][SELECTOR_LINE] = a.getColor(
395 R.styleable.TimePicker_numbersSelectorColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700396 R.color.timepicker_default_selector_color_material);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700397
398 mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700399 mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700400 mColorSelector[MINUTES][SELECTOR_CIRCLE] = a.getColor(
401 R.styleable.TimePicker_numbersSelectorColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700402 R.color.timepicker_default_selector_color_material);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700403
404 mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700405 mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700406 mColorSelector[MINUTES][SELECTOR_DOT] = a.getColor(
407 R.styleable.TimePicker_numbersSelectorColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700408 R.color.timepicker_default_selector_color_material);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700409
410 mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700411 mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true);
412 mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700413 mColorSelector[MINUTES][SELECTOR_LINE] = a.getColor(
414 R.styleable.TimePicker_numbersSelectorColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700415 R.color.timepicker_default_selector_color_material);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700416
417 mPaintAmPmText.setColor(mAmPmTextColor);
418 mPaintAmPmText.setTypeface(mTypeface);
419 mPaintAmPmText.setAntiAlias(true);
420 mPaintAmPmText.setTextAlign(Paint.Align.CENTER);
421
422 mPaintAmPmCircle[AM] = new Paint();
423 mPaintAmPmCircle[AM].setAntiAlias(true);
424 mPaintAmPmCircle[PM] = new Paint();
425 mPaintAmPmCircle[PM].setAntiAlias(true);
426
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700427 mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor,
Alan Viverette830960c2014-06-06 15:48:55 -0700428 res.getColor(R.color.timepicker_default_numbers_background_color_material)));
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700429 mPaintBackground.setAntiAlias(true);
430
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700431 if (DEBUG) {
432 mPaintDebug.setColor(DEBUG_COLOR);
433 mPaintDebug.setAntiAlias(true);
434 mPaintDebug.setStrokeWidth(DEBUG_STROKE_WIDTH);
435 mPaintDebug.setStyle(Paint.Style.STROKE);
436 mPaintDebug.setTextAlign(Paint.Align.CENTER);
437 }
438
439 mShowHours = true;
440 mIs24HourMode = false;
441 mAmOrPm = AM;
442 mAmOrPmPressed = -1;
443
444 initHoursAndMinutesText();
445 initData();
446
447 mTransitionMidRadiusMultiplier = Float.parseFloat(
448 res.getString(R.string.timepicker_transition_mid_radius_multiplier));
449 mTransitionEndRadiusMultiplier = Float.parseFloat(
450 res.getString(R.string.timepicker_transition_end_radius_multiplier));
451
452 mTextGridHeights[HOURS] = new float[7];
453 mTextGridHeights[MINUTES] = new float[7];
454
455 mSelectionRadiusMultiplier = Float.parseFloat(
456 res.getString(R.string.timepicker_selection_radius_multiplier));
457
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700458 a.recycle();
459
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700460 setOnTouchListener(this);
Alan Viveretteba9bf412014-09-03 20:14:21 -0700461 setClickable(true);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700462
463 // Initial values
464 final Calendar calendar = Calendar.getInstance(Locale.getDefault());
465 final int currentHour = calendar.get(Calendar.HOUR_OF_DAY);
466 final int currentMinute = calendar.get(Calendar.MINUTE);
467
468 setCurrentHour(currentHour);
469 setCurrentMinute(currentMinute);
470
471 setHapticFeedbackEnabled(true);
472 }
473
474 /**
475 * Measure the view to end up as a square, based on the minimum of the height and width.
476 */
477 @Override
478 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
479 int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
480 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
481 int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
482 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
483 int minDimension = Math.min(measuredWidth, measuredHeight);
484
485 super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode),
486 MeasureSpec.makeMeasureSpec(minDimension, heightMode));
487 }
488
489 public void initialize(int hour, int minute, boolean is24HourMode) {
490 mIs24HourMode = is24HourMode;
491 setCurrentHour(hour);
492 setCurrentMinute(minute);
493 }
494
495 public void setCurrentItemShowing(int item, boolean animate) {
496 switch (item){
497 case HOURS:
498 showHours(animate);
499 break;
500 case MINUTES:
501 showMinutes(animate);
502 break;
503 default:
504 Log.e(TAG, "ClockView does not support showing item " + item);
505 }
506 }
507
508 public int getCurrentItemShowing() {
509 return mShowHours ? HOURS : MINUTES;
510 }
511
512 public void setOnValueSelectedListener(OnValueSelectedListener listener) {
513 mListener = listener;
514 }
515
516 public void setCurrentHour(int hour) {
517 final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR;
518 mSelectionDegrees[HOURS] = degrees;
519 mSelectionDegrees[HOURS_INNER] = degrees;
520 mAmOrPm = ((hour % 24) < 12) ? AM : PM;
521 if (mIs24HourMode) {
522 mIsOnInnerCircle = (mAmOrPm == AM);
523 } else {
524 mIsOnInnerCircle = false;
525 }
526 initData();
527 updateLayoutData();
528 invalidate();
529 }
530
531 // Return hours in 0-23 range
532 public int getCurrentHour() {
533 int hours =
534 mSelectionDegrees[mIsOnInnerCircle ? HOURS_INNER : HOURS] / DEGREES_FOR_ONE_HOUR;
535 if (mIs24HourMode) {
536 if (mIsOnInnerCircle) {
537 hours = hours % 12;
Fabrice Di Meglio3fbad422014-07-24 20:13:56 -0700538 if (hours == 0) {
539 hours = 12;
540 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700541 } else {
542 if (hours != 0) {
543 hours += 12;
544 }
545 }
546 } else {
547 hours = hours % 12;
548 if (hours == 0) {
549 if (mAmOrPm == PM) {
550 hours = 12;
551 }
552 } else {
553 if (mAmOrPm == PM) {
554 hours += 12;
555 }
556 }
557 }
558 return hours;
559 }
560
561 public void setCurrentMinute(int minute) {
562 mSelectionDegrees[MINUTES] = (minute % 60) * DEGREES_FOR_ONE_MINUTE;
563 invalidate();
564 }
565
566 // Returns minutes in 0-59 range
567 public int getCurrentMinute() {
568 return (mSelectionDegrees[MINUTES] / DEGREES_FOR_ONE_MINUTE);
569 }
570
571 public void setAmOrPm(int val) {
572 mAmOrPm = (val % 2);
573 invalidate();
574 }
575
576 public int getAmOrPm() {
577 return mAmOrPm;
578 }
579
580 public void swapAmPm() {
581 mAmOrPm = (mAmOrPm == AM) ? PM : AM;
582 invalidate();
583 }
584
585 public void showHours(boolean animate) {
586 if (mShowHours) return;
587 mShowHours = true;
588 if (animate) {
589 startMinutesToHoursAnimation();
590 }
591 initData();
592 updateLayoutData();
593 invalidate();
594 }
595
596 public void showMinutes(boolean animate) {
597 if (!mShowHours) return;
598 mShowHours = false;
599 if (animate) {
600 startHoursToMinutesAnimation();
601 }
602 initData();
603 updateLayoutData();
604 invalidate();
605 }
606
607 private void initHoursAndMinutesText() {
608 // Initialize the hours and minutes numbers.
609 for (int i = 0; i < 12; i++) {
610 mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
611 mOuterHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]);
612 mInnerHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
613 mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]);
614 }
615
Alan Viveretteba9bf412014-09-03 20:14:21 -0700616 String[] amPmStrings = TimePickerClockDelegate.getAmPmStrings(mContext);
617 mAmPmText[AM] = amPmStrings[0];
618 mAmPmText[PM] = amPmStrings[1];
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700619 }
620
621 private void initData() {
622 if (mIs24HourMode) {
623 mOuterTextHours = mOuterHours24Texts;
624 mInnerTextHours = mInnerHours24Texts;
625 } else {
626 mOuterTextHours = mHours12Texts;
627 mInnerTextHours = null;
628 }
629
630 mOuterTextMinutes = mMinutesTexts;
631
632 final Resources res = getResources();
633
634 if (mShowHours) {
635 if (mIs24HourMode) {
636 mCircleRadiusMultiplier[HOURS] = Float.parseFloat(
637 res.getString(R.string.timepicker_circle_radius_multiplier_24HourMode));
638 mNumbersRadiusMultiplier[HOURS] = Float.parseFloat(
639 res.getString(R.string.timepicker_numbers_radius_multiplier_outer));
640 mTextSizeMultiplier[HOURS] = Float.parseFloat(
641 res.getString(R.string.timepicker_text_size_multiplier_outer));
642
643 mNumbersRadiusMultiplier[HOURS_INNER] = Float.parseFloat(
644 res.getString(R.string.timepicker_numbers_radius_multiplier_inner));
645 mTextSizeMultiplier[HOURS_INNER] = Float.parseFloat(
646 res.getString(R.string.timepicker_text_size_multiplier_inner));
647 } else {
648 mCircleRadiusMultiplier[HOURS] = Float.parseFloat(
649 res.getString(R.string.timepicker_circle_radius_multiplier));
650 mNumbersRadiusMultiplier[HOURS] = Float.parseFloat(
651 res.getString(R.string.timepicker_numbers_radius_multiplier_normal));
652 mTextSizeMultiplier[HOURS] = Float.parseFloat(
653 res.getString(R.string.timepicker_text_size_multiplier_normal));
654 }
655 } else {
656 mCircleRadiusMultiplier[MINUTES] = Float.parseFloat(
657 res.getString(R.string.timepicker_circle_radius_multiplier));
658 mNumbersRadiusMultiplier[MINUTES] = Float.parseFloat(
659 res.getString(R.string.timepicker_numbers_radius_multiplier_normal));
660 mTextSizeMultiplier[MINUTES] = Float.parseFloat(
661 res.getString(R.string.timepicker_text_size_multiplier_normal));
662 }
663
664 mAnimationRadiusMultiplier[HOURS] = 1;
665 mAnimationRadiusMultiplier[HOURS_INNER] = 1;
666 mAnimationRadiusMultiplier[MINUTES] = 1;
667
668 mAmPmCircleRadiusMultiplier = Float.parseFloat(
669 res.getString(R.string.timepicker_ampm_circle_radius_multiplier));
670
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700671 mAlpha[HOURS].setValue(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT);
672 mAlpha[MINUTES].setValue(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700673
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700674 mAlphaSelector[HOURS][SELECTOR_CIRCLE].setValue(
675 mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT);
676 mAlphaSelector[HOURS][SELECTOR_DOT].setValue(
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700677 mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700678 mAlphaSelector[HOURS][SELECTOR_LINE].setValue(
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700679 mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT);
680
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700681 mAlphaSelector[MINUTES][SELECTOR_CIRCLE].setValue(
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700682 mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700683 mAlphaSelector[MINUTES][SELECTOR_DOT].setValue(
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700684 mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700685 mAlphaSelector[MINUTES][SELECTOR_LINE].setValue(
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700686 mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR);
687 }
688
689 @Override
690 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
691 updateLayoutData();
692 }
693
694 private void updateLayoutData() {
695 mXCenter = getWidth() / 2;
696 mYCenter = getHeight() / 2;
697
698 final int min = Math.min(mXCenter, mYCenter);
699
700 mCircleRadius[HOURS] = min * mCircleRadiusMultiplier[HOURS];
701 mCircleRadius[HOURS_INNER] = min * mCircleRadiusMultiplier[HOURS];
702 mCircleRadius[MINUTES] = min * mCircleRadiusMultiplier[MINUTES];
703
704 if (!mIs24HourMode) {
705 // We'll need to draw the AM/PM circles, so the main circle will need to have
706 // a slightly higher center. To keep the entire view centered vertically, we'll
707 // have to push it up by half the radius of the AM/PM circles.
708 int amPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier);
709 mYCenter -= amPmCircleRadius / 2;
710 }
711
712 mMinHypotenuseForInnerNumber = (int) (mCircleRadius[HOURS]
713 * mNumbersRadiusMultiplier[HOURS_INNER]) - mSelectionRadius[HOURS];
714 mMaxHypotenuseForOuterNumber = (int) (mCircleRadius[HOURS]
715 * mNumbersRadiusMultiplier[HOURS]) + mSelectionRadius[HOURS];
716 mHalfwayHypotenusePoint = (int) (mCircleRadius[HOURS]
717 * ((mNumbersRadiusMultiplier[HOURS] + mNumbersRadiusMultiplier[HOURS_INNER]) / 2));
718
719 mTextSize[HOURS] = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS];
720 mTextSize[MINUTES] = mCircleRadius[MINUTES] * mTextSizeMultiplier[MINUTES];
721
722 if (mIs24HourMode) {
723 mInnerTextSize = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS_INNER];
724 }
725
726 calculateGridSizesHours();
727 calculateGridSizesMinutes();
728
729 mSelectionRadius[HOURS] = (int) (mCircleRadius[HOURS] * mSelectionRadiusMultiplier);
730 mSelectionRadius[HOURS_INNER] = mSelectionRadius[HOURS];
731 mSelectionRadius[MINUTES] = (int) (mCircleRadius[MINUTES] * mSelectionRadiusMultiplier);
732
733 mAmPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier);
734 mPaintAmPmText.setTextSize(mAmPmCircleRadius * 3 / 4);
735
736 // Line up the vertical center of the AM/PM circles with the bottom of the main circle.
737 mAmPmYCenter = mYCenter + mCircleRadius[HOURS];
738
739 // Line up the horizontal edges of the AM/PM circles with the horizontal edges
740 // of the main circle
741 mLeftIndicatorXCenter = mXCenter - mCircleRadius[HOURS] + mAmPmCircleRadius;
742 mRightIndicatorXCenter = mXCenter + mCircleRadius[HOURS] - mAmPmCircleRadius;
743 }
744
745 @Override
746 public void onDraw(Canvas canvas) {
Alan Viverette51344782014-07-16 17:39:27 -0700747 if (!mInputEnabled) {
748 canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), mDisabledAlpha);
749 } else {
750 canvas.save();
751 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700752
753 calculateGridSizesHours();
754 calculateGridSizesMinutes();
755
756 drawCircleBackground(canvas);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700757 drawSelector(canvas);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700758
759 drawTextElements(canvas, mTextSize[HOURS], mTypeface, mOuterTextHours,
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700760 mTextGridWidths[HOURS], mTextGridHeights[HOURS], mPaint[HOURS],
761 mColor[HOURS], mAlpha[HOURS].getValue());
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700762
763 if (mIs24HourMode && mInnerTextHours != null) {
764 drawTextElements(canvas, mInnerTextSize, mTypeface, mInnerTextHours,
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700765 mInnerTextGridWidths, mInnerTextGridHeights, mPaint[HOURS],
766 mColor[HOURS], mAlpha[HOURS].getValue());
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700767 }
768
769 drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mOuterTextMinutes,
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700770 mTextGridWidths[MINUTES], mTextGridHeights[MINUTES], mPaint[MINUTES],
771 mColor[MINUTES], mAlpha[MINUTES].getValue());
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700772
773 drawCenter(canvas);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700774 if (!mIs24HourMode) {
775 drawAmPm(canvas);
776 }
777
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700778 if (DEBUG) {
779 drawDebug(canvas);
780 }
781
782 canvas.restore();
783 }
784
785 private void drawCircleBackground(Canvas canvas) {
786 canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintBackground);
787 }
788
789 private void drawCenter(Canvas canvas) {
790 canvas.drawCircle(mXCenter, mYCenter, CENTER_RADIUS, mPaintCenter);
791 }
792
793 private void drawSelector(Canvas canvas) {
794 drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS);
795 drawSelector(canvas, MINUTES);
796 }
797
798 private void drawAmPm(Canvas canvas) {
799 final boolean isLayoutRtl = isLayoutRtl();
800
801 int amColor = mAmPmUnselectedColor;
802 int amAlpha = ALPHA_OPAQUE;
803 int pmColor = mAmPmUnselectedColor;
804 int pmAlpha = ALPHA_OPAQUE;
805 if (mAmOrPm == AM) {
806 amColor = mAmPmSelectedColor;
807 amAlpha = ALPHA_AMPM_SELECTED;
808 } else if (mAmOrPm == PM) {
809 pmColor = mAmPmSelectedColor;
810 pmAlpha = ALPHA_AMPM_SELECTED;
811 }
812 if (mAmOrPmPressed == AM) {
813 amColor = mAmPmSelectedColor;
814 amAlpha = ALPHA_AMPM_PRESSED;
815 } else if (mAmOrPmPressed == PM) {
816 pmColor = mAmPmSelectedColor;
817 pmAlpha = ALPHA_AMPM_PRESSED;
818 }
819
820 // Draw the two circles
821 mPaintAmPmCircle[AM].setColor(amColor);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700822 mPaintAmPmCircle[AM].setAlpha(getMultipliedAlpha(amColor, amAlpha));
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700823 canvas.drawCircle(isLayoutRtl ? mRightIndicatorXCenter : mLeftIndicatorXCenter,
824 mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[AM]);
825
826 mPaintAmPmCircle[PM].setColor(pmColor);
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700827 mPaintAmPmCircle[PM].setAlpha(getMultipliedAlpha(pmColor, pmAlpha));
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700828 canvas.drawCircle(isLayoutRtl ? mLeftIndicatorXCenter : mRightIndicatorXCenter,
829 mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[PM]);
830
831 // Draw the AM/PM texts on top
832 mPaintAmPmText.setColor(mAmPmTextColor);
833 float textYCenter = mAmPmYCenter -
834 (int) (mPaintAmPmText.descent() + mPaintAmPmText.ascent()) / 2;
835
836 canvas.drawText(isLayoutRtl ? mAmPmText[PM] : mAmPmText[AM], mLeftIndicatorXCenter,
837 textYCenter, mPaintAmPmText);
838 canvas.drawText(isLayoutRtl ? mAmPmText[AM] : mAmPmText[PM], mRightIndicatorXCenter,
839 textYCenter, mPaintAmPmText);
840 }
841
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700842 private int getMultipliedAlpha(int argb, int alpha) {
843 return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5);
844 }
845
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700846 private void drawSelector(Canvas canvas, int index) {
847 // Calculate the current radius at which to place the selection circle.
848 mLineLength[index] = (int) (mCircleRadius[index]
849 * mNumbersRadiusMultiplier[index] * mAnimationRadiusMultiplier[index]);
850
851 double selectionRadians = Math.toRadians(mSelectionDegrees[index]);
852
853 int pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians));
854 int pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians));
855
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700856 int color;
857 int alpha;
858 Paint paint;
859
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700860 // Draw the selection circle
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700861 color = mColorSelector[index % 2][SELECTOR_CIRCLE];
862 alpha = mAlphaSelector[index % 2][SELECTOR_CIRCLE].getValue();
863 paint = mPaintSelector[index % 2][SELECTOR_CIRCLE];
864 paint.setColor(color);
865 paint.setAlpha(getMultipliedAlpha(color, alpha));
866 canvas.drawCircle(pointX, pointY, mSelectionRadius[index], paint);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700867
868 // Draw the dot if needed
869 if (mSelectionDegrees[index] % 30 != 0) {
870 // We're not on a direct tick
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700871 color = mColorSelector[index % 2][SELECTOR_DOT];
872 alpha = mAlphaSelector[index % 2][SELECTOR_DOT].getValue();
873 paint = mPaintSelector[index % 2][SELECTOR_DOT];
874 paint.setColor(color);
875 paint.setAlpha(getMultipliedAlpha(color, alpha));
876 canvas.drawCircle(pointX, pointY, (mSelectionRadius[index] * 2 / 7), paint);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700877 } else {
878 // We're not drawing the dot, so shorten the line to only go as far as the edge of the
879 // selection circle
880 int lineLength = mLineLength[index] - mSelectionRadius[index];
881 pointX = mXCenter + (int) (lineLength * Math.sin(selectionRadians));
882 pointY = mYCenter - (int) (lineLength * Math.cos(selectionRadians));
883 }
884
885 // Draw the line
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700886 color = mColorSelector[index % 2][SELECTOR_LINE];
887 alpha = mAlphaSelector[index % 2][SELECTOR_LINE].getValue();
888 paint = mPaintSelector[index % 2][SELECTOR_LINE];
889 paint.setColor(color);
890 paint.setAlpha(getMultipliedAlpha(color, alpha));
891 canvas.drawLine(mXCenter, mYCenter, pointX, pointY, paint);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700892 }
893
894 private void drawDebug(Canvas canvas) {
895 // Draw outer numbers circle
896 final float outerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS];
897 canvas.drawCircle(mXCenter, mYCenter, outerRadius, mPaintDebug);
898
899 // Draw inner numbers circle
900 final float innerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS_INNER];
901 canvas.drawCircle(mXCenter, mYCenter, innerRadius, mPaintDebug);
902
903 // Draw outer background circle
904 canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintDebug);
905
906 // Draw outer rectangle for circles
907 float left = mXCenter - outerRadius;
908 float top = mYCenter - outerRadius;
909 float right = mXCenter + outerRadius;
910 float bottom = mYCenter + outerRadius;
911 mRectF = new RectF(left, top, right, bottom);
912 canvas.drawRect(mRectF, mPaintDebug);
913
914 // Draw outer rectangle for background
915 left = mXCenter - mCircleRadius[HOURS];
916 top = mYCenter - mCircleRadius[HOURS];
917 right = mXCenter + mCircleRadius[HOURS];
918 bottom = mYCenter + mCircleRadius[HOURS];
919 mRectF.set(left, top, right, bottom);
920 canvas.drawRect(mRectF, mPaintDebug);
921
922 // Draw outer view rectangle
923 mRectF.set(0, 0, getWidth(), getHeight());
924 canvas.drawRect(mRectF, mPaintDebug);
925
926 // Draw selected time
927 final String selected = String.format("%02d:%02d", getCurrentHour(), getCurrentMinute());
928
929 ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
930 ViewGroup.LayoutParams.WRAP_CONTENT);
931 TextView tv = new TextView(getContext());
932 tv.setLayoutParams(lp);
933 tv.setText(selected);
934 tv.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
935 Paint paint = tv.getPaint();
936 paint.setColor(DEBUG_TEXT_COLOR);
937
938 final int width = tv.getMeasuredWidth();
939
940 float height = paint.descent() - paint.ascent();
941 float x = mXCenter - width / 2;
942 float y = mYCenter + 1.5f * height;
943
944 canvas.drawText(selected.toString(), x, y, paint);
945 }
946
947 private void calculateGridSizesHours() {
948 // Calculate the text positions
949 float numbersRadius = mCircleRadius[HOURS]
950 * mNumbersRadiusMultiplier[HOURS] * mAnimationRadiusMultiplier[HOURS];
951
952 // Calculate the positions for the 12 numbers in the main circle.
953 calculateGridSizes(mPaint[HOURS], numbersRadius, mXCenter, mYCenter,
954 mTextSize[HOURS], mTextGridHeights[HOURS], mTextGridWidths[HOURS]);
955
956 // If we have an inner circle, calculate those positions too.
957 if (mIs24HourMode) {
958 float innerNumbersRadius = mCircleRadius[HOURS_INNER]
959 * mNumbersRadiusMultiplier[HOURS_INNER]
960 * mAnimationRadiusMultiplier[HOURS_INNER];
961
962 calculateGridSizes(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter,
963 mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths);
964 }
965 }
966
967 private void calculateGridSizesMinutes() {
968 // Calculate the text positions
969 float numbersRadius = mCircleRadius[MINUTES]
970 * mNumbersRadiusMultiplier[MINUTES] * mAnimationRadiusMultiplier[MINUTES];
971
972 // Calculate the positions for the 12 numbers in the main circle.
973 calculateGridSizes(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter,
974 mTextSize[MINUTES], mTextGridHeights[MINUTES], mTextGridWidths[MINUTES]);
975 }
976
977
978 /**
979 * Using the trigonometric Unit Circle, calculate the positions that the text will need to be
980 * drawn at based on the specified circle radius. Place the values in the textGridHeights and
981 * textGridWidths parameters.
982 */
983 private static void calculateGridSizes(Paint paint, float numbersRadius, float xCenter,
984 float yCenter, float textSize, float[] textGridHeights, float[] textGridWidths) {
985 /*
986 * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle.
987 */
988 final float offset1 = numbersRadius;
989 // cos(30) = a / r => r * cos(30)
990 final float offset2 = numbersRadius * COSINE_30_DEGREES;
991 // sin(30) = o / r => r * sin(30)
992 final float offset3 = numbersRadius * SINE_30_DEGREES;
993
994 paint.setTextSize(textSize);
995 // We'll need yTextBase to be slightly lower to account for the text's baseline.
996 yCenter -= (paint.descent() + paint.ascent()) / 2;
997
998 textGridHeights[0] = yCenter - offset1;
999 textGridWidths[0] = xCenter - offset1;
1000 textGridHeights[1] = yCenter - offset2;
1001 textGridWidths[1] = xCenter - offset2;
1002 textGridHeights[2] = yCenter - offset3;
1003 textGridWidths[2] = xCenter - offset3;
1004 textGridHeights[3] = yCenter;
1005 textGridWidths[3] = xCenter;
1006 textGridHeights[4] = yCenter + offset3;
1007 textGridWidths[4] = xCenter + offset3;
1008 textGridHeights[5] = yCenter + offset2;
1009 textGridWidths[5] = xCenter + offset2;
1010 textGridHeights[6] = yCenter + offset1;
1011 textGridWidths[6] = xCenter + offset1;
1012 }
1013
1014 /**
1015 * Draw the 12 text values at the positions specified by the textGrid parameters.
1016 */
1017 private void drawTextElements(Canvas canvas, float textSize, Typeface typeface, String[] texts,
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001018 float[] textGridWidths, float[] textGridHeights, Paint paint, int color, int alpha) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001019 paint.setTextSize(textSize);
1020 paint.setTypeface(typeface);
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001021 paint.setColor(color);
1022 paint.setAlpha(getMultipliedAlpha(color, alpha));
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001023 canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], paint);
1024 canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], paint);
1025 canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], paint);
1026 canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], paint);
1027 canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], paint);
1028 canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], paint);
1029 canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], paint);
1030 canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], paint);
1031 canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], paint);
1032 canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], paint);
1033 canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], paint);
1034 canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], paint);
1035 }
1036
1037 // Used for animating the hours by changing their radius
1038 private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) {
1039 mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier;
1040 mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier;
1041 }
1042
1043 // Used for animating the minutes by changing their radius
1044 private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) {
1045 mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier;
1046 }
1047
1048 private static ObjectAnimator getRadiusDisappearAnimator(Object target,
1049 String radiusPropertyName, InvalidateUpdateListener updateListener,
1050 float midRadiusMultiplier, float endRadiusMultiplier) {
1051 Keyframe kf0, kf1, kf2;
1052 float midwayPoint = 0.2f;
1053 int duration = 500;
1054
1055 kf0 = Keyframe.ofFloat(0f, 1);
1056 kf1 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier);
1057 kf2 = Keyframe.ofFloat(1f, endRadiusMultiplier);
1058 PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
1059 radiusPropertyName, kf0, kf1, kf2);
1060
1061 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
1062 target, radiusDisappear).setDuration(duration);
1063 animator.addUpdateListener(updateListener);
1064 return animator;
1065 }
1066
1067 private static ObjectAnimator getRadiusReappearAnimator(Object target,
1068 String radiusPropertyName, InvalidateUpdateListener updateListener,
1069 float midRadiusMultiplier, float endRadiusMultiplier) {
1070 Keyframe kf0, kf1, kf2, kf3;
1071 float midwayPoint = 0.2f;
1072 int duration = 500;
1073
1074 // Set up animator for reappearing.
1075 float delayMultiplier = 0.25f;
1076 float transitionDurationMultiplier = 1f;
1077 float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
1078 int totalDuration = (int) (duration * totalDurationMultiplier);
1079 float delayPoint = (delayMultiplier * duration) / totalDuration;
1080 midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
1081
1082 kf0 = Keyframe.ofFloat(0f, endRadiusMultiplier);
1083 kf1 = Keyframe.ofFloat(delayPoint, endRadiusMultiplier);
1084 kf2 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier);
1085 kf3 = Keyframe.ofFloat(1f, 1);
1086 PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
1087 radiusPropertyName, kf0, kf1, kf2, kf3);
1088
1089 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
1090 target, radiusReappear).setDuration(totalDuration);
1091 animator.addUpdateListener(updateListener);
1092 return animator;
1093 }
1094
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001095 private static ObjectAnimator getFadeOutAnimator(IntHolder target, int startAlpha, int endAlpha,
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001096 InvalidateUpdateListener updateListener) {
1097 int duration = 500;
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001098 ObjectAnimator animator = ObjectAnimator.ofInt(target, "value", startAlpha, endAlpha);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001099 animator.setDuration(duration);
1100 animator.addUpdateListener(updateListener);
1101
1102 return animator;
1103 }
1104
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001105 private static ObjectAnimator getFadeInAnimator(IntHolder target, int startAlpha, int endAlpha,
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001106 InvalidateUpdateListener updateListener) {
1107 Keyframe kf0, kf1, kf2;
1108 int duration = 500;
1109
1110 // Set up animator for reappearing.
1111 float delayMultiplier = 0.25f;
1112 float transitionDurationMultiplier = 1f;
1113 float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
1114 int totalDuration = (int) (duration * totalDurationMultiplier);
1115 float delayPoint = (delayMultiplier * duration) / totalDuration;
1116
1117 kf0 = Keyframe.ofInt(0f, startAlpha);
1118 kf1 = Keyframe.ofInt(delayPoint, startAlpha);
1119 kf2 = Keyframe.ofInt(1f, endAlpha);
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001120 PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("value", kf0, kf1, kf2);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001121
1122 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
1123 target, fadeIn).setDuration(totalDuration);
1124 animator.addUpdateListener(updateListener);
1125 return animator;
1126 }
1127
1128 private class InvalidateUpdateListener implements ValueAnimator.AnimatorUpdateListener {
1129 @Override
1130 public void onAnimationUpdate(ValueAnimator animation) {
1131 RadialTimePickerView.this.invalidate();
1132 }
1133 }
1134
1135 private void startHoursToMinutesAnimation() {
1136 if (mHoursToMinutesAnims.size() == 0) {
1137 mHoursToMinutesAnims.add(getRadiusDisappearAnimator(this,
1138 "animationRadiusMultiplierHours", mInvalidateUpdateListener,
1139 mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001140 mHoursToMinutesAnims.add(getFadeOutAnimator(mAlpha[HOURS],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001141 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001142 mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001143 ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001144 mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_DOT],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001145 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001146 mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_LINE],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001147 ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
1148
1149 mHoursToMinutesAnims.add(getRadiusReappearAnimator(this,
1150 "animationRadiusMultiplierMinutes", mInvalidateUpdateListener,
1151 mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001152 mHoursToMinutesAnims.add(getFadeInAnimator(mAlpha[MINUTES],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001153 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001154 mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001155 ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001156 mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001157 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001158 mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001159 ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener));
1160 }
1161
1162 if (mTransition != null && mTransition.isRunning()) {
1163 mTransition.end();
1164 }
1165 mTransition = new AnimatorSet();
1166 mTransition.playTogether(mHoursToMinutesAnims);
1167 mTransition.start();
1168 }
1169
1170 private void startMinutesToHoursAnimation() {
1171 if (mMinuteToHoursAnims.size() == 0) {
1172 mMinuteToHoursAnims.add(getRadiusDisappearAnimator(this,
1173 "animationRadiusMultiplierMinutes", mInvalidateUpdateListener,
1174 mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001175 mMinuteToHoursAnims.add(getFadeOutAnimator(mAlpha[MINUTES],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001176 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001177 mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001178 ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001179 mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001180 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001181 mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001182 ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
1183
1184 mMinuteToHoursAnims.add(getRadiusReappearAnimator(this,
1185 "animationRadiusMultiplierHours", mInvalidateUpdateListener,
1186 mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001187 mMinuteToHoursAnims.add(getFadeInAnimator(mAlpha[HOURS],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001188 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001189 mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001190 ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001191 mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_DOT],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001192 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001193 mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_LINE],
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001194 ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener));
1195 }
1196
1197 if (mTransition != null && mTransition.isRunning()) {
1198 mTransition.end();
1199 }
1200 mTransition = new AnimatorSet();
1201 mTransition.playTogether(mMinuteToHoursAnims);
1202 mTransition.start();
1203 }
1204
1205 private int getDegreesFromXY(float x, float y) {
1206 final double hypotenuse = Math.sqrt(
1207 (y - mYCenter) * (y - mYCenter) + (x - mXCenter) * (x - mXCenter));
1208
1209 // Basic check if we're outside the range of the disk
1210 if (hypotenuse > mCircleRadius[HOURS]) {
1211 return -1;
1212 }
1213 // Check
1214 if (mIs24HourMode && mShowHours) {
1215 if (hypotenuse >= mMinHypotenuseForInnerNumber
1216 && hypotenuse <= mHalfwayHypotenusePoint) {
1217 mIsOnInnerCircle = true;
1218 } else if (hypotenuse <= mMaxHypotenuseForOuterNumber
1219 && hypotenuse >= mHalfwayHypotenusePoint) {
1220 mIsOnInnerCircle = false;
1221 } else {
1222 return -1;
1223 }
1224 } else {
1225 final int index = (mShowHours) ? HOURS : MINUTES;
1226 final float length = (mCircleRadius[index] * mNumbersRadiusMultiplier[index]);
1227 final int distanceToNumber = (int) Math.abs(hypotenuse - length);
1228 final int maxAllowedDistance =
1229 (int) (mCircleRadius[index] * (1 - mNumbersRadiusMultiplier[index]));
1230 if (distanceToNumber > maxAllowedDistance) {
1231 return -1;
1232 }
1233 }
1234
1235 final float opposite = Math.abs(y - mYCenter);
1236 double degrees = Math.toDegrees(Math.asin(opposite / hypotenuse));
1237
1238 // Now we have to translate to the correct quadrant.
1239 boolean rightSide = (x > mXCenter);
1240 boolean topSide = (y < mYCenter);
1241 if (rightSide && topSide) {
1242 degrees = 90 - degrees;
1243 } else if (rightSide && !topSide) {
1244 degrees = 90 + degrees;
1245 } else if (!rightSide && !topSide) {
1246 degrees = 270 - degrees;
1247 } else if (!rightSide && topSide) {
1248 degrees = 270 + degrees;
1249 }
1250 return (int) degrees;
1251 }
1252
1253 private int getIsTouchingAmOrPm(float x, float y) {
1254 final boolean isLayoutRtl = isLayoutRtl();
1255 int squaredYDistance = (int) ((y - mAmPmYCenter) * (y - mAmPmYCenter));
1256
1257 int distanceToAmCenter = (int) Math.sqrt(
1258 (x - mLeftIndicatorXCenter) * (x - mLeftIndicatorXCenter) + squaredYDistance);
1259 if (distanceToAmCenter <= mAmPmCircleRadius) {
1260 return (isLayoutRtl ? PM : AM);
1261 }
1262
1263 int distanceToPmCenter = (int) Math.sqrt(
1264 (x - mRightIndicatorXCenter) * (x - mRightIndicatorXCenter) + squaredYDistance);
1265 if (distanceToPmCenter <= mAmPmCircleRadius) {
1266 return (isLayoutRtl ? AM : PM);
1267 }
1268
1269 // Neither was close enough.
1270 return -1;
1271 }
1272
1273 @Override
1274 public boolean onTouch(View v, MotionEvent event) {
1275 if(!mInputEnabled) {
1276 return true;
1277 }
1278
1279 final float eventX = event.getX();
1280 final float eventY = event.getY();
1281
1282 int degrees;
1283 int snapDegrees;
1284 boolean result = false;
1285
1286 switch(event.getAction()) {
1287 case MotionEvent.ACTION_DOWN:
1288 case MotionEvent.ACTION_MOVE:
1289 mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY);
1290 if (mAmOrPmPressed != -1) {
1291 result = true;
1292 } else {
1293 degrees = getDegreesFromXY(eventX, eventY);
1294 if (degrees != -1) {
1295 snapDegrees = (mShowHours ?
1296 snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360;
1297 if (mShowHours) {
1298 mSelectionDegrees[HOURS] = snapDegrees;
1299 mSelectionDegrees[HOURS_INNER] = snapDegrees;
1300 } else {
1301 mSelectionDegrees[MINUTES] = snapDegrees;
1302 }
1303 performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
1304 if (mListener != null) {
1305 if (mShowHours) {
1306 mListener.onValueSelected(HOURS, getCurrentHour(), false);
1307 } else {
1308 mListener.onValueSelected(MINUTES, getCurrentMinute(), false);
1309 }
1310 }
1311 result = true;
1312 }
1313 }
1314 invalidate();
1315 return result;
1316
1317 case MotionEvent.ACTION_UP:
1318 mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY);
1319 if (mAmOrPmPressed != -1) {
1320 if (mAmOrPm != mAmOrPmPressed) {
1321 swapAmPm();
1322 }
1323 mAmOrPmPressed = -1;
1324 if (mListener != null) {
1325 mListener.onValueSelected(AMPM, getCurrentHour(), true);
1326 }
1327 result = true;
1328 } else {
1329 degrees = getDegreesFromXY(eventX, eventY);
1330 if (degrees != -1) {
1331 snapDegrees = (mShowHours ?
1332 snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360;
1333 if (mShowHours) {
1334 mSelectionDegrees[HOURS] = snapDegrees;
1335 mSelectionDegrees[HOURS_INNER] = snapDegrees;
1336 } else {
1337 mSelectionDegrees[MINUTES] = snapDegrees;
1338 }
1339 if (mListener != null) {
1340 if (mShowHours) {
1341 mListener.onValueSelected(HOURS, getCurrentHour(), true);
1342 } else {
1343 mListener.onValueSelected(MINUTES, getCurrentMinute(), true);
1344 }
1345 }
1346 result = true;
1347 }
1348 }
1349 if (result) {
1350 invalidate();
1351 }
1352 return result;
1353
1354 default:
1355 break;
1356 }
1357 return false;
1358 }
1359
1360 /**
1361 * Necessary for accessibility, to ensure we support "scrolling" forward and backward
1362 * in the circle.
1363 */
1364 @Override
1365 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1366 super.onInitializeAccessibilityNodeInfo(info);
1367 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
1368 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
1369 }
1370
1371 /**
1372 * Announce the currently-selected time when launched.
1373 */
1374 @Override
1375 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1376 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
1377 // Clear the event's current text so that only the current time will be spoken.
1378 event.getText().clear();
1379 Time time = new Time();
1380 time.hour = getCurrentHour();
1381 time.minute = getCurrentMinute();
1382 long millis = time.normalize(true);
1383 int flags = DateUtils.FORMAT_SHOW_TIME;
1384 if (mIs24HourMode) {
1385 flags |= DateUtils.FORMAT_24HOUR;
1386 }
1387 String timeString = DateUtils.formatDateTime(getContext(), millis, flags);
1388 event.getText().add(timeString);
1389 return true;
1390 }
1391 return super.dispatchPopulateAccessibilityEvent(event);
1392 }
1393
1394 /**
1395 * When scroll forward/backward events are received, jump the time to the higher/lower
1396 * discrete, visible value on the circle.
1397 */
1398 @SuppressLint("NewApi")
1399 @Override
1400 public boolean performAccessibilityAction(int action, Bundle arguments) {
1401 if (super.performAccessibilityAction(action, arguments)) {
1402 return true;
1403 }
1404
1405 int changeMultiplier = 0;
1406 if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
1407 changeMultiplier = 1;
1408 } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
1409 changeMultiplier = -1;
1410 }
1411 if (changeMultiplier != 0) {
1412 int value = 0;
1413 int stepSize = 0;
1414 if (mShowHours) {
1415 stepSize = DEGREES_FOR_ONE_HOUR;
1416 value = getCurrentHour() % 12;
1417 } else {
1418 stepSize = DEGREES_FOR_ONE_MINUTE;
1419 value = getCurrentMinute();
1420 }
1421
1422 int degrees = value * stepSize;
1423 degrees = snapOnly30s(degrees, changeMultiplier);
1424 value = degrees / stepSize;
1425 int maxValue = 0;
1426 int minValue = 0;
1427 if (mShowHours) {
1428 if (mIs24HourMode) {
1429 maxValue = 23;
1430 } else {
1431 maxValue = 12;
1432 minValue = 1;
1433 }
1434 } else {
1435 maxValue = 55;
1436 }
1437 if (value > maxValue) {
1438 // If we scrolled forward past the highest number, wrap around to the lowest.
1439 value = minValue;
1440 } else if (value < minValue) {
1441 // If we scrolled backward past the lowest number, wrap around to the highest.
1442 value = maxValue;
1443 }
1444 if (mShowHours) {
1445 setCurrentHour(value);
1446 if (mListener != null) {
1447 mListener.onValueSelected(HOURS, value, false);
1448 }
1449 } else {
1450 setCurrentMinute(value);
1451 if (mListener != null) {
1452 mListener.onValueSelected(MINUTES, value, false);
1453 }
1454 }
1455 return true;
1456 }
1457
1458 return false;
1459 }
1460
1461 public void setInputEnabled(boolean inputEnabled) {
1462 mInputEnabled = inputEnabled;
1463 invalidate();
1464 }
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001465
1466 private static class IntHolder {
1467 private int mValue;
1468
1469 public IntHolder(int value) {
1470 mValue = value;
1471 }
1472
1473 public void setValue(int value) {
1474 mValue = value;
1475 }
1476
1477 public int getValue() {
1478 return mValue;
1479 }
1480 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001481}