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