FileDocCategorySizeDatePackage
RadialPickerLayout.javaAPI DocAndroid 5.1 API35171Thu Mar 12 22:22:50 GMT 2015com.android.datetimepicker.time

RadialPickerLayout

public class RadialPickerLayout extends android.widget.FrameLayout implements android.view.View.OnTouchListener
The primary layout to hold the circular picker, and the am/pm buttons. This view well measure itself to end up as a square. It also handles touches to be passed in to views that need to know when they'd been touched.

Fields Summary
private static final String
TAG
private final int
TOUCH_SLOP
private final int
TAP_TIMEOUT
private static final int
VISIBLE_DEGREES_STEP_SIZE
private static final int
HOUR_VALUE_TO_DEGREES_STEP_SIZE
private static final int
MINUTE_VALUE_TO_DEGREES_STEP_SIZE
private static final int
HOUR_INDEX
private static final int
MINUTE_INDEX
private static final int
AMPM_INDEX
private static final int
ENABLE_PICKER_INDEX
private static final int
AM
private static final int
PM
private int
mLastValueSelected
private com.android.datetimepicker.HapticFeedbackController
mHapticFeedbackController
private OnValueSelectedListener
mListener
private boolean
mTimeInitialized
private int
mCurrentHoursOfDay
private int
mCurrentMinutes
private boolean
mIs24HourMode
private boolean
mHideAmPm
private int
mCurrentItemShowing
private CircleView
mCircleView
private AmPmCirclesView
mAmPmCirclesView
private RadialTextsView
mHourRadialTextsView
private RadialTextsView
mMinuteRadialTextsView
private RadialSelectorView
mHourRadialSelectorView
private RadialSelectorView
mMinuteRadialSelectorView
private android.view.View
mGrayBox
private int[]
mSnapPrefer30sMap
private boolean
mInputEnabled
private int
mIsTouchingAmOrPm
private boolean
mDoingMove
private boolean
mDoingTouch
private int
mDownDegrees
private float
mDownX
private float
mDownY
private android.view.accessibility.AccessibilityManager
mAccessibilityManager
private android.animation.AnimatorSet
mTransition
private android.os.Handler
mHandler
Constructors Summary
public RadialPickerLayout(android.content.Context context, android.util.AttributeSet attrs)


       
              
    

         
        super(context, attrs);

        setOnTouchListener(this);
        ViewConfiguration vc = ViewConfiguration.get(context);
        TOUCH_SLOP = vc.getScaledTouchSlop();
        TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
        mDoingMove = false;

        mCircleView = new CircleView(context);
        addView(mCircleView);

        mAmPmCirclesView = new AmPmCirclesView(context);
        addView(mAmPmCirclesView);

        mHourRadialTextsView = new RadialTextsView(context);
        addView(mHourRadialTextsView);
        mMinuteRadialTextsView = new RadialTextsView(context);
        addView(mMinuteRadialTextsView);

        mHourRadialSelectorView = new RadialSelectorView(context);
        addView(mHourRadialSelectorView);
        mMinuteRadialSelectorView = new RadialSelectorView(context);
        addView(mMinuteRadialSelectorView);

        // Prepare mapping to snap touchable degrees to selectable degrees.
        preparePrefer30sMap();

        mLastValueSelected = -1;

        mInputEnabled = true;
        mGrayBox = new View(context);
        mGrayBox.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        mGrayBox.setBackgroundColor(getResources().getColor(R.color.transparent_black));
        mGrayBox.setVisibility(View.INVISIBLE);
        addView(mGrayBox);

        mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);

        mTimeInitialized = false;
    
Methods Summary
public booleandispatchPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent event)
Announce the currently-selected time when launched.

        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            // Clear the event's current text so that only the current time will be spoken.
            event.getText().clear();
            Time time = new Time();
            time.hour = getHours();
            time.minute = getMinutes();
            long millis = time.normalize(true);
            int flags = DateUtils.FORMAT_SHOW_TIME;
            if (mIs24HourMode) {
                flags |= DateUtils.FORMAT_24HOUR;
            }
            String timeString = DateUtils.formatDateTime(getContext(), millis, flags);
            event.getText().add(timeString);
            return true;
        }
        return super.dispatchPopulateAccessibilityEvent(event);
    
public intgetCurrentItemShowing()
Get the item (hours or minutes) that is currently showing.

        if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX) {
            Log.e(TAG, "Current item showing was unfortunately set to "+mCurrentItemShowing);
            return -1;
        }
        return mCurrentItemShowing;
    
