RadialPickerLayout.java revision b8f95646fc0510eebfeaa27864023d630f34090f
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.datetimepicker.time;
18
19import android.animation.AnimatorSet;
20import android.animation.ObjectAnimator;
21import android.annotation.SuppressLint;
22import android.app.Service;
23import android.content.Context;
24import android.content.res.Resources;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.SystemClock;
28import android.os.Vibrator;
29import android.text.format.DateUtils;
30import android.text.format.Time;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.view.MotionEvent;
34import android.view.View;
35import android.view.View.OnTouchListener;
36import android.view.ViewConfiguration;
37import android.view.ViewGroup;
38import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.AccessibilityManager;
40import android.view.accessibility.AccessibilityNodeInfo;
41import android.widget.FrameLayout;
42
43import com.android.datetimepicker.R;
44
45public class RadialPickerLayout extends FrameLayout implements OnTouchListener {
46    private static final String TAG = "TimePicker";
47
48    private final int TOUCH_SLOP;
49    private final int TAP_TIMEOUT;
50    private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = 30;
51    private static final int MINUTE_VALUE_TO_DEGREES_STEP_SIZE = 6;
52    private static final int HOUR_INDEX = TimePickerDialog.HOUR_INDEX;
53    private static final int MINUTE_INDEX = TimePickerDialog.MINUTE_INDEX;
54    private static final int AMPM_INDEX = TimePickerDialog.AMPM_INDEX;
55    private static final int ENABLE_PICKER_INDEX = TimePickerDialog.ENABLE_PICKER_INDEX;
56    private static final int AM = TimePickerDialog.AM;
57    private static final int PM = TimePickerDialog.PM;
58
59    private Vibrator mVibrator;
60    private long mLastVibrate;
61    private int mLastValueSelected;
62
63    private OnValueSelectedListener mListener;
64    private boolean mTimeInitialized;
65    private int mCurrentHoursOfDay;
66    private int mCurrentMinutes;
67    private boolean mIs24HourMode;
68    private boolean mHideAmPm;
69    private int mCurrentItemShowing;
70
71    private CircleView mCircleView;
72    private AmPmCirclesView mAmPmCirclesView;
73    private RadialTextsView mHourRadialTextsView;
74    private RadialTextsView mMinuteRadialTextsView;
75    private RadialSelectorView mHourRadialSelectorView;
76    private RadialSelectorView mMinuteRadialSelectorView;
77    private View mGrayBox;
78
79    private boolean mInputEnabled;
80    private int mIsTouchingAmOrPm = -1;
81    private boolean mDoingMove;
82    private boolean mDoingTouch;
83    private int mDownDegrees;
84    private float mDownX;
85    private float mDownY;
86    private AccessibilityManager mAccessibilityManager;
87
88    private Handler mHandler = new Handler();
89
90    public interface OnValueSelectedListener {
91        void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
92    }
93
94    public RadialPickerLayout(Context context, AttributeSet attrs) {
95        super(context, attrs);
96
97        setOnTouchListener(this);
98        ViewConfiguration vc = ViewConfiguration.get(context);
99        TOUCH_SLOP = vc.getScaledTouchSlop();
100        TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
101        mDoingMove = false;
102
103        mCircleView = new CircleView(context);
104        addView(mCircleView);
105
106        mAmPmCirclesView = new AmPmCirclesView(context);
107        addView(mAmPmCirclesView);
108
109        mHourRadialTextsView = new RadialTextsView(context);
110        addView(mHourRadialTextsView);
111        mMinuteRadialTextsView = new RadialTextsView(context);
112        addView(mMinuteRadialTextsView);
113
114        mHourRadialSelectorView = new RadialSelectorView(context);
115        addView(mHourRadialSelectorView);
116        mMinuteRadialSelectorView = new RadialSelectorView(context);
117        addView(mMinuteRadialSelectorView);
118
119        mVibrator = (Vibrator) context.getSystemService(Service.VIBRATOR_SERVICE);
120        mLastVibrate = 0;
121        mLastValueSelected = -1;
122
123        mTimeInitialized = false;
124
125        mInputEnabled = true;
126        mGrayBox = new View(context);
127        mGrayBox.setLayoutParams(new ViewGroup.LayoutParams(
128                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
129        mGrayBox.setBackgroundColor(getResources().getColor(R.color.black_50));
130        mGrayBox.setVisibility(View.INVISIBLE);
131        addView(mGrayBox);
132
133        mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
134    }
135
136    @Override
137    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
138        int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
139        int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
140        super.onMeasure(widthMeasureSpec,
141                measuredWidth < measuredHeight? widthMeasureSpec : heightMeasureSpec);
142    }
143
144    public void setOnValueSelectedListener(OnValueSelectedListener listener) {
145        mListener = listener;
146    }
147
148    public void initialize(Context context, int initialHoursOfDay, int initialMinutes,
149            boolean is24HourMode) {
150        if (mTimeInitialized) {
151            Log.e(TAG, "Time has already been initialized.");
152            return;
153        }
154        mIs24HourMode = is24HourMode;
155        mHideAmPm = mAccessibilityManager.isTouchExplorationEnabled()? true : mIs24HourMode;
156
157        mCircleView.initialize(context, mHideAmPm);
158        mCircleView.invalidate();
159        if (!mHideAmPm) {
160            mAmPmCirclesView.initialize(context, initialHoursOfDay < 12? AM : PM);
161            mAmPmCirclesView.invalidate();
162        }
163
164        Resources res = context.getResources();
165        int[] hours = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
166        int[] hours_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
167        int[] minutes = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
168        String[] hoursTexts = new String[12];
169        String[] innerHoursTexts = new String[12];
170        String[] minutesTexts = new String[12];
171        for (int i = 0; i < 12; i++) {
172            hoursTexts[i] = is24HourMode?
173                    String.format("%02d", hours_24[i]) : String.format("%d", hours[i]);
174            innerHoursTexts[i] = String.format("%d", hours[i]);
175            minutesTexts[i] = String.format("%02d", minutes[i]);
176        }
177        mHourRadialTextsView.initialize(res,
178                hoursTexts, (is24HourMode? innerHoursTexts : null), mHideAmPm, true);
179        mHourRadialTextsView.invalidate();
180        mMinuteRadialTextsView.initialize(res, minutesTexts, null, mHideAmPm, false);
181        mMinuteRadialTextsView.invalidate();
182
183        setValueForItem(HOUR_INDEX, initialHoursOfDay);
184        setValueForItem(MINUTE_INDEX, initialMinutes);
185        int hourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
186        mHourRadialSelectorView.initialize(context, mHideAmPm, is24HourMode, true,
187                hourDegrees, isHourInnerCircle(initialHoursOfDay));
188        int minuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
189        mMinuteRadialSelectorView.initialize(context, mHideAmPm, false, false,
190                minuteDegrees, false);
191
192        mTimeInitialized = true;
193    }
194
195    public void setTime(int hours, int minutes) {
196        setItem(HOUR_INDEX, hours);
197        setItem(MINUTE_INDEX, minutes);
198    }
199
200    private void setItem(int index, int value) {
201        if (index == HOUR_INDEX) {
202            setValueForItem(HOUR_INDEX, value);
203            int hourDegrees = (value % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
204            mHourRadialSelectorView.setSelection(hourDegrees, isHourInnerCircle(value),
205                    false, false, false);
206            mHourRadialSelectorView.invalidate();
207        } else if (index == MINUTE_INDEX) {
208            setValueForItem(MINUTE_INDEX, value);
209            int minuteDegrees = value * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
210            mMinuteRadialSelectorView.setSelection(minuteDegrees, false, false, false, false);
211            mMinuteRadialSelectorView.invalidate();
212        }
213    }
214
215    private boolean isHourInnerCircle(int hourOfDay) {
216        // We'll have the 00 hours on the outside circle.
217        return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0);
218    }
219
220    public int getHours() {
221        return mCurrentHoursOfDay;
222    }
223
224    public int getMinutes() {
225        return mCurrentMinutes;
226    }
227
228    private int getCurrentlyShowingValue() {
229        int currentIndex = getCurrentItemShowing();
230        if (currentIndex == HOUR_INDEX) {
231            return mCurrentHoursOfDay;
232        } else if (currentIndex == MINUTE_INDEX) {
233            return mCurrentMinutes;
234        } else {
235            return -1;
236        }
237    }
238
239    public int getIsCurrentlyAmOrPm() {
240        if (mCurrentHoursOfDay < 12) {
241            return AM;
242        } else if (mCurrentHoursOfDay < 24) {
243            return PM;
244        }
245        return -1;
246    }
247
248    private void setValueForItem(int index, int value) {
249        if (index == HOUR_INDEX) {
250            mCurrentHoursOfDay = value;
251        } else if (index == MINUTE_INDEX){
252            mCurrentMinutes = value;
253        } else if (index == AMPM_INDEX) {
254            if (value == AM) {
255                mCurrentHoursOfDay = mCurrentHoursOfDay % 12;
256            } else if (value == PM) {
257                mCurrentHoursOfDay = (mCurrentHoursOfDay % 12) + 12;
258            }
259        }
260    }
261
262    public void setAmOrPm(int amOrPm) {
263        mAmPmCirclesView.setAmOrPm(amOrPm);
264        mAmPmCirclesView.invalidate();
265        setValueForItem(AMPM_INDEX, amOrPm);
266    }
267
268    private int highPass30sFilter(int degrees) {
269        int offset = (degrees + 2) / 30;
270        degrees = Math.max(degrees - (30*offset + 4), 0) + 20*offset;
271        degrees /= 4;
272        degrees *= 6;
273        /* // less aggressive filtering.
274        degrees /= 5;
275        int offset = degrees / 6;
276        degrees = degrees - offset;
277        degrees *= 6; */
278        return degrees;
279    }
280
281    private int snapToStepSize(int degrees, int stepSize, int ceilingOrFloor) {
282        int floor = (degrees / stepSize) * stepSize;
283        int ceiling = floor + stepSize;
284        if (ceilingOrFloor == 1) {
285            degrees = ceiling;
286        } else if (ceilingOrFloor == -1) {
287            if (degrees == floor) {
288                floor -= stepSize;
289            }
290            degrees = floor;
291        } else {
292            if ((degrees - floor) < (ceiling - degrees)) {
293                degrees = floor;
294            } else {
295                degrees = ceiling;
296            }
297        }
298        return degrees;
299    }
300
301    private int reselectSelector(int degrees, boolean isInnerCircle,
302            boolean forceNotFineGrained, boolean forceDrawLine, boolean forceDrawDot) {
303        if (degrees == -1) {
304            return -1;
305        }
306        int currentShowing = getCurrentItemShowing();
307
308        int stepSize;
309        boolean allowFineGrained = !forceNotFineGrained && (currentShowing == MINUTE_INDEX);
310        if (allowFineGrained) {
311            degrees = highPass30sFilter(degrees);
312        } else {
313            degrees = snapToStepSize(degrees, HOUR_VALUE_TO_DEGREES_STEP_SIZE, 0);
314        }
315
316        RadialSelectorView radialSelectorView;
317        if (currentShowing == HOUR_INDEX) {
318            radialSelectorView = mHourRadialSelectorView;
319            stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
320        } else {
321            radialSelectorView = mMinuteRadialSelectorView;
322            stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
323        }
324        radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawLine, forceDrawDot, false);
325        radialSelectorView.invalidate();
326
327
328        if (currentShowing == HOUR_INDEX) {
329            if (mIs24HourMode) {
330                if (degrees == 0 && isInnerCircle) {
331                    degrees = 360;
332                } else if (degrees == 360 && !isInnerCircle) {
333                    degrees = 0;
334                }
335            } else if (degrees == 0) {
336                degrees = 360;
337            }
338        } else if (degrees == 360 && currentShowing == MINUTE_INDEX) {
339            degrees = 0;
340        }
341
342        int value = degrees / stepSize;
343        if (currentShowing == HOUR_INDEX && mIs24HourMode && !isInnerCircle && degrees != 0) {
344            value += 12;
345        }
346        return value;
347    }
348
349    private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
350            final Boolean[] isInnerCircle) {
351        int currentItem = getCurrentItemShowing();
352        if (currentItem == HOUR_INDEX) {
353            return mHourRadialSelectorView.getDegreesFromCoords(
354                    pointX, pointY, forceLegal, isInnerCircle);
355        } else if (currentItem == MINUTE_INDEX) {
356            return mMinuteRadialSelectorView.getDegreesFromCoords(
357                    pointX, pointY, forceLegal, isInnerCircle);
358        } else {
359            return -1;
360        }
361    }
362
363    public int getCurrentItemShowing() {
364        if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX) {
365            Log.e(TAG, "Current item showing was unfortunately set to "+mCurrentItemShowing);
366            return -1;
367        }
368        return mCurrentItemShowing;
369    }
370
371    public void setCurrentItemShowing(int index, boolean animate) {
372        if (index != HOUR_INDEX && index != MINUTE_INDEX) {
373            Log.e(TAG, "TimePicker does not support view at index "+index);
374            return;
375        }
376
377        int lastIndex = getCurrentItemShowing();
378        mCurrentItemShowing = index;
379
380        if (animate && (index != lastIndex)) {
381            ObjectAnimator[] anims = new ObjectAnimator[4];
382            if (index == MINUTE_INDEX) {
383                anims[0] = mHourRadialTextsView.getDisappearAnimator();
384                anims[1] = mHourRadialSelectorView.getDisappearAnimator();
385                anims[2] = mMinuteRadialTextsView.getReappearAnimator();
386                anims[3] = mMinuteRadialSelectorView.getReappearAnimator();
387            } else if (index == HOUR_INDEX){
388                anims[0] = mHourRadialTextsView.getReappearAnimator();
389                anims[1] = mHourRadialSelectorView.getReappearAnimator();
390                anims[2] = mMinuteRadialTextsView.getDisappearAnimator();
391                anims[3] = mMinuteRadialSelectorView.getDisappearAnimator();
392            }
393
394            AnimatorSet transition = new AnimatorSet();
395            transition.playTogether(anims);
396            transition.start();
397        } else {
398            int hourAlpha = (index == HOUR_INDEX) ? 255 : 0;
399            int minuteAlpha = (index == MINUTE_INDEX) ? 255 : 0;
400            mHourRadialTextsView.setAlpha(hourAlpha);
401            mHourRadialSelectorView.setAlpha(hourAlpha);
402            mMinuteRadialTextsView.setAlpha(minuteAlpha);
403            mMinuteRadialSelectorView.setAlpha(minuteAlpha);
404        }
405
406    }
407
408    @Override
409    public boolean onTouch(View v, MotionEvent event) {
410        final float eventX = event.getX();
411        final float eventY = event.getY();
412        int degrees;
413        int value;
414        final Boolean[] isInnerCircle = new Boolean[1];
415        isInnerCircle[0] = false;
416
417        long millis = SystemClock.uptimeMillis();
418
419        switch(event.getAction()) {
420            case MotionEvent.ACTION_DOWN:
421                if (!mInputEnabled) {
422                    return true;
423                }
424
425                mDownX = eventX;
426                mDownY = eventY;
427
428                mLastValueSelected = -1;
429                mDoingMove = false;
430                mDoingTouch = true;
431                if (!mHideAmPm) {
432                    mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
433                } else {
434                    mIsTouchingAmOrPm = -1;
435                }
436                if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
437                    tryVibrate();
438                    mDownDegrees = -1;
439                    mHandler.postDelayed(new Runnable() {
440                        @Override
441                        public void run() {
442                            mAmPmCirclesView.setAmOrPmPressed(mIsTouchingAmOrPm);
443                            mAmPmCirclesView.invalidate();
444                        }
445                    }, TAP_TIMEOUT);
446                } else {
447                    boolean forceLegal = mAccessibilityManager.isTouchExplorationEnabled();
448                    mDownDegrees = getDegreesFromCoords(eventX, eventY, forceLegal, isInnerCircle);
449                    if (mDownDegrees != -1) {
450                        tryVibrate();
451                        mHandler.postDelayed(new Runnable() {
452                            @Override
453                            public void run() {
454                                mDoingMove = true;
455                                int value = reselectSelector(mDownDegrees,
456                                        isInnerCircle[0], false, true, true);
457                                mLastValueSelected = value;
458                                mListener.onValueSelected(getCurrentItemShowing(), value, false);
459                            }
460                        }, TAP_TIMEOUT);
461                    }
462                }
463                return true;
464            case MotionEvent.ACTION_MOVE:
465                if (!mInputEnabled) {
466                    // We shouldn't be in this state, because input is disabled.
467                    Log.e(TAG, "Input was disabled, but received ACTION_MOVE.");
468                    return true;
469                }
470
471                float dY = Math.abs(eventY - mDownY);
472                float dX = Math.abs(eventX - mDownX);
473
474                if (!mDoingMove && dX <= TOUCH_SLOP && dY <= TOUCH_SLOP) {
475                    // Hasn't registered down yet, just slight, accidental movement of finger.
476                    break;
477                }
478
479                // If we're in the middle of touching down on AM or PM, check if we still are.
480                // If so, no-op. If not, remove its pressed state. Either way, no need to check
481                // for touches on the other circle.
482                if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
483                    mHandler.removeCallbacksAndMessages(null);
484                    int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
485                    if (isTouchingAmOrPm != mIsTouchingAmOrPm) {
486                        mAmPmCirclesView.setAmOrPmPressed(-1);
487                        mAmPmCirclesView.invalidate();
488                        mIsTouchingAmOrPm = -1;
489                    }
490                    break;
491                }
492
493                if (mDownDegrees == -1) {
494                    // Original down was illegal, so no movement will register.
495                    break;
496                }
497
498                mDoingMove = true;
499                mHandler.removeCallbacksAndMessages(null);
500                degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle);
501                if (degrees != -1) {
502                    value = reselectSelector(degrees,
503                            isInnerCircle[0], false, true, true);
504                    if (value != mLastValueSelected) {
505                        tryVibrate();
506                        mLastValueSelected = value;
507                        mListener.onValueSelected(getCurrentItemShowing(), value, false);
508                    }
509                }
510                return true;
511            case MotionEvent.ACTION_UP:
512                if (!mInputEnabled) {
513                    Log.d(TAG, "Input was disabled, but received ACTION_UP.");
514                    mListener.onValueSelected(ENABLE_PICKER_INDEX, 1, false);
515                    return true;
516                }
517
518                mHandler.removeCallbacksAndMessages(null);
519                mDoingTouch = false;
520
521                if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
522                    int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
523                    mAmPmCirclesView.setAmOrPmPressed(-1);
524                    mAmPmCirclesView.invalidate();
525
526                    if (isTouchingAmOrPm == mIsTouchingAmOrPm) {
527                        mAmPmCirclesView.setAmOrPm(isTouchingAmOrPm);
528                        if (getIsCurrentlyAmOrPm() != isTouchingAmOrPm) {
529                            mListener.onValueSelected(AMPM_INDEX, mIsTouchingAmOrPm, false);
530                            setValueForItem(AMPM_INDEX, isTouchingAmOrPm);
531                        }
532                    }
533                    mIsTouchingAmOrPm = -1;
534                    break;
535                }
536
537                if (mDownDegrees != -1) {
538                    degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle);
539                    if (degrees != -1) {
540                        value = reselectSelector(degrees, isInnerCircle[0],
541                                !mDoingMove, true, false);
542                        if (getCurrentItemShowing() == HOUR_INDEX && !mIs24HourMode) {
543                            int amOrPm = getIsCurrentlyAmOrPm();
544                            if (amOrPm == AM && value == 12) {
545                                value = 0;
546                            } else if (amOrPm == PM && value != 12) {
547                                value += 12;
548                            }
549                        }
550                        setValueForItem(getCurrentItemShowing(), value);
551                        mListener.onValueSelected(getCurrentItemShowing(), value, true);
552                    }
553                }
554                mDoingMove = false;
555                return true;
556            default:
557                break;
558        }
559        return false;
560    }
561
562    public void tryVibrate() {
563        if (mVibrator != null) {
564            long now = SystemClock.uptimeMillis();
565            // We want to try to vibrate each individual tick discretely.
566            if (now - mLastVibrate >= 125) {
567                mVibrator.vibrate(5);
568                mLastVibrate = now;
569            }
570        }
571    }
572
573    public boolean trySettingInputEnabled(boolean inputEnabled) {
574        if (mDoingTouch && !inputEnabled) {
575            // If we're trying to disable input, but we're in the middle of a touch event,
576            // we'll allow the touch event to continue before disabling input.
577            return false;
578        }
579        mInputEnabled = inputEnabled;
580        mGrayBox.setVisibility(inputEnabled? View.INVISIBLE : View.VISIBLE);
581        return true;
582    }
583
584    @Override
585    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
586      super.onInitializeAccessibilityNodeInfo(info);
587      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
588      info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
589    }
590
591    @Override
592    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
593        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
594            event.getText().clear();
595            Time time = new Time();
596            time.hour = getHours();
597            time.minute = getMinutes();
598            long millis = time.normalize(true);
599            int flags = DateUtils.FORMAT_SHOW_TIME;
600            if (mIs24HourMode) {
601                flags |= DateUtils.FORMAT_24HOUR;
602            }
603            String timeString = DateUtils.formatDateTime(getContext(), millis, flags);
604            event.getText().add(timeString);
605            return true;
606        }
607        return super.dispatchPopulateAccessibilityEvent(event);
608    }
609
610    @SuppressLint("NewApi")
611    @Override
612    public boolean performAccessibilityAction(int action, Bundle arguments) {
613        if (super.performAccessibilityAction(action, arguments)) {
614            return true;
615        }
616
617        int changeMultiplier = 0;
618        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
619            changeMultiplier = 1;
620        } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
621            changeMultiplier = -1;
622        }
623        if (changeMultiplier != 0) {
624            int value = getCurrentlyShowingValue();
625            int stepSize = 0;
626            int currentItemShowing = getCurrentItemShowing();
627            if (currentItemShowing == HOUR_INDEX) {
628                stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
629                value %= 12;
630            } else if (currentItemShowing == MINUTE_INDEX) {
631                stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
632            }
633
634            int degrees = value * stepSize;
635            degrees = snapToStepSize(degrees, HOUR_VALUE_TO_DEGREES_STEP_SIZE, changeMultiplier);
636            value = degrees / stepSize;
637            int maxValue = 0;
638            int minValue = 0;
639            if (currentItemShowing == HOUR_INDEX) {
640                if (mIs24HourMode) {
641                    maxValue = 23;
642                } else {
643                    maxValue = 12;
644                    minValue = 1;
645                }
646            } else {
647                maxValue = 55;
648            }
649            if (value > maxValue) {
650                // If we scrolled forward past the highest number, wrap around to the lowest.
651                value = minValue;
652            } else if (value < minValue) {
653                // If we scrolled backward past the lowest number, wrap around to the highest.
654                value = maxValue;
655            }
656            setItem(currentItemShowing, value);
657            mListener.onValueSelected(currentItemShowing, value, false);
658            return true;
659        }
660
661        return false;
662    }
663}
664