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