private intgetCurrentlyShowingValue()
If the hours are showing, return the current hour. If the minutes are showing, return the current minute.

        int currentIndex = getCurrentItemShowing();
        if (currentIndex == HOUR_INDEX) {
            return mCurrentHoursOfDay;
        } else if (currentIndex == MINUTE_INDEX) {
            return mCurrentMinutes;
        } else {
            return -1;
        }
    
private intgetDegreesFromCoords(float pointX, float pointY, boolean forceLegal, java.lang.Boolean[] isInnerCircle)
Calculate the degrees within the circle that corresponds to the specified coordinates, if the coordinates are within the range that will trigger a selection.

param
pointX The x coordinate.
param
pointY The y coordinate.
param
forceLegal Force the selection to be legal, regardless of how far the coordinates are from the actual numbers.
param
isInnerCircle If the selection may be in the inner circle, pass in a size-1 boolean array here, inside which the value will be true if the selection is in the inner circle, and false if in the outer circle.
return
Degrees from 0 to 360, if the selection was within the legal range. -1 if not.

        int currentItem = getCurrentItemShowing();
        if (currentItem == HOUR_INDEX) {
            return mHourRadialSelectorView.getDegreesFromCoords(
                    pointX, pointY, forceLegal, isInnerCircle);
        } else if (currentItem == MINUTE_INDEX) {
            return mMinuteRadialSelectorView.getDegreesFromCoords(
                    pointX, pointY, forceLegal, isInnerCircle);
        } else {
            return -1;
        }
    
public intgetHours()

        return mCurrentHoursOfDay;
    
public intgetIsCurrentlyAmOrPm()

        if (mCurrentHoursOfDay < 12) {
            return AM;
        } else if (mCurrentHoursOfDay < 24) {
            return PM;
        }
        return -1;
    
public intgetMinutes()

        return mCurrentMinutes;
    
public voidinitialize(android.content.Context context, com.android.datetimepicker.HapticFeedbackController hapticFeedbackController, int initialHoursOfDay, int initialMinutes, boolean is24HourMode)
Initialize the Layout with starting values.

param
context
param
initialHoursOfDay
param
initialMinutes
param
is24HourMode

        if (mTimeInitialized) {
            Log.e(TAG, "Time has already been initialized.");
            return;
        }

        mHapticFeedbackController = hapticFeedbackController;
        mIs24HourMode = is24HourMode;
        mHideAmPm = mAccessibilityManager.isTouchExplorationEnabled()? true : mIs24HourMode;

        // Initialize the circle and AM/PM circles if applicable.
        mCircleView.initialize(context, mHideAmPm);
        mCircleView.invalidate();
        if (!mHideAmPm) {
            mAmPmCirclesView.initialize(context, initialHoursOfDay < 12? AM : PM);
            mAmPmCirclesView.invalidate();
        }

        // Initialize the hours and minutes numbers.
        Resources res = context.getResources();
        int[] hours = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
        int[] hours_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
        int[] minutes = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
        String[] hoursTexts = new String[12];
        String[] innerHoursTexts = new String[12];
        String[] minutesTexts = new String[12];
        for (int i = 0; i < 12; i++) {
            hoursTexts[i] = is24HourMode?
                    String.format("%02d", hours_24[i]) : String.format("%d", hours[i]);
            innerHoursTexts[i] = String.format("%d", hours[i]);
            minutesTexts[i] = String.format("%02d", minutes[i]);
        }
        mHourRadialTextsView.initialize(res,
                hoursTexts, (is24HourMode? innerHoursTexts : null), mHideAmPm, true);
        mHourRadialTextsView.invalidate();
        mMinuteRadialTextsView.initialize(res, minutesTexts, null, mHideAmPm, false);
        mMinuteRadialTextsView.invalidate();

        // Initialize the currently-selected hour and minute.
        setValueForItem(HOUR_INDEX, initialHoursOfDay);
        setValueForItem(MINUTE_INDEX, initialMinutes);
        int hourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
        mHourRadialSelectorView.initialize(context, mHideAmPm, is24HourMode, true,
                hourDegrees, isHourInnerCircle(initialHoursOfDay));
        int minuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
        mMinuteRadialSelectorView.initialize(context, mHideAmPm, false, false,
                minuteDegrees, false);

        mTimeInitialized = true;
    
