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