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 com.android.datetimepicker.time; 18 19 import android.animation.AnimatorSet; 20 import android.animation.ObjectAnimator; 21 import android.annotation.SuppressLint; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.os.Bundle; 25 import android.os.Handler; 26 import android.text.format.DateUtils; 27 import android.text.format.Time; 28 import android.util.AttributeSet; 29 import android.util.Log; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.View.OnTouchListener; 33 import android.view.ViewConfiguration; 34 import android.view.ViewGroup; 35 import android.view.accessibility.AccessibilityEvent; 36 import android.view.accessibility.AccessibilityManager; 37 import android.view.accessibility.AccessibilityNodeInfo; 38 import android.widget.FrameLayout; 39 40 import com.android.datetimepicker.HapticFeedbackController; 41 import com.android.datetimepicker.R; 42 43 /** 44 * The primary layout to hold the circular picker, and the am/pm buttons. This view well measure 45 * itself to end up as a square. It also handles touches to be passed in to views that need to know 46 * when they'd been touched. 47 * 48 * @deprecated This module is deprecated. Do not use this class. 49 */ 50 public class RadialPickerLayout extends FrameLayout implements OnTouchListener { 51 private static final String TAG = "RadialPickerLayout"; 52 53 private final int TOUCH_SLOP; 54 private final int TAP_TIMEOUT; 55 56 private static final int VISIBLE_DEGREES_STEP_SIZE = 30; 57 private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = VISIBLE_DEGREES_STEP_SIZE; 58 private static final int MINUTE_VALUE_TO_DEGREES_STEP_SIZE = 6; 59 private static final int HOUR_INDEX = TimePickerDialog.HOUR_INDEX; 60 private static final int MINUTE_INDEX = TimePickerDialog.MINUTE_INDEX; 61 private static final int AMPM_INDEX = TimePickerDialog.AMPM_INDEX; 62 private static final int ENABLE_PICKER_INDEX = TimePickerDialog.ENABLE_PICKER_INDEX; 63 private static final int AM = TimePickerDialog.AM; 64 private static final int PM = TimePickerDialog.PM; 65 66 private int mLastValueSelected; 67 68 private HapticFeedbackController mHapticFeedbackController; 69 private OnValueSelectedListener mListener; 70 private boolean mTimeInitialized; 71 private int mCurrentHoursOfDay; 72 private int mCurrentMinutes; 73 private boolean mIs24HourMode; 74 private boolean mHideAmPm; 75 private int mCurrentItemShowing; 76 77 private CircleView mCircleView; 78 private AmPmCirclesView mAmPmCirclesView; 79 private RadialTextsView mHourRadialTextsView; 80 private RadialTextsView mMinuteRadialTextsView; 81 private RadialSelectorView mHourRadialSelectorView; 82 private RadialSelectorView mMinuteRadialSelectorView; 83 private View mGrayBox; 84 85 private int[] mSnapPrefer30sMap; 86 private boolean mInputEnabled; 87 private int mIsTouchingAmOrPm = -1; 88 private boolean mDoingMove; 89 private boolean mDoingTouch; 90 private int mDownDegrees; 91 private float mDownX; 92 private float mDownY; 93 private AccessibilityManager mAccessibilityManager; 94 95 private AnimatorSet mTransition; 96 private Handler mHandler = new Handler(); 97 98 public interface OnValueSelectedListener { onValueSelected(int pickerIndex, int newValue, boolean autoAdvance)99 void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); 100 } 101 RadialPickerLayout(Context context, AttributeSet attrs)102 public RadialPickerLayout(Context context, AttributeSet attrs) { 103 super(context, attrs); 104 105 setOnTouchListener(this); 106 ViewConfiguration vc = ViewConfiguration.get(context); 107 TOUCH_SLOP = vc.getScaledTouchSlop(); 108 TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); 109 mDoingMove = false; 110 111 mCircleView = new CircleView(context); 112 addView(mCircleView); 113 114 mAmPmCirclesView = new AmPmCirclesView(context); 115 addView(mAmPmCirclesView); 116 117 mHourRadialTextsView = new RadialTextsView(context); 118 addView(mHourRadialTextsView); 119 mMinuteRadialTextsView = new RadialTextsView(context); 120 addView(mMinuteRadialTextsView); 121 122 mHourRadialSelectorView = new RadialSelectorView(context); 123 addView(mHourRadialSelectorView); 124 mMinuteRadialSelectorView = new RadialSelectorView(context); 125 addView(mMinuteRadialSelectorView); 126 127 // Prepare mapping to snap touchable degrees to selectable degrees. 128 preparePrefer30sMap(); 129 130 mLastValueSelected = -1; 131 132 mInputEnabled = true; 133 mGrayBox = new View(context); 134 mGrayBox.setLayoutParams(new ViewGroup.LayoutParams( 135 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 136 mGrayBox.setBackgroundColor(getResources().getColor(R.color.transparent_black)); 137 mGrayBox.setVisibility(View.INVISIBLE); 138 addView(mGrayBox); 139 140 mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 141 142 mTimeInitialized = false; 143 } 144 145 /** 146 * Measure the view to end up as a square, based on the minimum of the height and width. 147 */ 148 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)149 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 150 int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); 151 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 152 int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); 153 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 154 int minDimension = Math.min(measuredWidth, measuredHeight); 155 156 super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode), 157 MeasureSpec.makeMeasureSpec(minDimension, heightMode)); 158 } 159 setOnValueSelectedListener(OnValueSelectedListener listener)160 public void setOnValueSelectedListener(OnValueSelectedListener listener) { 161 mListener = listener; 162 } 163 164 /** 165 * Initialize the Layout with starting values. 166 * @param context 167 * @param initialHoursOfDay 168 * @param initialMinutes 169 * @param is24HourMode 170 */ initialize(Context context, HapticFeedbackController hapticFeedbackController, int initialHoursOfDay, int initialMinutes, boolean is24HourMode)171 public void initialize(Context context, HapticFeedbackController hapticFeedbackController, 172 int initialHoursOfDay, int initialMinutes, boolean is24HourMode) { 173 if (mTimeInitialized) { 174 Log.e(TAG, "Time has already been initialized."); 175 return; 176 } 177 178 mHapticFeedbackController = hapticFeedbackController; 179 mIs24HourMode = is24HourMode; 180 mHideAmPm = mAccessibilityManager.isTouchExplorationEnabled()? true : mIs24HourMode; 181 182 // Initialize the circle and AM/PM circles if applicable. 183 mCircleView.initialize(context, mHideAmPm); 184 mCircleView.invalidate(); 185 if (!mHideAmPm) { 186 mAmPmCirclesView.initialize(context, initialHoursOfDay < 12? AM : PM); 187 mAmPmCirclesView.invalidate(); 188 } 189 190 // Initialize the hours and minutes numbers. 191 Resources res = context.getResources(); 192 int[] hours = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; 193 int[] hours_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; 194 int[] minutes = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}; 195 String[] hoursTexts = new String[12]; 196 String[] innerHoursTexts = new String[12]; 197 String[] minutesTexts = new String[12]; 198 for (int i = 0; i < 12; i++) { 199 hoursTexts[i] = is24HourMode? 200 String.format("%02d", hours_24[i]) : String.format("%d", hours[i]); 201 innerHoursTexts[i] = String.format("%d", hours[i]); 202 minutesTexts[i] = String.format("%02d", minutes[i]); 203 } 204 mHourRadialTextsView.initialize(res, 205 hoursTexts, (is24HourMode? innerHoursTexts : null), mHideAmPm, true); 206 mHourRadialTextsView.invalidate(); 207 mMinuteRadialTextsView.initialize(res, minutesTexts, null, mHideAmPm, false); 208 mMinuteRadialTextsView.invalidate(); 209 210 // Initialize the currently-selected hour and minute. 211 setValueForItem(HOUR_INDEX, initialHoursOfDay); 212 setValueForItem(MINUTE_INDEX, initialMinutes); 213 int hourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE; 214 mHourRadialSelectorView.initialize(context, mHideAmPm, is24HourMode, true, 215 hourDegrees, isHourInnerCircle(initialHoursOfDay)); 216 int minuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE; 217 mMinuteRadialSelectorView.initialize(context, mHideAmPm, false, false, 218 minuteDegrees, false); 219 220 mTimeInitialized = true; 221 } 222 setTheme(Context context, boolean themeDark)223 /* package */ void setTheme(Context context, boolean themeDark) { 224 mCircleView.setTheme(context, themeDark); 225 mAmPmCirclesView.setTheme(context, themeDark); 226 mHourRadialTextsView.setTheme(context, themeDark); 227 mMinuteRadialTextsView.setTheme(context, themeDark); 228 mHourRadialSelectorView.setTheme(context, themeDark); 229 mMinuteRadialSelectorView.setTheme(context, themeDark); 230 } 231 setTime(int hours, int minutes)232 public void setTime(int hours, int minutes) { 233 setItem(HOUR_INDEX, hours); 234 setItem(MINUTE_INDEX, minutes); 235 } 236 237 /** 238 * Set either the hour or the minute. Will set the internal value, and set the selection. 239 */ setItem(int index, int value)240 private void setItem(int index, int value) { 241 if (index == HOUR_INDEX) { 242 setValueForItem(HOUR_INDEX, value); 243 int hourDegrees = (value % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE; 244 mHourRadialSelectorView.setSelection(hourDegrees, isHourInnerCircle(value), false); 245 mHourRadialSelectorView.invalidate(); 246 } else if (index == MINUTE_INDEX) { 247 setValueForItem(MINUTE_INDEX, value); 248 int minuteDegrees = value * MINUTE_VALUE_TO_DEGREES_STEP_SIZE; 249 mMinuteRadialSelectorView.setSelection(minuteDegrees, false, false); 250 mMinuteRadialSelectorView.invalidate(); 251 } 252 } 253 254 /** 255 * Check if a given hour appears in the outer circle or the inner circle 256 * @return true if the hour is in the inner circle, false if it's in the outer circle. 257 */ isHourInnerCircle(int hourOfDay)258 private boolean isHourInnerCircle(int hourOfDay) { 259 // We'll have the 00 hours on the outside circle. 260 return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0); 261 } 262 getHours()263 public int getHours() { 264 return mCurrentHoursOfDay; 265 } 266 getMinutes()267 public int getMinutes() { 268 return mCurrentMinutes; 269 } 270 271 /** 272 * If the hours are showing, return the current hour. If the minutes are showing, return the 273 * current minute. 274 */ getCurrentlyShowingValue()275 private int getCurrentlyShowingValue() { 276 int currentIndex = getCurrentItemShowing(); 277 if (currentIndex == HOUR_INDEX) { 278 return mCurrentHoursOfDay; 279 } else if (currentIndex == MINUTE_INDEX) { 280 return mCurrentMinutes; 281 } else { 282 return -1; 283 } 284 } 285 getIsCurrentlyAmOrPm()286 public int getIsCurrentlyAmOrPm() { 287 if (mCurrentHoursOfDay < 12) { 288 return AM; 289 } else if (mCurrentHoursOfDay < 24) { 290 return PM; 291 } 292 return -1; 293 } 294 295 /** 296 * Set the internal value for the hour, minute, or AM/PM. 297 */ setValueForItem(int index, int value)298 private void setValueForItem(int index, int value) { 299 if (index == HOUR_INDEX) { 300 mCurrentHoursOfDay = value; 301 } else if (index == MINUTE_INDEX){ 302 mCurrentMinutes = value; 303 } else if (index == AMPM_INDEX) { 304 if (value == AM) { 305 mCurrentHoursOfDay = mCurrentHoursOfDay % 12; 306 } else if (value == PM) { 307 mCurrentHoursOfDay = (mCurrentHoursOfDay % 12) + 12; 308 } 309 } 310 } 311 312 /** 313 * Set the internal value as either AM or PM, and update the AM/PM circle displays. 314 * @param amOrPm 315 */ setAmOrPm(int amOrPm)316 public void setAmOrPm(int amOrPm) { 317 mAmPmCirclesView.setAmOrPm(amOrPm); 318 mAmPmCirclesView.invalidate(); 319 setValueForItem(AMPM_INDEX, amOrPm); 320 } 321 322 /** 323 * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger 324 * selectable area to each of the 12 visible values, such that the ratio of space apportioned 325 * to a visible value : space apportioned to a non-visible value will be 14 : 4. 326 * E.g. the output of 30 degrees should have a higher range of input associated with it than 327 * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock 328 * circle (5 on the minutes, 1 or 13 on the hours). 329 */ preparePrefer30sMap()330 private void preparePrefer30sMap() { 331 // We'll split up the visible output and the non-visible output such that each visible 332 // output will correspond to a range of 14 associated input degrees, and each non-visible 333 // output will correspond to a range of 4 associate input degrees, so visible numbers 334 // are more than 3 times easier to get than non-visible numbers: 335 // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc. 336 // 337 // If an output of 30 degrees should correspond to a range of 14 associated degrees, then 338 // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should 339 // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you 340 // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this 341 // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the 342 // ability to aggressively prefer the visible values by a factor of more than 3:1, which 343 // greatly contributes to the selectability of these values. 344 345 // Our input will be 0 through 360. 346 mSnapPrefer30sMap = new int[361]; 347 348 // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}. 349 int snappedOutputDegrees = 0; 350 // Count of how many inputs we've designated to the specified output. 351 int count = 1; 352 // How many input we expect for a specified output. This will be 14 for output divisible 353 // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so 354 // the caller can decide which they need. 355 int expectedCount = 8; 356 // Iterate through the input. 357 for (int degrees = 0; degrees < 361; degrees++) { 358 // Save the input-output mapping. 359 mSnapPrefer30sMap[degrees] = snappedOutputDegrees; 360 // If this is the last input for the specified output, calculate the next output and 361 // the next expected count. 362 if (count == expectedCount) { 363 snappedOutputDegrees += 6; 364 if (snappedOutputDegrees == 360) { 365 expectedCount = 7; 366 } else if (snappedOutputDegrees % 30 == 0) { 367 expectedCount = 14; 368 } else { 369 expectedCount = 4; 370 } 371 count = 1; 372 } else { 373 count++; 374 } 375 } 376 } 377 378 /** 379 * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees, 380 * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be 381 * weighted heavier than the degrees corresponding to non-visible numbers. 382 * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the 383 * mapping. 384 */ snapPrefer30s(int degrees)385 private int snapPrefer30s(int degrees) { 386 if (mSnapPrefer30sMap == null) { 387 return -1; 388 } 389 return mSnapPrefer30sMap[degrees]; 390 } 391 392 /** 393 * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all 394 * multiples of 30), where the input will be "snapped" to the closest visible degrees. 395 * @param degrees The input degrees 396 * @param forceAboveOrBelow The output may be forced to either the higher or lower step, or may 397 * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force 398 * strictly lower, and 0 to snap to the closer one. 399 * @return output degrees, will be a multiple of 30 400 */ snapOnly30s(int degrees, int forceHigherOrLower)401 private static int snapOnly30s(int degrees, int forceHigherOrLower) { 402 int stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; 403 int floor = (degrees / stepSize) * stepSize; 404 int ceiling = floor + stepSize; 405 if (forceHigherOrLower == 1) { 406 degrees = ceiling; 407 } else if (forceHigherOrLower == -1) { 408 if (degrees == floor) { 409 floor -= stepSize; 410 } 411 degrees = floor; 412 } else { 413 if ((degrees - floor) < (ceiling - degrees)) { 414 degrees = floor; 415 } else { 416 degrees = ceiling; 417 } 418 } 419 return degrees; 420 } 421 422 /** 423 * For the currently showing view (either hours or minutes), re-calculate the position for the 424 * selector, and redraw it at that position. The input degrees will be snapped to a selectable 425 * value. 426 * @param degrees Degrees which should be selected. 427 * @param isInnerCircle Whether the selection should be in the inner circle; will be ignored 428 * if there is no inner circle. 429 * @param forceToVisibleValue Even if the currently-showing circle allows for fine-grained 430 * selection (i.e. minutes), force the selection to one of the visibly-showing values. 431 * @param forceDrawDot The dot in the circle will generally only be shown when the selection 432 * is on non-visible values, but use this to force the dot to be shown. 433 * @return The value that was selected, i.e. 0-23 for hours, 0-59 for minutes. 434 */ reselectSelector(int degrees, boolean isInnerCircle, boolean forceToVisibleValue, boolean forceDrawDot)435 private int reselectSelector(int degrees, boolean isInnerCircle, 436 boolean forceToVisibleValue, boolean forceDrawDot) { 437 if (degrees == -1) { 438 return -1; 439 } 440 int currentShowing = getCurrentItemShowing(); 441 442 int stepSize; 443 boolean allowFineGrained = !forceToVisibleValue && (currentShowing == MINUTE_INDEX); 444 if (allowFineGrained) { 445 degrees = snapPrefer30s(degrees); 446 } else { 447 degrees = snapOnly30s(degrees, 0); 448 } 449 450 RadialSelectorView radialSelectorView; 451 if (currentShowing == HOUR_INDEX) { 452 radialSelectorView = mHourRadialSelectorView; 453 stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; 454 } else { 455 radialSelectorView = mMinuteRadialSelectorView; 456 stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE; 457 } 458 radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawDot); 459 radialSelectorView.invalidate(); 460 461 462 if (currentShowing == HOUR_INDEX) { 463 if (mIs24HourMode) { 464 if (degrees == 0 && isInnerCircle) { 465 degrees = 360; 466 } else if (degrees == 360 && !isInnerCircle) { 467 degrees = 0; 468 } 469 } else if (degrees == 0) { 470 degrees = 360; 471 } 472 } else if (degrees == 360 && currentShowing == MINUTE_INDEX) { 473 degrees = 0; 474 } 475 476 int value = degrees / stepSize; 477 if (currentShowing == HOUR_INDEX && mIs24HourMode && !isInnerCircle && degrees != 0) { 478 value += 12; 479 } 480 return value; 481 } 482 483 /** 484 * Calculate the degrees within the circle that corresponds to the specified coordinates, if 485 * the coordinates are within the range that will trigger a selection. 486 * @param pointX The x coordinate. 487 * @param pointY The y coordinate. 488 * @param forceLegal Force the selection to be legal, regardless of how far the coordinates are 489 * from the actual numbers. 490 * @param isInnerCircle If the selection may be in the inner circle, pass in a size-1 boolean 491 * array here, inside which the value will be true if the selection is in the inner circle, 492 * and false if in the outer circle. 493 * @return Degrees from 0 to 360, if the selection was within the legal range. -1 if not. 494 */ getDegreesFromCoords(float pointX, float pointY, boolean forceLegal, final Boolean[] isInnerCircle)495 private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal, 496 final Boolean[] isInnerCircle) { 497 int currentItem = getCurrentItemShowing(); 498 if (currentItem == HOUR_INDEX) { 499 return mHourRadialSelectorView.getDegreesFromCoords( 500 pointX, pointY, forceLegal, isInnerCircle); 501 } else if (currentItem == MINUTE_INDEX) { 502 return mMinuteRadialSelectorView.getDegreesFromCoords( 503 pointX, pointY, forceLegal, isInnerCircle); 504 } else { 505 return -1; 506 } 507 } 508 509 /** 510 * Get the item (hours or minutes) that is currently showing. 511 */ getCurrentItemShowing()512 public int getCurrentItemShowing() { 513 if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX) { 514 Log.e(TAG, "Current item showing was unfortunately set to "+mCurrentItemShowing); 515 return -1; 516 } 517 return mCurrentItemShowing; 518 } 519 520 /** 521 * Set either minutes or hours as showing. 522 * @param animate True to animate the transition, false to show with no animation. 523 */ setCurrentItemShowing(int index, boolean animate)524 public void setCurrentItemShowing(int index, boolean animate) { 525 if (index != HOUR_INDEX && index != MINUTE_INDEX) { 526 Log.e(TAG, "TimePicker does not support view at index "+index); 527 return; 528 } 529 530 int lastIndex = getCurrentItemShowing(); 531 mCurrentItemShowing = index; 532 533 if (animate && (index != lastIndex)) { 534 ObjectAnimator[] anims = new ObjectAnimator[4]; 535 if (index == MINUTE_INDEX) { 536 anims[0] = mHourRadialTextsView.getDisappearAnimator(); 537 anims[1] = mHourRadialSelectorView.getDisappearAnimator(); 538 anims[2] = mMinuteRadialTextsView.getReappearAnimator(); 539 anims[3] = mMinuteRadialSelectorView.getReappearAnimator(); 540 } else if (index == HOUR_INDEX){ 541 anims[0] = mHourRadialTextsView.getReappearAnimator(); 542 anims[1] = mHourRadialSelectorView.getReappearAnimator(); 543 anims[2] = mMinuteRadialTextsView.getDisappearAnimator(); 544 anims[3] = mMinuteRadialSelectorView.getDisappearAnimator(); 545 } 546 547 if (mTransition != null && mTransition.isRunning()) { 548 mTransition.end(); 549 } 550 mTransition = new AnimatorSet(); 551 mTransition.playTogether(anims); 552 mTransition.start(); 553 } else { 554 int hourAlpha = (index == HOUR_INDEX) ? 255 : 0; 555 int minuteAlpha = (index == MINUTE_INDEX) ? 255 : 0; 556 mHourRadialTextsView.setAlpha(hourAlpha); 557 mHourRadialSelectorView.setAlpha(hourAlpha); 558 mMinuteRadialTextsView.setAlpha(minuteAlpha); 559 mMinuteRadialSelectorView.setAlpha(minuteAlpha); 560 } 561 562 } 563 564 @Override onTouch(View v, MotionEvent event)565 public boolean onTouch(View v, MotionEvent event) { 566 final float eventX = event.getX(); 567 final float eventY = event.getY(); 568 int degrees; 569 int value; 570 final Boolean[] isInnerCircle = new Boolean[1]; 571 isInnerCircle[0] = false; 572 573 switch(event.getAction()) { 574 case MotionEvent.ACTION_DOWN: 575 if (!mInputEnabled) { 576 return true; 577 } 578 579 mDownX = eventX; 580 mDownY = eventY; 581 582 mLastValueSelected = -1; 583 mDoingMove = false; 584 mDoingTouch = true; 585 // If we're showing the AM/PM, check to see if the user is touching it. 586 if (!mHideAmPm) { 587 mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); 588 } else { 589 mIsTouchingAmOrPm = -1; 590 } 591 if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) { 592 // If the touch is on AM or PM, set it as "touched" after the TAP_TIMEOUT 593 // in case the user moves their finger quickly. 594 mHapticFeedbackController.tryVibrate(); 595 mDownDegrees = -1; 596 mHandler.postDelayed(new Runnable() { 597 @Override 598 public void run() { 599 mAmPmCirclesView.setAmOrPmPressed(mIsTouchingAmOrPm); 600 mAmPmCirclesView.invalidate(); 601 } 602 }, TAP_TIMEOUT); 603 } else { 604 // If we're in accessibility mode, force the touch to be legal. Otherwise, 605 // it will only register within the given touch target zone. 606 boolean forceLegal = mAccessibilityManager.isTouchExplorationEnabled(); 607 // Calculate the degrees that is currently being touched. 608 mDownDegrees = getDegreesFromCoords(eventX, eventY, forceLegal, isInnerCircle); 609 if (mDownDegrees != -1) { 610 // If it's a legal touch, set that number as "selected" after the 611 // TAP_TIMEOUT in case the user moves their finger quickly. 612 mHapticFeedbackController.tryVibrate(); 613 mHandler.postDelayed(new Runnable() { 614 @Override 615 public void run() { 616 mDoingMove = true; 617 int value = reselectSelector(mDownDegrees, isInnerCircle[0], 618 false, true); 619 mLastValueSelected = value; 620 mListener.onValueSelected(getCurrentItemShowing(), value, false); 621 } 622 }, TAP_TIMEOUT); 623 } 624 } 625 return true; 626 case MotionEvent.ACTION_MOVE: 627 if (!mInputEnabled) { 628 // We shouldn't be in this state, because input is disabled. 629 Log.e(TAG, "Input was disabled, but received ACTION_MOVE."); 630 return true; 631 } 632 633 float dY = Math.abs(eventY - mDownY); 634 float dX = Math.abs(eventX - mDownX); 635 636 if (!mDoingMove && dX <= TOUCH_SLOP && dY <= TOUCH_SLOP) { 637 // Hasn't registered down yet, just slight, accidental movement of finger. 638 break; 639 } 640 641 // If we're in the middle of touching down on AM or PM, check if we still are. 642 // If so, no-op. If not, remove its pressed state. Either way, no need to check 643 // for touches on the other circle. 644 if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) { 645 mHandler.removeCallbacksAndMessages(null); 646 int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); 647 if (isTouchingAmOrPm != mIsTouchingAmOrPm) { 648 mAmPmCirclesView.setAmOrPmPressed(-1); 649 mAmPmCirclesView.invalidate(); 650 mIsTouchingAmOrPm = -1; 651 } 652 break; 653 } 654 655 if (mDownDegrees == -1) { 656 // Original down was illegal, so no movement will register. 657 break; 658 } 659 660 // We're doing a move along the circle, so move the selection as appropriate. 661 mDoingMove = true; 662 mHandler.removeCallbacksAndMessages(null); 663 degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle); 664 if (degrees != -1) { 665 value = reselectSelector(degrees, isInnerCircle[0], false, true); 666 if (value != mLastValueSelected) { 667 mHapticFeedbackController.tryVibrate(); 668 mLastValueSelected = value; 669 mListener.onValueSelected(getCurrentItemShowing(), value, false); 670 } 671 } 672 return true; 673 case MotionEvent.ACTION_UP: 674 if (!mInputEnabled) { 675 // If our touch input was disabled, tell the listener to re-enable us. 676 Log.d(TAG, "Input was disabled, but received ACTION_UP."); 677 mListener.onValueSelected(ENABLE_PICKER_INDEX, 1, false); 678 return true; 679 } 680 681 mHandler.removeCallbacksAndMessages(null); 682 mDoingTouch = false; 683 684 // If we're touching AM or PM, set it as selected, and tell the listener. 685 if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) { 686 int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY); 687 mAmPmCirclesView.setAmOrPmPressed(-1); 688 mAmPmCirclesView.invalidate(); 689 690 if (isTouchingAmOrPm == mIsTouchingAmOrPm) { 691 mAmPmCirclesView.setAmOrPm(isTouchingAmOrPm); 692 if (getIsCurrentlyAmOrPm() != isTouchingAmOrPm) { 693 mListener.onValueSelected(AMPM_INDEX, mIsTouchingAmOrPm, false); 694 setValueForItem(AMPM_INDEX, isTouchingAmOrPm); 695 } 696 } 697 mIsTouchingAmOrPm = -1; 698 break; 699 } 700 701 // If we have a legal degrees selected, set the value and tell the listener. 702 if (mDownDegrees != -1) { 703 degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle); 704 if (degrees != -1) { 705 value = reselectSelector(degrees, isInnerCircle[0], !mDoingMove, false); 706 if (getCurrentItemShowing() == HOUR_INDEX && !mIs24HourMode) { 707 int amOrPm = getIsCurrentlyAmOrPm(); 708 if (amOrPm == AM && value == 12) { 709 value = 0; 710 } else if (amOrPm == PM && value != 12) { 711 value += 12; 712 } 713 } 714 setValueForItem(getCurrentItemShowing(), value); 715 mListener.onValueSelected(getCurrentItemShowing(), value, true); 716 } 717 } 718 mDoingMove = false; 719 return true; 720 default: 721 break; 722 } 723 return false; 724 } 725 726 /** 727 * Set touch input as enabled or disabled, for use with keyboard mode. 728 */ trySettingInputEnabled(boolean inputEnabled)729 public boolean trySettingInputEnabled(boolean inputEnabled) { 730 if (mDoingTouch && !inputEnabled) { 731 // If we're trying to disable input, but we're in the middle of a touch event, 732 // we'll allow the touch event to continue before disabling input. 733 return false; 734 } 735 mInputEnabled = inputEnabled; 736 mGrayBox.setVisibility(inputEnabled? View.INVISIBLE : View.VISIBLE); 737 return true; 738 } 739 740 /** 741 * Necessary for accessibility, to ensure we support "scrolling" forward and backward 742 * in the circle. 743 */ 744 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)745 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 746 super.onInitializeAccessibilityNodeInfo(info); 747 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 748 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 749 } 750 751 /** 752 * Announce the currently-selected time when launched. 753 */ 754 @Override dispatchPopulateAccessibilityEvent(AccessibilityEvent event)755 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 756 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 757 // Clear the event's current text so that only the current time will be spoken. 758 event.getText().clear(); 759 Time time = new Time(); 760 time.hour = getHours(); 761 time.minute = getMinutes(); 762 long millis = time.normalize(true); 763 int flags = DateUtils.FORMAT_SHOW_TIME; 764 if (mIs24HourMode) { 765 flags |= DateUtils.FORMAT_24HOUR; 766 } 767 String timeString = DateUtils.formatDateTime(getContext(), millis, flags); 768 event.getText().add(timeString); 769 return true; 770 } 771 return super.dispatchPopulateAccessibilityEvent(event); 772 } 773 774 /** 775 * When scroll forward/backward events are received, jump the time to the higher/lower 776 * discrete, visible value on the circle. 777 */ 778 @SuppressLint("NewApi") 779 @Override performAccessibilityAction(int action, Bundle arguments)780 public boolean performAccessibilityAction(int action, Bundle arguments) { 781 if (super.performAccessibilityAction(action, arguments)) { 782 return true; 783 } 784 785 int changeMultiplier = 0; 786 if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { 787 changeMultiplier = 1; 788 } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { 789 changeMultiplier = -1; 790 } 791 if (changeMultiplier != 0) { 792 int value = getCurrentlyShowingValue(); 793 int stepSize = 0; 794 int currentItemShowing = getCurrentItemShowing(); 795 if (currentItemShowing == HOUR_INDEX) { 796 stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE; 797 value %= 12; 798 } else if (currentItemShowing == MINUTE_INDEX) { 799 stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE; 800 } 801 802 int degrees = value * stepSize; 803 degrees = snapOnly30s(degrees, changeMultiplier); 804 value = degrees / stepSize; 805 int maxValue = 0; 806 int minValue = 0; 807 if (currentItemShowing == HOUR_INDEX) { 808 if (mIs24HourMode) { 809 maxValue = 23; 810 } else { 811 maxValue = 12; 812 minValue = 1; 813 } 814 } else { 815 maxValue = 55; 816 } 817 if (value > maxValue) { 818 // If we scrolled forward past the highest number, wrap around to the lowest. 819 value = minValue; 820 } else if (value < minValue) { 821 // If we scrolled backward past the lowest number, wrap around to the highest. 822 value = maxValue; 823 } 824 setItem(currentItemShowing, value); 825 mListener.onValueSelected(currentItemShowing, value, false); 826 return true; 827 } 828 829 return false; 830 } 831 } 832