private booleanisHourInnerCircle(int hourOfDay)
Check if a given hour appears in the outer circle or the inner circle

return
true if the hour is in the inner circle, false if it's in the outer circle.

        // We'll have the 00 hours on the outside circle.
        return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0);
    
public voidonInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo info)
Necessary for accessibility, to ensure we support "scrolling" forward and backward in the circle.

      super.onInitializeAccessibilityNodeInfo(info);
      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
    
public voidonMeasure(int widthMeasureSpec, int heightMeasureSpec)
Measure the view to end up as a square, based on the minimum of the height and width.

        int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int minDimension = Math.min(measuredWidth, measuredHeight);

        super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode),
                MeasureSpec.makeMeasureSpec(minDimension, heightMode));
    
public booleanonTouch(android.view.View v, android.view.MotionEvent event)

        final float eventX = event.getX();
        final float eventY = event.getY();
        int degrees;
        int value;
        final Boolean[] isInnerCircle = new Boolean[1];
        isInnerCircle[0] = false;

        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mInputEnabled) {
                    return true;
                }

                mDownX = eventX;
                mDownY = eventY;

                mLastValueSelected = -1;
                mDoingMove = false;
                mDoingTouch = true;
                // If we're showing the AM/PM, check to see if the user is touching it.
                if (!mHideAmPm) {
                    mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
                } else {
                    mIsTouchingAmOrPm = -1;
                }
                if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
                    // If the touch is on AM or PM, set it as "touched" after the TAP_TIMEOUT
                    // in case the user moves their finger quickly.
                    mHapticFeedbackController.tryVibrate();
                    mDownDegrees = -1;
                    mHandler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mAmPmCirclesView.setAmOrPmPressed(mIsTouchingAmOrPm);
                            mAmPmCirclesView.invalidate();
                        }
                    }, TAP_TIMEOUT);
                } else {
                    // If we're in accessibility mode, force the touch to be legal. Otherwise,
                    // it will only register within the given touch target zone.
                    boolean forceLegal = mAccessibilityManager.isTouchExplorationEnabled();
                    // Calculate the degrees that is currently being touched.
                    mDownDegrees = getDegreesFromCoords(eventX, eventY, forceLegal, isInnerCircle);
                    if (mDownDegrees != -1) {
                        // If it's a legal touch, set that number as "selected" after the
                        // TAP_TIMEOUT in case the user moves their finger quickly.
                        mHapticFeedbackController.tryVibrate();
                        mHandler.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                mDoingMove = true;
                                int value = reselectSelector(mDownDegrees, isInnerCircle[0],
                                        false, true);
                                mLastValueSelected = value;
                                mListener.onValueSelected(getCurrentItemShowing(), value, false);
                            }
                        }, TAP_TIMEOUT);
                    }
                }
                return true;
            case MotionEvent.ACTION_MOVE:
                if (!mInputEnabled) {
                    // We shouldn't be in this state, because input is disabled.
                    Log.e(TAG, "Input was disabled, but received ACTION_MOVE.");
                    return true;
                }

                float dY = Math.abs(eventY - mDownY);
                float dX = Math.abs(eventX - mDownX);

                if (!mDoingMove && dX <= TOUCH_SLOP && dY <= TOUCH_SLOP) {
                    // Hasn't registered down yet, just slight, accidental movement of finger.
                    break;
                }

                // If we're in the middle of touching down on AM or PM, check if we still are.
                // If so, no-op. If not, remove its pressed state. Either way, no need to check
                // for touches on the other circle.
                if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
                    mHandler.removeCallbacksAndMessages(null);
                    int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
                    if (isTouchingAmOrPm != mIsTouchingAmOrPm) {
                        mAmPmCirclesView.setAmOrPmPressed(-1);
                        mAmPmCirclesView.invalidate();
                        mIsTouchingAmOrPm = -1;
                    }
                    break;
                }

                if (mDownDegrees == -1) {
                    // Original down was illegal, so no movement will register.
                    break;
                }

                // We're doing a move along the circle, so move the selection as appropriate.
                mDoingMove = true;
                mHandler.removeCallbacksAndMessages(null);
                degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle);
                if (degrees != -1) {
                    value = reselectSelector(degrees, isInnerCircle[0], false, true);
                    if (value != mLastValueSelected) {
                        mHapticFeedbackController.tryVibrate();
                        mLastValueSelected = value;
                        mListener.onValueSelected(getCurrentItemShowing(), value, false);
                    }
                }
                return true;
            case MotionEvent.ACTION_UP:
                if (!mInputEnabled) {
                    // If our touch input was disabled, tell the listener to re-enable us.
                    Log.d(TAG, "Input was disabled, but received ACTION_UP.");
                    mListener.onValueSelected(ENABLE_PICKER_INDEX, 1, false);
                    return true;
                }

                mHandler.removeCallbacksAndMessages(null);
                mDoingTouch = false;

                // If we're touching AM or PM, set it as selected, and tell the listener.
                if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
                    int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
                    mAmPmCirclesView.setAmOrPmPressed(-1);
                    mAmPmCirclesView.invalidate();

                    if (isTouchingAmOrPm == mIsTouchingAmOrPm) {
                        mAmPmCirclesView.setAmOrPm(isTouchingAmOrPm);
                        if (getIsCurrentlyAmOrPm() != isTouchingAmOrPm) {
                            mListener.onValueSelected(AMPM_INDEX, mIsTouchingAmOrPm, false);
                            setValueForItem(AMPM_INDEX, isTouchingAmOrPm);
                        }
                    }
                    mIsTouchingAmOrPm = -1;
                    break;
                }

                // If we have a legal degrees selected, set the value and tell the listener.
                if (mDownDegrees != -1) {
                    degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle);
                    if (degrees != -1) {
                        value = reselectSelector(degrees, isInnerCircle[0], !mDoingMove, false);
                        if (getCurrentItemShowing() == HOUR_INDEX && !mIs24HourMode) {
                            int amOrPm = getIsCurrentlyAmOrPm();
                            if (amOrPm == AM && value == 12) {
                                value = 0;
                            } else if (amOrPm == PM && value != 12) {
                                value += 12;
                            }
                        }
                        setValueForItem(getCurrentItemShowing(), value);
                        mListener.onValueSelected(getCurrentItemShowing(), value, true);
                    }
                }
                mDoingMove = false;
                return true;
            default:
                break;
        }
        return false;
    
