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