Home | History | Annotate | Download | only in widget
      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 
     17 package android.widget;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorSet;
     21 import android.animation.Keyframe;
     22 import android.animation.ObjectAnimator;
     23 import android.animation.PropertyValuesHolder;
     24 import android.animation.ValueAnimator;
     25 import android.content.Context;
     26 import android.content.res.ColorStateList;
     27 import android.content.res.Resources;
     28 import android.content.res.TypedArray;
     29 import android.graphics.Canvas;
     30 import android.graphics.Color;
     31 import android.graphics.Paint;
     32 import android.graphics.Path;
     33 import android.graphics.Rect;
     34 import android.graphics.Region;
     35 import android.graphics.Typeface;
     36 import android.os.Bundle;
     37 import android.util.AttributeSet;
     38 import android.util.IntArray;
     39 import android.util.Log;
     40 import android.util.MathUtils;
     41 import android.util.StateSet;
     42 import android.util.TypedValue;
     43 import android.view.HapticFeedbackConstants;
     44 import android.view.MotionEvent;
     45 import android.view.View;
     46 import android.view.accessibility.AccessibilityEvent;
     47 import android.view.accessibility.AccessibilityNodeInfo;
     48 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
     49 
     50 import com.android.internal.R;
     51 import com.android.internal.widget.ExploreByTouchHelper;
     52 
     53 import java.util.ArrayList;
     54 import java.util.Calendar;
     55 import java.util.Locale;
     56 
     57 /**
     58  * View to show a clock circle picker (with one or two picking circles)
     59  *
     60  * @hide
     61  */
     62 public class RadialTimePickerView extends View {
     63     private static final String TAG = "RadialTimePickerView";
     64 
     65     private static final int HOURS = 0;
     66     private static final int MINUTES = 1;
     67     private static final int HOURS_INNER = 2;
     68 
     69     private static final int SELECTOR_CIRCLE = 0;
     70     private static final int SELECTOR_DOT = 1;
     71     private static final int SELECTOR_LINE = 2;
     72 
     73     private static final int AM = 0;
     74     private static final int PM = 1;
     75 
     76     // Opaque alpha level
     77     private static final int ALPHA_OPAQUE = 255;
     78 
     79     // Transparent alpha level
     80     private static final int ALPHA_TRANSPARENT = 0;
     81 
     82     private static final int HOURS_IN_CIRCLE = 12;
     83     private static final int MINUTES_IN_CIRCLE = 60;
     84     private static final int DEGREES_FOR_ONE_HOUR = 360 / HOURS_IN_CIRCLE;
     85     private static final int DEGREES_FOR_ONE_MINUTE = 360 / MINUTES_IN_CIRCLE;
     86 
     87     private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
     88     private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
     89     private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
     90 
     91     private static final int FADE_OUT_DURATION = 500;
     92     private static final int FADE_IN_DURATION = 500;
     93 
     94     private static final int[] SNAP_PREFER_30S_MAP = new int[361];
     95 
     96     private static final int NUM_POSITIONS = 12;
     97     private static final float[] COS_30 = new float[NUM_POSITIONS];
     98     private static final float[] SIN_30 = new float[NUM_POSITIONS];
     99 
    100     static {
    101         // Prepare mapping to snap touchable degrees to selectable degrees.
    102         preparePrefer30sMap();
    103 
    104         final double increment = 2.0 * Math.PI / NUM_POSITIONS;
    105         double angle = Math.PI / 2.0;
    106         for (int i = 0; i < NUM_POSITIONS; i++) {
    107             COS_30[i] = (float) Math.cos(angle);
    108             SIN_30[i] = (float) Math.sin(angle);
    109             angle += increment;
    110         }
    111     }
    112 
    113     private final InvalidateUpdateListener mInvalidateUpdateListener =
    114             new InvalidateUpdateListener();
    115 
    116     private final String[] mHours12Texts = new String[12];
    117     private final String[] mOuterHours24Texts = new String[12];
    118     private final String[] mInnerHours24Texts = new String[12];
    119     private final String[] mMinutesTexts = new String[12];
    120 
    121     private final Paint[] mPaint = new Paint[2];
    122     private final IntHolder[] mAlpha = new IntHolder[2];
    123 
    124     private final Paint mPaintCenter = new Paint();
    125 
    126     private final Paint[][] mPaintSelector = new Paint[2][3];
    127 
    128     private final int mSelectorColor;
    129     private final int mSelectorDotColor;
    130 
    131     private final Paint mPaintBackground = new Paint();
    132 
    133     private final Typeface mTypeface;
    134 
    135     private final ColorStateList[] mTextColor = new ColorStateList[3];
    136     private final int[] mTextSize = new int[3];
    137     private final int[] mTextInset = new int[3];
    138 
    139     private final float[][] mOuterTextX = new float[2][12];
    140     private final float[][] mOuterTextY = new float[2][12];
    141 
    142     private final float[] mInnerTextX = new float[12];
    143     private final float[] mInnerTextY = new float[12];
    144 
    145     private final int[] mSelectionDegrees = new int[2];
    146 
    147     private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<>();
    148     private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<>();
    149 
    150     private final RadialPickerTouchHelper mTouchHelper;
    151 
    152     private final Path mSelectorPath = new Path();
    153 
    154     private boolean mIs24HourMode;
    155     private boolean mShowHours;
    156 
    157     /**
    158      * When in 24-hour mode, indicates that the current hour is between
    159      * 1 and 12 (inclusive).
    160      */
    161     private boolean mIsOnInnerCircle;
    162 
    163     private int mSelectorRadius;
    164     private int mSelectorStroke;
    165     private int mSelectorDotRadius;
    166     private int mCenterDotRadius;
    167 
    168     private int mXCenter;
    169     private int mYCenter;
    170     private int mCircleRadius;
    171 
    172     private int mMinDistForInnerNumber;
    173     private int mMaxDistForOuterNumber;
    174     private int mHalfwayDist;
    175 
    176     private String[] mOuterTextHours;
    177     private String[] mInnerTextHours;
    178     private String[] mMinutesText;
    179     private AnimatorSet mTransition;
    180 
    181     private int mAmOrPm;
    182 
    183     private float mDisabledAlpha;
    184 
    185     private OnValueSelectedListener mListener;
    186 
    187     private boolean mInputEnabled = true;
    188 
    189     public interface OnValueSelectedListener {
    190         void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
    191     }
    192 
    193     /**
    194      * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger
    195      * selectable area to each of the 12 visible values, such that the ratio of space apportioned
    196      * to a visible value : space apportioned to a non-visible value will be 14 : 4.
    197      * E.g. the output of 30 degrees should have a higher range of input associated with it than
    198      * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock
    199      * circle (5 on the minutes, 1 or 13 on the hours).
    200      */
    201     private static void preparePrefer30sMap() {
    202         // We'll split up the visible output and the non-visible output such that each visible
    203         // output will correspond to a range of 14 associated input degrees, and each non-visible
    204         // output will correspond to a range of 4 associate input degrees, so visible numbers
    205         // are more than 3 times easier to get than non-visible numbers:
    206         // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc.
    207         //
    208         // If an output of 30 degrees should correspond to a range of 14 associated degrees, then
    209         // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should
    210         // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you
    211         // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this
    212         // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the
    213         // ability to aggressively prefer the visible values by a factor of more than 3:1, which
    214         // greatly contributes to the selectability of these values.
    215 
    216         // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}.
    217         int snappedOutputDegrees = 0;
    218         // Count of how many inputs we've designated to the specified output.
    219         int count = 1;
    220         // How many input we expect for a specified output. This will be 14 for output divisible
    221         // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so
    222         // the caller can decide which they need.
    223         int expectedCount = 8;
    224         // Iterate through the input.
    225         for (int degrees = 0; degrees < 361; degrees++) {
    226             // Save the input-output mapping.
    227             SNAP_PREFER_30S_MAP[degrees] = snappedOutputDegrees;
    228             // If this is the last input for the specified output, calculate the next output and
    229             // the next expected count.
    230             if (count == expectedCount) {
    231                 snappedOutputDegrees += 6;
    232                 if (snappedOutputDegrees == 360) {
    233                     expectedCount = 7;
    234                 } else if (snappedOutputDegrees % 30 == 0) {
    235                     expectedCount = 14;
    236                 } else {
    237                     expectedCount = 4;
    238                 }
    239                 count = 1;
    240             } else {
    241                 count++;
    242             }
    243         }
    244     }
    245 
    246     /**
    247      * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees,
    248      * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be
    249      * weighted heavier than the degrees corresponding to non-visible numbers.
    250      * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the
    251      * mapping.
    252      */
    253     private static int snapPrefer30s(int degrees) {
    254         if (SNAP_PREFER_30S_MAP == null) {
    255             return -1;
    256         }
    257         return SNAP_PREFER_30S_MAP[degrees];
    258     }
    259 
    260     /**
    261      * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all
    262      * multiples of 30), where the input will be "snapped" to the closest visible degrees.
    263      * @param degrees The input degrees
    264      * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may
    265      * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force
    266      * strictly lower, and 0 to snap to the closer one.
    267      * @return output degrees, will be a multiple of 30
    268      */
    269     private static int snapOnly30s(int degrees, int forceHigherOrLower) {
    270         final int stepSize = DEGREES_FOR_ONE_HOUR;
    271         int floor = (degrees / stepSize) * stepSize;
    272         final int ceiling = floor + stepSize;
    273         if (forceHigherOrLower == 1) {
    274             degrees = ceiling;
    275         } else if (forceHigherOrLower == -1) {
    276             if (degrees == floor) {
    277                 floor -= stepSize;
    278             }
    279             degrees = floor;
    280         } else {
    281             if ((degrees - floor) < (ceiling - degrees)) {
    282                 degrees = floor;
    283             } else {
    284                 degrees = ceiling;
    285             }
    286         }
    287         return degrees;
    288     }
    289 
    290     @SuppressWarnings("unused")
    291     public RadialTimePickerView(Context context)  {
    292         this(context, null);
    293     }
    294 
    295     public RadialTimePickerView(Context context, AttributeSet attrs)  {
    296         this(context, attrs, R.attr.timePickerStyle);
    297     }
    298 
    299     public RadialTimePickerView(Context context, AttributeSet attrs, int defStyleAttr)  {
    300         this(context, attrs, defStyleAttr, 0);
    301     }
    302 
    303     public RadialTimePickerView(
    304             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)  {
    305         super(context, attrs);
    306 
    307         // Pull disabled alpha from theme.
    308         final TypedValue outValue = new TypedValue();
    309         context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true);
    310         mDisabledAlpha = outValue.getFloat();
    311 
    312         // process style attributes
    313         final Resources res = getResources();
    314         final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker,
    315                 defStyleAttr, defStyleRes);
    316 
    317         mTypeface = Typeface.create("sans-serif", Typeface.NORMAL);
    318 
    319         // Initialize all alpha values to opaque.
    320         for (int i = 0; i < mAlpha.length; i++) {
    321             mAlpha[i] = new IntHolder(ALPHA_OPAQUE);
    322         }
    323 
    324         mTextColor[HOURS] = a.getColorStateList(R.styleable.TimePicker_numbersTextColor);
    325         mTextColor[HOURS_INNER] = a.getColorStateList(R.styleable.TimePicker_numbersInnerTextColor);
    326         mTextColor[MINUTES] = mTextColor[HOURS];
    327 
    328         mPaint[HOURS] = new Paint();
    329         mPaint[HOURS].setAntiAlias(true);
    330         mPaint[HOURS].setTextAlign(Paint.Align.CENTER);
    331 
    332         mPaint[MINUTES] = new Paint();
    333         mPaint[MINUTES].setAntiAlias(true);
    334         mPaint[MINUTES].setTextAlign(Paint.Align.CENTER);
    335 
    336         final ColorStateList selectorColors = a.getColorStateList(
    337                 R.styleable.TimePicker_numbersSelectorColor);
    338         final int selectorActivatedColor = selectorColors.getColorForState(
    339                 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
    340 
    341         mPaintCenter.setColor(selectorActivatedColor);
    342         mPaintCenter.setAntiAlias(true);
    343 
    344         final int[] activatedStateSet = StateSet.get(
    345                 StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED);
    346 
    347         mSelectorColor = selectorActivatedColor;
    348         mSelectorDotColor = mTextColor[HOURS].getColorForState(activatedStateSet, 0);
    349 
    350         mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint();
    351         mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true);
    352 
    353         mPaintSelector[HOURS][SELECTOR_DOT] = new Paint();
    354         mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true);
    355 
    356         mPaintSelector[HOURS][SELECTOR_LINE] = new Paint();
    357         mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true);
    358         mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2);
    359 
    360         mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint();
    361         mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true);
    362 
    363         mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint();
    364         mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true);
    365 
    366         mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint();
    367         mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true);
    368         mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2);
    369 
    370         mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor,
    371                 context.getColor(R.color.timepicker_default_numbers_background_color_material)));
    372         mPaintBackground.setAntiAlias(true);
    373 
    374         mSelectorRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_radius);
    375         mSelectorStroke = res.getDimensionPixelSize(R.dimen.timepicker_selector_stroke);
    376         mSelectorDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_dot_radius);
    377         mCenterDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_center_dot_radius);
    378 
    379         mTextSize[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal);
    380         mTextSize[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal);
    381         mTextSize[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_inner);
    382 
    383         mTextInset[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal);
    384         mTextInset[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal);
    385         mTextInset[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_inner);
    386 
    387         mShowHours = true;
    388         mIs24HourMode = false;
    389         mAmOrPm = AM;
    390 
    391         // Set up accessibility components.
    392         mTouchHelper = new RadialPickerTouchHelper();
    393         setAccessibilityDelegate(mTouchHelper);
    394 
    395         if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
    396             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    397         }
    398 
    399         initHoursAndMinutesText();
    400         initData();
    401 
    402         a.recycle();
    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         setCurrentHourInternal(currentHour, false, false);
    410         setCurrentMinuteInternal(currentMinute, false);
    411 
    412         setHapticFeedbackEnabled(true);
    413     }
    414 
    415     public void initialize(int hour, int minute, boolean is24HourMode) {
    416         if (mIs24HourMode != is24HourMode) {
    417             mIs24HourMode = is24HourMode;
    418             initData();
    419         }
    420 
    421         setCurrentHourInternal(hour, false, false);
    422         setCurrentMinuteInternal(minute, false);
    423     }
    424 
    425     public void setCurrentItemShowing(int item, boolean animate) {
    426         switch (item){
    427             case HOURS:
    428                 showHours(animate);
    429                 break;
    430             case MINUTES:
    431                 showMinutes(animate);
    432                 break;
    433             default:
    434                 Log.e(TAG, "ClockView does not support showing item " + item);
    435         }
    436     }
    437 
    438     public int getCurrentItemShowing() {
    439         return mShowHours ? HOURS : MINUTES;
    440     }
    441 
    442     public void setOnValueSelectedListener(OnValueSelectedListener listener) {
    443         mListener = listener;
    444     }
    445 
    446     /**
    447      * Sets the current hour in 24-hour time.
    448      *
    449      * @param hour the current hour between 0 and 23 (inclusive)
    450      */
    451     public void setCurrentHour(int hour) {
    452         setCurrentHourInternal(hour, true, false);
    453     }
    454 
    455     /**
    456      * Sets the current hour.
    457      *
    458      * @param hour The current hour
    459      * @param callback Whether the value listener should be invoked
    460      * @param autoAdvance Whether the listener should auto-advance to the next
    461      *                    selection mode, e.g. hour to minutes
    462      */
    463     private void setCurrentHourInternal(int hour, boolean callback, boolean autoAdvance) {
    464         final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR;
    465         mSelectionDegrees[HOURS] = degrees;
    466 
    467         // 0 is 12 AM (midnight) and 12 is 12 PM (noon).
    468         final int amOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM;
    469         final boolean isOnInnerCircle = getInnerCircleForHour(hour);
    470         if (mAmOrPm != amOrPm || mIsOnInnerCircle != isOnInnerCircle) {
    471             mAmOrPm = amOrPm;
    472             mIsOnInnerCircle = isOnInnerCircle;
    473 
    474             initData();
    475             mTouchHelper.invalidateRoot();
    476         }
    477 
    478         invalidate();
    479 
    480         if (callback && mListener != null) {
    481             mListener.onValueSelected(HOURS, hour, autoAdvance);
    482         }
    483     }
    484 
    485     /**
    486      * Returns the current hour in 24-hour time.
    487      *
    488      * @return the current hour between 0 and 23 (inclusive)
    489      */
    490     public int getCurrentHour() {
    491         return getHourForDegrees(mSelectionDegrees[HOURS], mIsOnInnerCircle);
    492     }
    493 
    494     private int getHourForDegrees(int degrees, boolean innerCircle) {
    495         int hour = (degrees / DEGREES_FOR_ONE_HOUR) % 12;
    496         if (mIs24HourMode) {
    497             // Convert the 12-hour value into 24-hour time based on where the
    498             // selector is positioned.
    499             if (!innerCircle && hour == 0) {
    500                 // Outer circle is 1 through 12.
    501                 hour = 12;
    502             } else if (innerCircle && hour != 0) {
    503                 // Inner circle is 13 through 23 and 0.
    504                 hour += 12;
    505             }
    506         } else if (mAmOrPm == PM) {
    507             hour += 12;
    508         }
    509         return hour;
    510     }
    511 
    512     /**
    513      * @param hour the hour in 24-hour time or 12-hour time
    514      */
    515     private int getDegreesForHour(int hour) {
    516         // Convert to be 0-11.
    517         if (mIs24HourMode) {
    518             if (hour >= 12) {
    519                 hour -= 12;
    520             }
    521         } else if (hour == 12) {
    522             hour = 0;
    523         }
    524         return hour * DEGREES_FOR_ONE_HOUR;
    525     }
    526 
    527     /**
    528      * @param hour the hour in 24-hour time or 12-hour time
    529      */
    530     private boolean getInnerCircleForHour(int hour) {
    531         return mIs24HourMode && (hour == 0 || hour > 12);
    532     }
    533 
    534     public void setCurrentMinute(int minute) {
    535         setCurrentMinuteInternal(minute, true);
    536     }
    537 
    538     private void setCurrentMinuteInternal(int minute, boolean callback) {
    539         mSelectionDegrees[MINUTES] = (minute % MINUTES_IN_CIRCLE) * DEGREES_FOR_ONE_MINUTE;
    540 
    541         invalidate();
    542 
    543         if (callback && mListener != null) {
    544             mListener.onValueSelected(MINUTES, minute, false);
    545         }
    546     }
    547 
    548     // Returns minutes in 0-59 range
    549     public int getCurrentMinute() {
    550         return getMinuteForDegrees(mSelectionDegrees[MINUTES]);
    551     }
    552 
    553     private int getMinuteForDegrees(int degrees) {
    554         return degrees / DEGREES_FOR_ONE_MINUTE;
    555     }
    556 
    557     private int getDegreesForMinute(int minute) {
    558         return minute * DEGREES_FOR_ONE_MINUTE;
    559     }
    560 
    561     public void setAmOrPm(int val) {
    562         mAmOrPm = (val % 2);
    563         invalidate();
    564         mTouchHelper.invalidateRoot();
    565     }
    566 
    567     public int getAmOrPm() {
    568         return mAmOrPm;
    569     }
    570 
    571     public void showHours(boolean animate) {
    572         if (mShowHours) {
    573             return;
    574         }
    575 
    576         mShowHours = true;
    577 
    578         if (animate) {
    579             startMinutesToHoursAnimation();
    580         }
    581 
    582         initData();
    583         invalidate();
    584         mTouchHelper.invalidateRoot();
    585     }
    586 
    587     public void showMinutes(boolean animate) {
    588         if (!mShowHours) {
    589             return;
    590         }
    591 
    592         mShowHours = false;
    593 
    594         if (animate) {
    595             startHoursToMinutesAnimation();
    596         }
    597 
    598         initData();
    599         invalidate();
    600         mTouchHelper.invalidateRoot();
    601     }
    602 
    603     private void initHoursAndMinutesText() {
    604         // Initialize the hours and minutes numbers.
    605         for (int i = 0; i < 12; i++) {
    606             mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
    607             mInnerHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]);
    608             mOuterHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
    609             mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]);
    610         }
    611     }
    612 
    613     private void initData() {
    614         if (mIs24HourMode) {
    615             mOuterTextHours = mOuterHours24Texts;
    616             mInnerTextHours = mInnerHours24Texts;
    617         } else {
    618             mOuterTextHours = mHours12Texts;
    619             mInnerTextHours = mHours12Texts;
    620         }
    621 
    622         mMinutesText = mMinutesTexts;
    623 
    624         final int hoursAlpha = mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT;
    625         mAlpha[HOURS].setValue(hoursAlpha);
    626 
    627         final int minutesAlpha = mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE;
    628         mAlpha[MINUTES].setValue(minutesAlpha);
    629     }
    630 
    631     @Override
    632     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    633         if (!changed) {
    634             return;
    635         }
    636 
    637         mXCenter = getWidth() / 2;
    638         mYCenter = getHeight() / 2;
    639         mCircleRadius = Math.min(mXCenter, mYCenter);
    640 
    641         mMinDistForInnerNumber = mCircleRadius - mTextInset[HOURS_INNER] - mSelectorRadius;
    642         mMaxDistForOuterNumber = mCircleRadius - mTextInset[HOURS] + mSelectorRadius;
    643         mHalfwayDist = mCircleRadius - (mTextInset[HOURS] + mTextInset[HOURS_INNER]) / 2;
    644 
    645         calculatePositionsHours();
    646         calculatePositionsMinutes();
    647 
    648         mTouchHelper.invalidateRoot();
    649     }
    650 
    651     @Override
    652     public void onDraw(Canvas canvas) {
    653         final float alphaMod = mInputEnabled ? 1 : mDisabledAlpha;
    654 
    655         drawCircleBackground(canvas);
    656         drawHours(canvas, alphaMod);
    657         drawMinutes(canvas, alphaMod);
    658         drawCenter(canvas, alphaMod);
    659     }
    660 
    661     private void drawCircleBackground(Canvas canvas) {
    662         canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaintBackground);
    663     }
    664 
    665     private void drawHours(Canvas canvas, float alphaMod) {
    666         final int hoursAlpha = (int) (mAlpha[HOURS].getValue() * alphaMod + 0.5f);
    667         if (hoursAlpha > 0) {
    668             // Draw the hour selector under the elements.
    669             drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS, null, alphaMod);
    670 
    671             // Draw outer hours.
    672             drawTextElements(canvas, mTextSize[HOURS], mTypeface, mTextColor[HOURS],
    673                     mOuterTextHours, mOuterTextX[HOURS], mOuterTextY[HOURS], mPaint[HOURS],
    674                     hoursAlpha, !mIsOnInnerCircle, mSelectionDegrees[HOURS], false);
    675 
    676             // Draw inner hours (13-00) for 24-hour time.
    677             if (mIs24HourMode && mInnerTextHours != null) {
    678                 drawTextElements(canvas, mTextSize[HOURS_INNER], mTypeface, mTextColor[HOURS_INNER],
    679                         mInnerTextHours, mInnerTextX, mInnerTextY, mPaint[HOURS], hoursAlpha,
    680                         mIsOnInnerCircle, mSelectionDegrees[HOURS], false);
    681             }
    682         }
    683     }
    684 
    685     private void drawMinutes(Canvas canvas, float alphaMod) {
    686         final int minutesAlpha = (int) (mAlpha[MINUTES].getValue() * alphaMod + 0.5f);
    687         if (minutesAlpha > 0) {
    688             // Draw the minute selector under the elements.
    689             drawSelector(canvas, MINUTES, mSelectorPath, alphaMod);
    690 
    691             // Exclude the selector region, then draw minutes with no
    692             // activated states.
    693             canvas.save(Canvas.CLIP_SAVE_FLAG);
    694             canvas.clipPath(mSelectorPath, Region.Op.DIFFERENCE);
    695             drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mTextColor[MINUTES],
    696                     mMinutesText, mOuterTextX[MINUTES], mOuterTextY[MINUTES], mPaint[MINUTES],
    697                     minutesAlpha, false, 0, false);
    698             canvas.restore();
    699 
    700             // Intersect the selector region, then draw minutes with only
    701             // activated states.
    702             canvas.save(Canvas.CLIP_SAVE_FLAG);
    703             canvas.clipPath(mSelectorPath, Region.Op.INTERSECT);
    704             drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mTextColor[MINUTES],
    705                     mMinutesText, mOuterTextX[MINUTES], mOuterTextY[MINUTES], mPaint[MINUTES],
    706                     minutesAlpha, true, mSelectionDegrees[MINUTES], true);
    707             canvas.restore();
    708         }
    709     }
    710 
    711     private void drawCenter(Canvas canvas, float alphaMod) {
    712         mPaintCenter.setAlpha((int) (255 * alphaMod + 0.5f));
    713         canvas.drawCircle(mXCenter, mYCenter, mCenterDotRadius, mPaintCenter);
    714     }
    715 
    716     private int applyAlpha(int argb, int alpha) {
    717         final int srcAlpha = (argb >> 24) & 0xFF;
    718         final int dstAlpha = (int) (srcAlpha * (alpha / 255.0) + 0.5f);
    719         return (0xFFFFFF & argb) | (dstAlpha << 24);
    720     }
    721 
    722     private int getMultipliedAlpha(int argb, int alpha) {
    723         return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5);
    724     }
    725 
    726     private void drawSelector(Canvas canvas, int index, Path selectorPath, float alphaMod) {
    727         final int alpha = (int) (mAlpha[index % 2].getValue() * alphaMod + 0.5f);
    728         final int color = applyAlpha(mSelectorColor, alpha);
    729 
    730         // Calculate the current radius at which to place the selection circle.
    731         final int selRadius = mSelectorRadius;
    732         final int selLength = mCircleRadius - mTextInset[index];
    733         final double selAngleRad = Math.toRadians(mSelectionDegrees[index % 2]);
    734         final float selCenterX = mXCenter + selLength * (float) Math.sin(selAngleRad);
    735         final float selCenterY = mYCenter - selLength * (float) Math.cos(selAngleRad);
    736 
    737         // Draw the selection circle.
    738         final Paint paint = mPaintSelector[index % 2][SELECTOR_CIRCLE];
    739         paint.setColor(color);
    740         canvas.drawCircle(selCenterX, selCenterY, selRadius, paint);
    741 
    742         // If needed, set up the clip path for later.
    743         if (selectorPath != null) {
    744             selectorPath.reset();
    745             selectorPath.addCircle(selCenterX, selCenterY, selRadius, Path.Direction.CCW);
    746         }
    747 
    748         // Draw the dot if we're between two items.
    749         final boolean shouldDrawDot = mSelectionDegrees[index % 2] % 30 != 0;
    750         if (shouldDrawDot) {
    751             final Paint dotPaint = mPaintSelector[index % 2][SELECTOR_DOT];
    752             dotPaint.setColor(mSelectorDotColor);
    753             canvas.drawCircle(selCenterX, selCenterY, mSelectorDotRadius, dotPaint);
    754         }
    755 
    756         // Shorten the line to only go from the edge of the center dot to the
    757         // edge of the selection circle.
    758         final double sin = Math.sin(selAngleRad);
    759         final double cos = Math.cos(selAngleRad);
    760         final int lineLength = selLength - selRadius;
    761         final int centerX = mXCenter + (int) (mCenterDotRadius * sin);
    762         final int centerY = mYCenter - (int) (mCenterDotRadius * cos);
    763         final float linePointX = centerX + (int) (lineLength * sin);
    764         final float linePointY = centerY - (int) (lineLength * cos);
    765 
    766         // Draw the line.
    767         final Paint linePaint = mPaintSelector[index % 2][SELECTOR_LINE];
    768         linePaint.setColor(color);
    769         linePaint.setStrokeWidth(mSelectorStroke);
    770         canvas.drawLine(mXCenter, mYCenter, linePointX, linePointY, linePaint);
    771     }
    772 
    773     private void calculatePositionsHours() {
    774         // Calculate the text positions
    775         final float numbersRadius = mCircleRadius - mTextInset[HOURS];
    776 
    777         // Calculate the positions for the 12 numbers in the main circle.
    778         calculatePositions(mPaint[HOURS], numbersRadius, mXCenter, mYCenter,
    779                 mTextSize[HOURS], mOuterTextX[HOURS], mOuterTextY[HOURS]);
    780 
    781         // If we have an inner circle, calculate those positions too.
    782         if (mIs24HourMode) {
    783             final int innerNumbersRadius = mCircleRadius - mTextInset[HOURS_INNER];
    784             calculatePositions(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter,
    785                     mTextSize[HOURS_INNER], mInnerTextX, mInnerTextY);
    786         }
    787     }
    788 
    789     private void calculatePositionsMinutes() {
    790         // Calculate the text positions
    791         final float numbersRadius = mCircleRadius - mTextInset[MINUTES];
    792 
    793         // Calculate the positions for the 12 numbers in the main circle.
    794         calculatePositions(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter,
    795                 mTextSize[MINUTES], mOuterTextX[MINUTES], mOuterTextY[MINUTES]);
    796     }
    797 
    798     /**
    799      * Using the trigonometric Unit Circle, calculate the positions that the text will need to be
    800      * drawn at based on the specified circle radius. Place the values in the textGridHeights and
    801      * textGridWidths parameters.
    802      */
    803     private static void calculatePositions(Paint paint, float radius, float xCenter, float yCenter,
    804             float textSize, float[] x, float[] y) {
    805         // Adjust yCenter to account for the text's baseline.
    806         paint.setTextSize(textSize);
    807         yCenter -= (paint.descent() + paint.ascent()) / 2;
    808 
    809         for (int i = 0; i < NUM_POSITIONS; i++) {
    810             x[i] = xCenter - radius * COS_30[i];
    811             y[i] = yCenter - radius * SIN_30[i];
    812         }
    813     }
    814 
    815     /**
    816      * Draw the 12 text values at the positions specified by the textGrid parameters.
    817      */
    818     private void drawTextElements(Canvas canvas, float textSize, Typeface typeface,
    819             ColorStateList textColor, String[] texts, float[] textX, float[] textY, Paint paint,
    820             int alpha, boolean showActivated, int activatedDegrees, boolean activatedOnly) {
    821         paint.setTextSize(textSize);
    822         paint.setTypeface(typeface);
    823 
    824         // The activated index can touch a range of elements.
    825         final float activatedIndex = activatedDegrees / (360.0f / NUM_POSITIONS);
    826         final int activatedFloor = (int) activatedIndex;
    827         final int activatedCeil = ((int) Math.ceil(activatedIndex)) % NUM_POSITIONS;
    828 
    829         for (int i = 0; i < 12; i++) {
    830             final boolean activated = (activatedFloor == i || activatedCeil == i);
    831             if (activatedOnly && !activated) {
    832                 continue;
    833             }
    834 
    835             final int stateMask = StateSet.VIEW_STATE_ENABLED
    836                     | (showActivated && activated ? StateSet.VIEW_STATE_ACTIVATED : 0);
    837             final int color = textColor.getColorForState(StateSet.get(stateMask), 0);
    838             paint.setColor(color);
    839             paint.setAlpha(getMultipliedAlpha(color, alpha));
    840 
    841             canvas.drawText(texts[i], textX[i], textY[i], paint);
    842         }
    843     }
    844 
    845     private static ObjectAnimator getFadeOutAnimator(IntHolder target, int startAlpha, int endAlpha,
    846                 InvalidateUpdateListener updateListener) {
    847         final ObjectAnimator animator = ObjectAnimator.ofInt(target, "value", startAlpha, endAlpha);
    848         animator.setDuration(FADE_OUT_DURATION);
    849         animator.addUpdateListener(updateListener);
    850         return animator;
    851     }
    852 
    853     private static ObjectAnimator getFadeInAnimator(IntHolder target, int startAlpha, int endAlpha,
    854                 InvalidateUpdateListener updateListener) {
    855         final float delayMultiplier = 0.25f;
    856         final float transitionDurationMultiplier = 1f;
    857         final float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
    858         final int totalDuration = (int) (FADE_IN_DURATION * totalDurationMultiplier);
    859         final float delayPoint = (delayMultiplier * FADE_IN_DURATION) / totalDuration;
    860 
    861         final Keyframe kf0, kf1, kf2;
    862         kf0 = Keyframe.ofInt(0f, startAlpha);
    863         kf1 = Keyframe.ofInt(delayPoint, startAlpha);
    864         kf2 = Keyframe.ofInt(1f, endAlpha);
    865         final PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("value", kf0, kf1, kf2);
    866 
    867         final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(target, fadeIn);
    868         animator.setDuration(totalDuration);
    869         animator.addUpdateListener(updateListener);
    870         return animator;
    871     }
    872 
    873     private class InvalidateUpdateListener implements ValueAnimator.AnimatorUpdateListener {
    874         @Override
    875         public void onAnimationUpdate(ValueAnimator animation) {
    876             RadialTimePickerView.this.invalidate();
    877         }
    878     }
    879 
    880     private void startHoursToMinutesAnimation() {
    881         if (mHoursToMinutesAnims.size() == 0) {
    882             mHoursToMinutesAnims.add(getFadeOutAnimator(mAlpha[HOURS],
    883                     ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
    884             mHoursToMinutesAnims.add(getFadeInAnimator(mAlpha[MINUTES],
    885                     ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
    886         }
    887 
    888         if (mTransition != null && mTransition.isRunning()) {
    889             mTransition.end();
    890         }
    891         mTransition = new AnimatorSet();
    892         mTransition.playTogether(mHoursToMinutesAnims);
    893         mTransition.start();
    894     }
    895 
    896     private void startMinutesToHoursAnimation() {
    897         if (mMinuteToHoursAnims.size() == 0) {
    898             mMinuteToHoursAnims.add(getFadeOutAnimator(mAlpha[MINUTES],
    899                     ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
    900             mMinuteToHoursAnims.add(getFadeInAnimator(mAlpha[HOURS],
    901                     ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
    902         }
    903 
    904         if (mTransition != null && mTransition.isRunning()) {
    905             mTransition.end();
    906         }
    907         mTransition = new AnimatorSet();
    908         mTransition.playTogether(mMinuteToHoursAnims);
    909         mTransition.start();
    910     }
    911 
    912     private int getDegreesFromXY(float x, float y, boolean constrainOutside) {
    913         // Ensure the point is inside the touchable area.
    914         final int innerBound;
    915         final int outerBound;
    916         if (mIs24HourMode && mShowHours) {
    917             innerBound = mMinDistForInnerNumber;
    918             outerBound = mMaxDistForOuterNumber;
    919         } else {
    920             final int index = mShowHours ? HOURS : MINUTES;
    921             final int center = mCircleRadius - mTextInset[index];
    922             innerBound = center - mSelectorRadius;
    923             outerBound = center + mSelectorRadius;
    924         }
    925 
    926         final double dX = x - mXCenter;
    927         final double dY = y - mYCenter;
    928         final double distFromCenter = Math.sqrt(dX * dX + dY * dY);
    929         if (distFromCenter < innerBound || constrainOutside && distFromCenter > outerBound) {
    930             return -1;
    931         }
    932 
    933         // Convert to degrees.
    934         final int degrees = (int) (Math.toDegrees(Math.atan2(dY, dX) + Math.PI / 2) + 0.5);
    935         if (degrees < 0) {
    936             return degrees + 360;
    937         } else {
    938             return degrees;
    939         }
    940     }
    941 
    942     private boolean getInnerCircleFromXY(float x, float y) {
    943         if (mIs24HourMode && mShowHours) {
    944             final double dX = x - mXCenter;
    945             final double dY = y - mYCenter;
    946             final double distFromCenter = Math.sqrt(dX * dX + dY * dY);
    947             return distFromCenter <= mHalfwayDist;
    948         }
    949         return false;
    950     }
    951 
    952     boolean mChangedDuringTouch = false;
    953 
    954     @Override
    955     public boolean onTouchEvent(MotionEvent event) {
    956         if (!mInputEnabled) {
    957             return true;
    958         }
    959 
    960         final int action = event.getActionMasked();
    961         if (action == MotionEvent.ACTION_MOVE
    962                 || action == MotionEvent.ACTION_UP
    963                 || action == MotionEvent.ACTION_DOWN) {
    964             boolean forceSelection = false;
    965             boolean autoAdvance = false;
    966 
    967             if (action == MotionEvent.ACTION_DOWN) {
    968                 // This is a new event stream, reset whether the value changed.
    969                 mChangedDuringTouch = false;
    970             } else if (action == MotionEvent.ACTION_UP) {
    971                 autoAdvance = true;
    972 
    973                 // If we saw a down/up pair without the value changing, assume
    974                 // this is a single-tap selection and force a change.
    975                 if (!mChangedDuringTouch) {
    976                     forceSelection = true;
    977                 }
    978             }
    979 
    980             mChangedDuringTouch |= handleTouchInput(
    981                     event.getX(), event.getY(), forceSelection, autoAdvance);
    982         }
    983 
    984         return true;
    985     }
    986 
    987     private boolean handleTouchInput(
    988             float x, float y, boolean forceSelection, boolean autoAdvance) {
    989         final boolean isOnInnerCircle = getInnerCircleFromXY(x, y);
    990         final int degrees = getDegreesFromXY(x, y, false);
    991         if (degrees == -1) {
    992             return false;
    993         }
    994 
    995         final int type;
    996         final int newValue;
    997         final boolean valueChanged;
    998 
    999         if (mShowHours) {
   1000             final int snapDegrees = snapOnly30s(degrees, 0) % 360;
   1001             valueChanged = mIsOnInnerCircle != isOnInnerCircle
   1002                     || mSelectionDegrees[HOURS] != snapDegrees;
   1003             mIsOnInnerCircle = isOnInnerCircle;
   1004             mSelectionDegrees[HOURS] = snapDegrees;
   1005             type = HOURS;
   1006             newValue = getCurrentHour();
   1007         } else {
   1008             final int snapDegrees = snapPrefer30s(degrees) % 360;
   1009             valueChanged = mSelectionDegrees[MINUTES] != snapDegrees;
   1010             mSelectionDegrees[MINUTES] = snapDegrees;
   1011             type = MINUTES;
   1012             newValue = getCurrentMinute();
   1013         }
   1014 
   1015         if (valueChanged || forceSelection || autoAdvance) {
   1016             // Fire the listener even if we just need to auto-advance.
   1017             if (mListener != null) {
   1018                 mListener.onValueSelected(type, newValue, autoAdvance);
   1019             }
   1020 
   1021             // Only provide feedback if the value actually changed.
   1022             if (valueChanged || forceSelection) {
   1023                 performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
   1024                 invalidate();
   1025             }
   1026             return true;
   1027         }
   1028 
   1029         return false;
   1030     }
   1031 
   1032     @Override
   1033     public boolean dispatchHoverEvent(MotionEvent event) {
   1034         // First right-of-refusal goes the touch exploration helper.
   1035         if (mTouchHelper.dispatchHoverEvent(event)) {
   1036             return true;
   1037         }
   1038         return super.dispatchHoverEvent(event);
   1039     }
   1040 
   1041     public void setInputEnabled(boolean inputEnabled) {
   1042         mInputEnabled = inputEnabled;
   1043         invalidate();
   1044     }
   1045 
   1046     private class RadialPickerTouchHelper extends ExploreByTouchHelper {
   1047         private final Rect mTempRect = new Rect();
   1048 
   1049         private final int TYPE_HOUR = 1;
   1050         private final int TYPE_MINUTE = 2;
   1051 
   1052         private final int SHIFT_TYPE = 0;
   1053         private final int MASK_TYPE = 0xF;
   1054 
   1055         private final int SHIFT_VALUE = 8;
   1056         private final int MASK_VALUE = 0xFF;
   1057 
   1058         /** Increment in which virtual views are exposed for minutes. */
   1059         private final int MINUTE_INCREMENT = 5;
   1060 
   1061         public RadialPickerTouchHelper() {
   1062             super(RadialTimePickerView.this);
   1063         }
   1064 
   1065         @Override
   1066         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
   1067             super.onInitializeAccessibilityNodeInfo(host, info);
   1068 
   1069             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
   1070             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
   1071         }
   1072 
   1073         @Override
   1074         public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
   1075             if (super.performAccessibilityAction(host, action, arguments)) {
   1076                 return true;
   1077             }
   1078 
   1079             switch (action) {
   1080                 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
   1081                     adjustPicker(1);
   1082                     return true;
   1083                 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
   1084                     adjustPicker(-1);
   1085                     return true;
   1086             }
   1087 
   1088             return false;
   1089         }
   1090 
   1091         private void adjustPicker(int step) {
   1092             final int stepSize;
   1093             final int initialStep;
   1094             final int maxValue;
   1095             final int minValue;
   1096             if (mShowHours) {
   1097                 stepSize = 1;
   1098 
   1099                 final int currentHour24 = getCurrentHour();
   1100                 if (mIs24HourMode) {
   1101                     initialStep = currentHour24;
   1102                     minValue = 0;
   1103                     maxValue = 23;
   1104                 } else {
   1105                     initialStep = hour24To12(currentHour24);
   1106                     minValue = 1;
   1107                     maxValue = 12;
   1108                 }
   1109             } else {
   1110                 stepSize = 5;
   1111                 initialStep = getCurrentMinute() / stepSize;
   1112                 minValue = 0;
   1113                 maxValue = 55;
   1114             }
   1115 
   1116             final int nextValue = (initialStep + step) * stepSize;
   1117             final int clampedValue = MathUtils.constrain(nextValue, minValue, maxValue);
   1118             if (mShowHours) {
   1119                 setCurrentHour(clampedValue);
   1120             } else {
   1121                 setCurrentMinute(clampedValue);
   1122             }
   1123         }
   1124 
   1125         @Override
   1126         protected int getVirtualViewAt(float x, float y) {
   1127             final int id;
   1128             final int degrees = getDegreesFromXY(x, y, true);
   1129             if (degrees != -1) {
   1130                 final int snapDegrees = snapOnly30s(degrees, 0) % 360;
   1131                 if (mShowHours) {
   1132                     final boolean isOnInnerCircle = getInnerCircleFromXY(x, y);
   1133                     final int hour24 = getHourForDegrees(snapDegrees, isOnInnerCircle);
   1134                     final int hour = mIs24HourMode ? hour24 : hour24To12(hour24);
   1135                     id = makeId(TYPE_HOUR, hour);
   1136                 } else {
   1137                     final int current = getCurrentMinute();
   1138                     final int touched = getMinuteForDegrees(degrees);
   1139                     final int snapped = getMinuteForDegrees(snapDegrees);
   1140 
   1141                     // If the touched minute is closer to the current minute
   1142                     // than it is to the snapped minute, return current.
   1143                     final int currentOffset = getCircularDiff(current, touched, MINUTES_IN_CIRCLE);
   1144                     final int snappedOffset = getCircularDiff(snapped, touched, MINUTES_IN_CIRCLE);
   1145                     final int minute;
   1146                     if (currentOffset < snappedOffset) {
   1147                         minute = current;
   1148                     } else {
   1149                         minute = snapped;
   1150                     }
   1151                     id = makeId(TYPE_MINUTE, minute);
   1152                 }
   1153             } else {
   1154                 id = INVALID_ID;
   1155             }
   1156 
   1157             return id;
   1158         }
   1159 
   1160         /**
   1161          * Returns the difference in degrees between two values along a circle.
   1162          *
   1163          * @param first value in the range [0,max]
   1164          * @param second value in the range [0,max]
   1165          * @param max the maximum value along the circle
   1166          * @return the difference in between the two values
   1167          */
   1168         private int getCircularDiff(int first, int second, int max) {
   1169             final int diff = Math.abs(first - second);
   1170             final int midpoint = max / 2;
   1171             return (diff > midpoint) ? (max - diff) : diff;
   1172         }
   1173 
   1174         @Override
   1175         protected void getVisibleVirtualViews(IntArray virtualViewIds) {
   1176             if (mShowHours) {
   1177                 final int min = mIs24HourMode ? 0 : 1;
   1178                 final int max = mIs24HourMode ? 23 : 12;
   1179                 for (int i = min; i <= max ; i++) {
   1180                     virtualViewIds.add(makeId(TYPE_HOUR, i));
   1181                 }
   1182             } else {
   1183                 final int current = getCurrentMinute();
   1184                 for (int i = 0; i < MINUTES_IN_CIRCLE; i += MINUTE_INCREMENT) {
   1185                     virtualViewIds.add(makeId(TYPE_MINUTE, i));
   1186 
   1187                     // If the current minute falls between two increments,
   1188                     // insert an extra node for it.
   1189                     if (current > i && current < i + MINUTE_INCREMENT) {
   1190                         virtualViewIds.add(makeId(TYPE_MINUTE, current));
   1191                     }
   1192                 }
   1193             }
   1194         }
   1195 
   1196         @Override
   1197         protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
   1198             event.setClassName(getClass().getName());
   1199 
   1200             final int type = getTypeFromId(virtualViewId);
   1201             final int value = getValueFromId(virtualViewId);
   1202             final CharSequence description = getVirtualViewDescription(type, value);
   1203             event.setContentDescription(description);
   1204         }
   1205 
   1206         @Override
   1207         protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
   1208             node.setClassName(getClass().getName());
   1209             node.addAction(AccessibilityAction.ACTION_CLICK);
   1210 
   1211             final int type = getTypeFromId(virtualViewId);
   1212             final int value = getValueFromId(virtualViewId);
   1213             final CharSequence description = getVirtualViewDescription(type, value);
   1214             node.setContentDescription(description);
   1215 
   1216             getBoundsForVirtualView(virtualViewId, mTempRect);
   1217             node.setBoundsInParent(mTempRect);
   1218 
   1219             final boolean selected = isVirtualViewSelected(type, value);
   1220             node.setSelected(selected);
   1221 
   1222             final int nextId = getVirtualViewIdAfter(type, value);
   1223             if (nextId != INVALID_ID) {
   1224                 node.setTraversalBefore(RadialTimePickerView.this, nextId);
   1225             }
   1226         }
   1227 
   1228         private int getVirtualViewIdAfter(int type, int value) {
   1229             if (type == TYPE_HOUR) {
   1230                 final int nextValue = value + 1;
   1231                 final int max = mIs24HourMode ? 23 : 12;
   1232                 if (nextValue <= max) {
   1233                     return makeId(type, nextValue);
   1234                 }
   1235             } else if (type == TYPE_MINUTE) {
   1236                 final int current = getCurrentMinute();
   1237                 final int snapValue = value - (value % MINUTE_INCREMENT);
   1238                 final int nextValue = snapValue + MINUTE_INCREMENT;
   1239                 if (value < current && nextValue > current) {
   1240                     // The current value is between two snap values.
   1241                     return makeId(type, current);
   1242                 } else if (nextValue < MINUTES_IN_CIRCLE) {
   1243                     return makeId(type, nextValue);
   1244                 }
   1245             }
   1246             return INVALID_ID;
   1247         }
   1248 
   1249         @Override
   1250         protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
   1251                 Bundle arguments) {
   1252             if (action == AccessibilityNodeInfo.ACTION_CLICK) {
   1253                 final int type = getTypeFromId(virtualViewId);
   1254                 final int value = getValueFromId(virtualViewId);
   1255                 if (type == TYPE_HOUR) {
   1256                     final int hour = mIs24HourMode ? value : hour12To24(value, mAmOrPm);
   1257                     setCurrentHour(hour);
   1258                     return true;
   1259                 } else if (type == TYPE_MINUTE) {
   1260                     setCurrentMinute(value);
   1261                     return true;
   1262                 }
   1263             }
   1264             return false;
   1265         }
   1266 
   1267         private int hour12To24(int hour12, int amOrPm) {
   1268             int hour24 = hour12;
   1269             if (hour12 == 12) {
   1270                 if (amOrPm == AM) {
   1271                     hour24 = 0;
   1272                 }
   1273             } else if (amOrPm == PM) {
   1274                 hour24 += 12;
   1275             }
   1276             return hour24;
   1277         }
   1278 
   1279         private int hour24To12(int hour24) {
   1280             if (hour24 == 0) {
   1281                 return 12;
   1282             } else if (hour24 > 12) {
   1283                 return hour24 - 12;
   1284             } else {
   1285                 return hour24;
   1286             }
   1287         }
   1288 
   1289         private void getBoundsForVirtualView(int virtualViewId, Rect bounds) {
   1290             final float radius;
   1291             final int type = getTypeFromId(virtualViewId);
   1292             final int value = getValueFromId(virtualViewId);
   1293             final float centerRadius;
   1294             final float degrees;
   1295             if (type == TYPE_HOUR) {
   1296                 final boolean innerCircle = getInnerCircleForHour(value);
   1297                 if (innerCircle) {
   1298                     centerRadius = mCircleRadius - mTextInset[HOURS_INNER];
   1299                     radius = mSelectorRadius;
   1300                 } else {
   1301                     centerRadius = mCircleRadius - mTextInset[HOURS];
   1302                     radius = mSelectorRadius;
   1303                 }
   1304 
   1305                 degrees = getDegreesForHour(value);
   1306             } else if (type == TYPE_MINUTE) {
   1307                 centerRadius = mCircleRadius - mTextInset[MINUTES];
   1308                 degrees = getDegreesForMinute(value);
   1309                 radius = mSelectorRadius;
   1310             } else {
   1311                 // This should never happen.
   1312                 centerRadius = 0;
   1313                 degrees = 0;
   1314                 radius = 0;
   1315             }
   1316 
   1317             final double radians = Math.toRadians(degrees);
   1318             final float xCenter = mXCenter + centerRadius * (float) Math.sin(radians);
   1319             final float yCenter = mYCenter - centerRadius * (float) Math.cos(radians);
   1320 
   1321             bounds.set((int) (xCenter - radius), (int) (yCenter - radius),
   1322                     (int) (xCenter + radius), (int) (yCenter + radius));
   1323         }
   1324 
   1325         private CharSequence getVirtualViewDescription(int type, int value) {
   1326             final CharSequence description;
   1327             if (type == TYPE_HOUR || type == TYPE_MINUTE) {
   1328                 description = Integer.toString(value);
   1329             } else {
   1330                 description = null;
   1331             }
   1332             return description;
   1333         }
   1334 
   1335         private boolean isVirtualViewSelected(int type, int value) {
   1336             final boolean selected;
   1337             if (type == TYPE_HOUR) {
   1338                 selected = getCurrentHour() == value;
   1339             } else if (type == TYPE_MINUTE) {
   1340                 selected = getCurrentMinute() == value;
   1341             } else {
   1342                 selected = false;
   1343             }
   1344             return selected;
   1345         }
   1346 
   1347         private int makeId(int type, int value) {
   1348             return type << SHIFT_TYPE | value << SHIFT_VALUE;
   1349         }
   1350 
   1351         private int getTypeFromId(int id) {
   1352             return id >>> SHIFT_TYPE & MASK_TYPE;
   1353         }
   1354 
   1355         private int getValueFromId(int id) {
   1356             return id >>> SHIFT_VALUE & MASK_VALUE;
   1357         }
   1358     }
   1359 
   1360     private static class IntHolder {
   1361         private int mValue;
   1362 
   1363         public IntHolder(int value) {
   1364             mValue = value;
   1365         }
   1366 
   1367         public void setValue(int value) {
   1368             mValue = value;
   1369         }
   1370 
   1371         public int getValue() {
   1372             return mValue;
   1373         }
   1374     }
   1375 }
   1376