public booleanperformAccessibilityAction(int action, android.os.Bundle arguments)
When scroll forward/backward events are received, jump the time to the higher/lower discrete, visible value on the circle.

        if (super.performAccessibilityAction(action, arguments)) {
            return true;
        }

        int changeMultiplier = 0;
        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
            changeMultiplier = 1;
        } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
            changeMultiplier = -1;
        }
        if (changeMultiplier != 0) {
            int value = getCurrentlyShowingValue();
            int stepSize = 0;
            int currentItemShowing = getCurrentItemShowing();
            if (currentItemShowing == HOUR_INDEX) {
                stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
                value %= 12;
            } else if (currentItemShowing == MINUTE_INDEX) {
                stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
            }

            int degrees = value * stepSize;
            degrees = snapOnly30s(degrees, changeMultiplier);
            value = degrees / stepSize;
            int maxValue = 0;
            int minValue = 0;
            if (currentItemShowing == HOUR_INDEX) {
                if (mIs24HourMode) {
                    maxValue = 23;
                } else {
                    maxValue = 12;
                    minValue = 1;
                }
            } else {
                maxValue = 55;
            }
            if (value > maxValue) {
                // If we scrolled forward past the highest number, wrap around to the lowest.
                value = minValue;
            } else if (value < minValue) {
                // If we scrolled backward past the lowest number, wrap around to the highest.
                value = maxValue;
            }
            setItem(currentItemShowing, value);
            mListener.onValueSelected(currentItemShowing, value, false);
            return true;
        }

        return false;
    
private voidpreparePrefer30sMap()
Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger selectable area to each of the 12 visible values, such that the ratio of space apportioned to a visible value : space apportioned to a non-visible value will be 14 : 4. E.g. the output of 30 degrees should have a higher range of input associated with it than the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock circle (5 on the minutes, 1 or 13 on the hours).

        // We'll split up the visible output and the non-visible output such that each visible
        // output will correspond to a range of 14 associated input degrees, and each non-visible
        // output will correspond to a range of 4 associate input degrees, so visible numbers
        // are more than 3 times easier to get than non-visible numbers:
        // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc.
        //
        // If an output of 30 degrees should correspond to a range of 14 associated degrees, then
        // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should
        // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you
        // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this
        // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the
        // ability to aggressively prefer the visible values by a factor of more than 3:1, which
        // greatly contributes to the selectability of these values.

        // Our input will be 0 through 360.
        mSnapPrefer30sMap = new int[361];

        // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}.
        int snappedOutputDegrees = 0;
        // Count of how many inputs we've designated to the specified output.
        int count = 1;
        // How many input we expect for a specified output. This will be 14 for output divisible
        // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so
        // the caller can decide which they need.
        int expectedCount = 8;
        // Iterate through the input.
        for (int degrees = 0; degrees < 361; degrees++) {
            // Save the input-output mapping.
            mSnapPrefer30sMap[degrees] = snappedOutputDegrees;
            // If this is the last input for the specified output, calculate the next output and
            // the next expected count.
            if (count == expectedCount) {
                snappedOutputDegrees += 6;
                if (snappedOutputDegrees == 360) {
                    expectedCount = 7;
                } else if (snappedOutputDegrees % 30 == 0) {
                    expectedCount = 14;
                } else {
                    expectedCount = 4;
                }
                count = 1;
            } else {
                count++;
            }
        }
    
private intreselectSelector(int degrees, boolean isInnerCircle, boolean forceToVisibleValue, boolean forceDrawDot)
For the currently showing view (either hours or minutes), re-calculate the position for the selector, and redraw it at that position. The input degrees will be snapped to a selectable value.

param
degrees Degrees which should be selected.
param
isInnerCircle Whether the selection should be in the inner circle; will be ignored if there is no inner circle.
param
forceToVisibleValue Even if the currently-showing circle allows for fine-grained selection (i.e. minutes), force the selection to one of the visibly-showing values.
param
forceDrawDot The dot in the circle will generally only be shown when the selection is on non-visible values, but use this to force the dot to be shown.
return
The value that was selected, i.e. 0-23 for hours, 0-59 for minutes.

        if (degrees == -1) {
            return -1;
        }
        int currentShowing = getCurrentItemShowing();

        int stepSize;
        boolean allowFineGrained = !forceToVisibleValue && (currentShowing == MINUTE_INDEX);
        if (allowFineGrained) {
            degrees = snapPrefer30s(degrees);
        } else {
            degrees = snapOnly30s(degrees, 0);
        }

        RadialSelectorView radialSelectorView;
        if (currentShowing == HOUR_INDEX) {
            radialSelectorView = mHourRadialSelectorView;
            stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
        } else {
            radialSelectorView = mMinuteRadialSelectorView;
            stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
        }
        radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawDot);
        radialSelectorView.invalidate();


        if (currentShowing == HOUR_INDEX) {
            if (mIs24HourMode) {
                if (degrees == 0 && isInnerCircle) {
                    degrees = 360;
                } else if (degrees == 360 && !isInnerCircle) {
                    degrees = 0;
                }
            } else if (degrees == 0) {
                degrees = 360;
            }
        } else if (degrees == 360 && currentShowing == MINUTE_INDEX) {
            degrees = 0;
        }

        int value = degrees / stepSize;
        if (currentShowing == HOUR_INDEX && mIs24HourMode && !isInnerCircle && degrees != 0) {
            value += 12;
        }
        return value;
    
public voidsetAmOrPm(int amOrPm)
Set the internal value as either AM or PM, and update the AM/PM circle displays.

param
amOrPm

        mAmPmCirclesView.setAmOrPm(amOrPm);
        mAmPmCirclesView.invalidate();
        setValueForItem(AMPM_INDEX, amOrPm);
    
public voidsetCurrentItemShowing(int index, boolean animate)
Set either minutes or hours as showing.

param
animate True to animate the transition, false to show with no animation.

        if (index != HOUR_INDEX && index != MINUTE_INDEX) {
            Log.e(TAG, "TimePicker does not support view at index "+index);
            return;
        }

        int lastIndex = getCurrentItemShowing();
        mCurrentItemShowing = index;

        if (animate && (index != lastIndex)) {
            ObjectAnimator[] anims = new ObjectAnimator[4];
            if (index == MINUTE_INDEX) {
                anims[0] = mHourRadialTextsView.getDisappearAnimator();
                anims[1] = mHourRadialSelectorView.getDisappearAnimator();
                anims[2] = mMinuteRadialTextsView.getReappearAnimator();
                anims[3] = mMinuteRadialSelectorView.getReappearAnimator();
            } else if (index == HOUR_INDEX){
                anims[0] = mHourRadialTextsView.getReappearAnimator();
                anims[1] = mHourRadialSelectorView.getReappearAnimator();
                anims[2] = mMinuteRadialTextsView.getDisappearAnimator();
                anims[3] = mMinuteRadialSelectorView.getDisappearAnimator();
            }

            if (mTransition != null && mTransition.isRunning()) {
                mTransition.end();
            }
            mTransition = new AnimatorSet();
            mTransition.playTogether(anims);
            mTransition.start();
        } else {
            int hourAlpha = (index == HOUR_INDEX) ? 255 : 0;
            int minuteAlpha = (index == MINUTE_INDEX) ? 255 : 0;
            mHourRadialTextsView.setAlpha(hourAlpha);
            mHourRadialSelectorView.setAlpha(hourAlpha);
            mMinuteRadialTextsView.setAlpha(minuteAlpha);
            mMinuteRadialSelectorView.setAlpha(minuteAlpha);
        }

    
private voidsetItem(int index, int value)
Set either the hour or the minute. Will set the internal value, and set the selection.

        if (index == HOUR_INDEX) {
            setValueForItem(HOUR_INDEX, value);
            int hourDegrees = (value % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
            mHourRadialSelectorView.setSelection(hourDegrees, isHourInnerCircle(value), false);
            mHourRadialSelectorView.invalidate();
        } else if (index == MINUTE_INDEX) {
            setValueForItem(MINUTE_INDEX, value);
            int minuteDegrees = value * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
            mMinuteRadialSelectorView.setSelection(minuteDegrees, false, false);
            mMinuteRadialSelectorView.invalidate();
        }
    
public voidsetOnValueSelectedListener(com.android.datetimepicker.time.RadialPickerLayout$OnValueSelectedListener listener)

        mListener = listener;
    
voidsetTheme(android.content.Context context, boolean themeDark)

        mCircleView.setTheme(context, themeDark);
        mAmPmCirclesView.setTheme(context, themeDark);
        mHourRadialTextsView.setTheme(context, themeDark);
        mMinuteRadialTextsView.setTheme(context, themeDark);
        mHourRadialSelectorView.setTheme(context, themeDark);
        mMinuteRadialSelectorView.setTheme(context, themeDark);
   
public voidsetTime(int hours, int minutes)

        setItem(HOUR_INDEX, hours);
        setItem(MINUTE_INDEX, minutes);
    
private voidsetValueForItem(int index, int value)
Set the internal value for the hour, minute, or AM/PM.

        if (index == HOUR_INDEX) {
            mCurrentHoursOfDay = value;
        } else if (index == MINUTE_INDEX){
            mCurrentMinutes = value;
        } else if (index == AMPM_INDEX) {
            if (value == AM) {
                mCurrentHoursOfDay = mCurrentHoursOfDay % 12;
            } else if (value == PM) {
                mCurrentHoursOfDay = (mCurrentHoursOfDay % 12) + 12;
            }
        }
    
private static intsnapOnly30s(int degrees, int forceHigherOrLower)
Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all multiples of 30), where the input will be "snapped" to the closest visible degrees.

param
degrees The input degrees
param
forceAboveOrBelow The output may be forced to either the higher or lower step, or may be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force strictly lower, and 0 to snap to the closer one.
return
output degrees, will be a multiple of 30

        int stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
        int floor = (degrees / stepSize) * stepSize;
        int ceiling = floor + stepSize;
        if (forceHigherOrLower == 1) {
            degrees = ceiling;
        } else if (forceHigherOrLower == -1) {
            if (degrees == floor) {
                floor -= stepSize;
            }
            degrees = floor;
        } else {
            if ((degrees - floor) < (ceiling - degrees)) {
                degrees = floor;
            } else {
                degrees = ceiling;
            }
        }
        return degrees;
    
private intsnapPrefer30s(int degrees)
Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees, where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be weighted heavier than the degrees corresponding to non-visible numbers. See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the mapping.

        if (mSnapPrefer30sMap == null) {
            return -1;
        }
        return mSnapPrefer30sMap[degrees];
    
public booleantrySettingInputEnabled(boolean inputEnabled)
Set touch input as enabled or disabled, for use with keyboard mode.

        if (mDoingTouch && !inputEnabled) {
            // If we're trying to disable input, but we're in the middle of a touch event,
            // we'll allow the touch event to continue before disabling input.
            return false;
        }
        mInputEnabled = inputEnabled;
        mGrayBox.setVisibility(inputEnabled? View.INVISIBLE : View.VISIBLE);
        return true;