1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorSet; 21 import android.animation.Keyframe; 22 import android.animation.ObjectAnimator; 23 import android.animation.PropertyValuesHolder; 24 import android.animation.ValueAnimator; 25 import android.content.Context; 26 import android.content.res.ColorStateList; 27 import android.content.res.Resources; 28 import android.content.res.TypedArray; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.Paint; 32 import android.graphics.Path; 33 import android.graphics.Rect; 34 import android.graphics.Region; 35 import android.graphics.Typeface; 36 import android.os.Bundle; 37 import android.util.AttributeSet; 38 import android.util.IntArray; 39 import android.util.Log; 40 import android.util.MathUtils; 41 import android.util.StateSet; 42 import android.util.TypedValue; 43 import android.view.HapticFeedbackConstants; 44 import android.view.MotionEvent; 45 import android.view.View; 46 import android.view.accessibility.AccessibilityEvent; 47 import android.view.accessibility.AccessibilityNodeInfo; 48 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 49 50 import com.android.internal.R; 51 import com.android.internal.widget.ExploreByTouchHelper; 52 53 import java.util.ArrayList; 54 import java.util.Calendar; 55 import java.util.Locale; 56 57 /** 58 * View to show a clock circle picker (with one or two picking circles) 59 * 60 * @hide 61 */ 62 public class RadialTimePickerView extends View { 63 private static final String TAG = "RadialTimePickerView"; 64 65 private static final int HOURS = 0; 66 private static final int MINUTES = 1; 67 private static final int HOURS_INNER = 2; 68 69 private static final int SELECTOR_CIRCLE = 0; 70 private static final int SELECTOR_DOT = 1; 71 private static final int SELECTOR_LINE = 2; 72 73 private static final int AM = 0; 74 private static final int PM = 1; 75 76 // Opaque alpha level 77 private static final int ALPHA_OPAQUE = 255; 78 79 // Transparent alpha level 80 private static final int ALPHA_TRANSPARENT = 0; 81 82 private static final int HOURS_IN_CIRCLE = 12; 83 private static final int MINUTES_IN_CIRCLE = 60; 84 private static final int DEGREES_FOR_ONE_HOUR = 360 / HOURS_IN_CIRCLE; 85 private static final int DEGREES_FOR_ONE_MINUTE = 360 / MINUTES_IN_CIRCLE; 86 87 private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; 88 private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; 89 private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}; 90 91 private static final int FADE_OUT_DURATION = 500; 92 private static final int FADE_IN_DURATION = 500; 93 94 private static final int[] SNAP_PREFER_30S_MAP = new int[361]; 95 96 private static final int NUM_POSITIONS = 12; 97 private static final float[] COS_30 = new float[NUM_POSITIONS]; 98 private static final float[] SIN_30 = new float[NUM_POSITIONS]; 99 100 static { 101 // Prepare mapping to snap touchable degrees to selectable degrees. 102 preparePrefer30sMap(); 103 104 final double increment = 2.0 * Math.PI / NUM_POSITIONS; 105 double angle = Math.PI / 2.0; 106 for (int i = 0; i < NUM_POSITIONS; i++) { 107 COS_30[i] = (float) Math.cos(angle); 108 SIN_30[i] = (float) Math.sin(angle); 109 angle += increment; 110 } 111 } 112 113 private final InvalidateUpdateListener mInvalidateUpdateListener = 114 new InvalidateUpdateListener(); 115 116 private final String[] mHours12Texts = new String[12]; 117 private final String[] mOuterHours24Texts = new String[12]; 118 private final String[] mInnerHours24Texts = new String[12]; 119 private final String[] mMinutesTexts = new String[12]; 120 121 private final Paint[] mPaint = new Paint[2]; 122 private final IntHolder[] mAlpha = new IntHolder[2]; 123 124 private final Paint mPaintCenter = new Paint(); 125 126 private final Paint[][] mPaintSelector = new Paint[2][3]; 127 128 private final int mSelectorColor; 129 private final int mSelectorDotColor; 130 131 private final Paint mPaintBackground = new Paint(); 132 133 private final Typeface mTypeface; 134 135 private final ColorStateList[] mTextColor = new ColorStateList[3]; 136 private final int[] mTextSize = new int[3]; 137 private final int[] mTextInset = new int[3]; 138 139 private final float[][] mOuterTextX = new float[2][12]; 140 private final float[][] mOuterTextY = new float[2][12]; 141 142 private final float[] mInnerTextX = new float[12]; 143 private final float[] mInnerTextY = new float[12]; 144 145 private final int[] mSelectionDegrees = new int[2]; 146 147 private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<>(); 148 private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<>(); 149 150 private final RadialPickerTouchHelper mTouchHelper; 151 152 private final Path mSelectorPath = new Path(); 153 154 private boolean mIs24HourMode; 155 private boolean mShowHours; 156 157 /** 158 * When in 24-hour mode, indicates that the current hour is between 159 * 1 and 12 (inclusive). 160 */ 161 private boolean mIsOnInnerCircle; 162 163 private int mSelectorRadius; 164 private int mSelectorStroke; 165 private int mSelectorDotRadius; 166 private int mCenterDotRadius; 167 168 private int mXCenter; 169 private int mYCenter; 170 private int mCircleRadius; 171 172 private int mMinDistForInnerNumber; 173 private int mMaxDistForOuterNumber; 174 private int mHalfwayDist; 175 176 private String[] mOuterTextHours; 177 private String[] mInnerTextHours; 178 private String[] mMinutesText; 179 private AnimatorSet mTransition; 180 181 private int mAmOrPm; 182 183 private float mDisabledAlpha; 184 185 private OnValueSelectedListener mListener; 186 187 private boolean mInputEnabled = true; 188 189 public interface OnValueSelectedListener { 190 void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); 191 } 192 193 /** 194 * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger 195 * selectable area to each of the 12 visible values, such that the ratio of space apportioned 196 * to a visible value : space apportioned to a non-visible value will be 14 : 4. 197 * E.g. the output of 30 degrees should have a higher range of input associated with it than 198 * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock 199 * circle (5 on the minutes, 1 or 13 on the hours). 200 */ 201 private static void preparePrefer30sMap() { 202 // We'll split up the visible output and the non-visible output such that each visible 203 // output will correspond to a range of 14 associated input degrees, and each non-visible 204 // output will correspond to a range of 4 associate input degrees, so visible numbers 205 // are more than 3 times easier to get than non-visible numbers: 206 // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc. 207 // 208 // If an output of 30 degrees should correspond to a range of 14 associated degrees, then 209 // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should 210 // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you 211 // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this 212 // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the 213 // ability to aggressively prefer the visible values by a factor of more than 3:1, which 214 // greatly contributes to the selectability of these values. 215 216 // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}. 217 int snappedOutputDegrees = 0; 218 // Count of how many inputs we've designated to the specified output. 219 int count = 1; 220 // How many input we expect for a specified output. This will be 14 for output divisible 221 // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so 222 // the caller can decide which they need. 223 int expectedCount = 8; 224 // Iterate through the input. 225 for (int degrees = 0; degrees < 361; degrees++) { 226 // Save the input-output mapping. 227 SNAP_PREFER_30S_MAP[degrees] = snappedOutputDegrees; 228 // If this is the last input for the specified output, calculate the next output and 229 // the next expected count. 230 if (count == expectedCount) { 231 snappedOutputDegrees += 6; 232 if (snappedOutputDegrees == 360) { 233 expectedCount = 7; 234 } else if (snappedOutputDegrees % 30 == 0) { 235 expectedCount = 14; 236 } else { 237 expectedCount = 4; 238 } 239 count = 1; 240 } else { 241 count++; 242 } 243 } 244 } 245 246 /** 247 * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees, 248 * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be 249 * weighted heavier than the degrees corresponding to non-visible numbers. 250 * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the 251 * mapping. 252 */ 253 private static int snapPrefer30s(int degrees) { 254 if (SNAP_PREFER_30S_MAP == null) { 255 return -1; 256 } 257 return SNAP_PREFER_30S_MAP[degrees]; 258 } 259 260 /** 261 * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all 262 * multiples of 30), where the input will be "snapped" to the closest visible degrees. 263 * @param degrees The input degrees 264 * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may 265 * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force 266 * strictly lower, and 0 to snap to the closer one. 267 * @return output degrees, will be a multiple of 30 268 */ 269 private static int snapOnly30s(int degrees, int forceHigherOrLower) { 270 final int stepSize = DEGREES_FOR_ONE_HOUR; 271 int floor = (degrees / stepSize) * stepSize; 272 final int ceiling = floor + stepSize; 273 if (forceHigherOrLower == 1) { 274 degrees = ceiling; 275 } else if (forceHigherOrLower == -1) { 276 if (degrees == floor) { 277 floor -= stepSize; 278 } 279 degrees = floor; 280 } else { 281 if ((degrees - floor) < (ceiling - degrees)) { 282 degrees = floor; 283 } else { 284 degrees = ceiling; 285 } 286 } 287 return degrees; 288 } 289 290 @SuppressWarnings("unused") 291 public RadialTimePickerView(Context context) { 292 this(context, null); 293 } 294 295 public RadialTimePickerView(Context context, AttributeSet attrs) { 296 this(context, attrs, R.attr.timePickerStyle); 297 } 298 299 public RadialTimePickerView(Context context, AttributeSet attrs, int defStyleAttr) { 300 this(context, attrs, defStyleAttr, 0); 301 } 302 303 public RadialTimePickerView( 304 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 305 super(context, attrs); 306 307 // Pull disabled alpha from theme. 308 final TypedValue outValue = new TypedValue(); 309 context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); 310 mDisabledAlpha = outValue.getFloat(); 311 312 // process style attributes 313 final Resources res = getResources(); 314 final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker, 315 defStyleAttr, defStyleRes); 316 317 mTypeface = Typeface.create("sans-serif", Typeface.NORMAL); 318 319 // Initialize all alpha values to opaque. 320 for (int i = 0; i < mAlpha.length; i++) { 321 mAlpha[i] = new IntHolder(ALPHA_OPAQUE); 322 } 323 324 mTextColor[HOURS] = a.getColorStateList(R.styleable.TimePicker_numbersTextColor); 325 mTextColor[HOURS_INNER] = a.getColorStateList(R.styleable.TimePicker_numbersInnerTextColor); 326 mTextColor[MINUTES] = mTextColor[HOURS]; 327 328 mPaint[HOURS] = new Paint(); 329 mPaint[HOURS].setAntiAlias(true); 330 mPaint[HOURS].setTextAlign(Paint.Align.CENTER); 331 332 mPaint[MINUTES] = new Paint(); 333 mPaint[MINUTES].setAntiAlias(true); 334 mPaint[MINUTES].setTextAlign(Paint.Align.CENTER); 335 336 final ColorStateList selectorColors = a.getColorStateList( 337 R.styleable.TimePicker_numbersSelectorColor); 338 final int selectorActivatedColor = selectorColors.getColorForState( 339 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0); 340 341 mPaintCenter.setColor(selectorActivatedColor); 342 mPaintCenter.setAntiAlias(true); 343 344 final int[] activatedStateSet = StateSet.get( 345 StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED); 346 347 mSelectorColor = selectorActivatedColor; 348 mSelectorDotColor = mTextColor[HOURS].getColorForState(activatedStateSet, 0); 349 350 mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint(); 351 mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true); 352 353 mPaintSelector[HOURS][SELECTOR_DOT] = new Paint(); 354 mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true); 355 356 mPaintSelector[HOURS][SELECTOR_LINE] = new Paint(); 357 mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true); 358 mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2); 359 360 mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint(); 361 mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true); 362 363 mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint(); 364 mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true); 365 366 mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint(); 367 mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true); 368 mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2); 369 370 mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor, 371 context.getColor(R.color.timepicker_default_numbers_background_color_material))); 372 mPaintBackground.setAntiAlias(true); 373 374 mSelectorRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_radius); 375 mSelectorStroke = res.getDimensionPixelSize(R.dimen.timepicker_selector_stroke); 376 mSelectorDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_dot_radius); 377 mCenterDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_center_dot_radius); 378 379 mTextSize[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal); 380 mTextSize[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal); 381 mTextSize[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_inner); 382 383 mTextInset[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal); 384 mTextInset[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal); 385 mTextInset[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_inner); 386 387 mShowHours = true; 388 mIs24HourMode = false; 389 mAmOrPm = AM; 390 391 // Set up accessibility components. 392 mTouchHelper = new RadialPickerTouchHelper(); 393 setAccessibilityDelegate(mTouchHelper); 394 395 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 396 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 397 } 398 399 initHoursAndMinutesText(); 400 initData(); 401 402 a.recycle(); 403 404 // Initial values 405 final Calendar calendar = Calendar.getInstance(Locale.getDefault()); 406 final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); 407 final int currentMinute = calendar.get(Calendar.MINUTE); 408 409 setCurrentHourInternal(currentHour, false, false); 410 setCurrentMinuteInternal(currentMinute, false); 411 412 setHapticFeedbackEnabled(true); 413 } 414 415 public void initialize(int hour, int minute, boolean is24HourMode) { 416 if (mIs24HourMode != is24HourMode) { 417 mIs24HourMode = is24HourMode; 418 initData(); 419 } 420 421 setCurrentHourInternal(hour, false, false); 422 setCurrentMinuteInternal(minute, false); 423 } 424 425 public void setCurrentItemShowing(int item, boolean animate) { 426 switch (item){ 427 case HOURS: 428 showHours(animate); 429 break; 430 case MINUTES: 431 showMinutes(animate); 432 break; 433 default: 434 Log.e(TAG, "ClockView does not support showing item " + item); 435 } 436 } 437 438 public int getCurrentItemShowing() { 439 return mShowHours ? HOURS : MINUTES; 440 } 441 442 public void setOnValueSelectedListener(OnValueSelectedListener listener) { 443 mListener = listener; 444 } 445 446 /** 447 * Sets the current hour in 24-hour time. 448 * 449 * @param hour the current hour between 0 and 23 (inclusive) 450 */ 451 public void setCurrentHour(int hour) { 452 setCurrentHourInternal(hour, true, false); 453 } 454 455 /** 456 * Sets the current hour. 457 * 458 * @param hour The current hour 459 * @param callback Whether the value listener should be invoked 460 * @param autoAdvance Whether the listener should auto-advance to the next 461 * selection mode, e.g. hour to minutes 462 */ 463 private void setCurrentHourInternal(int hour, boolean callback, boolean autoAdvance) { 464 final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR; 465 mSelectionDegrees[HOURS] = degrees; 466 467 // 0 is 12 AM (midnight) and 12 is 12 PM (noon). 468 final int amOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM; 469 final boolean isOnInnerCircle = getInnerCircleForHour(hour); 470 if (mAmOrPm != amOrPm || mIsOnInnerCircle != isOnInnerCircle) { 471 mAmOrPm = amOrPm; 472 mIsOnInnerCircle = isOnInnerCircle; 473 474 initData(); 475 mTouchHelper.invalidateRoot(); 476 } 477 478 invalidate(); 479 480 if (callback && mListener != null) { 481 mListener.onValueSelected(HOURS, hour, autoAdvance); 482 } 483 } 484 485 /** 486 * Returns the current hour in 24-hour time. 487 * 488 * @return the current hour between 0 and 23 (inclusive) 489 */ 490 public int getCurrentHour() { 491 return getHourForDegrees(mSelectionDegrees[HOURS], mIsOnInnerCircle); 492 } 493 494 private int getHourForDegrees(int degrees, boolean innerCircle) { 495 int hour = (degrees / DEGREES_FOR_ONE_HOUR) % 12; 496 if (mIs24HourMode) { 497 // Convert the 12-hour value into 24-hour time based on where the 498 // selector is positioned. 499 if (!innerCircle && hour == 0) { 500 // Outer circle is 1 through 12. 501 hour = 12; 502 } else if (innerCircle && hour != 0) { 503 // Inner circle is 13 through 23 and 0. 504 hour += 12; 505 } 506 } else if (mAmOrPm == PM) { 507 hour += 12; 508 } 509 return hour; 510 } 511 512 /** 513 * @param hour the hour in 24-hour time or 12-hour time 514 */ 515 private int getDegreesForHour(int hour) { 516 // Convert to be 0-11. 517 if (mIs24HourMode) { 518 if (hour >= 12) { 519 hour -= 12; 520 } 521 } else if (hour == 12) { 522 hour = 0; 523 } 524 return hour * DEGREES_FOR_ONE_HOUR; 525 } 526 527 /** 528 * @param hour the hour in 24-hour time or 12-hour time 529 */ 530 private boolean getInnerCircleForHour(int hour) { 531 return mIs24HourMode && (hour == 0 || hour > 12); 532 } 533 534 public void setCurrentMinute(int minute) { 535 setCurrentMinuteInternal(minute, true); 536 } 537 538 private void setCurrentMinuteInternal(int minute, boolean callback) { 539 mSelectionDegrees[MINUTES] = (minute % MINUTES_IN_CIRCLE) * DEGREES_FOR_ONE_MINUTE; 540 541 invalidate(); 542 543 if (callback && mListener != null) { 544 mListener.onValueSelected(MINUTES, minute, false); 545 } 546 } 547 548 // Returns minutes in 0-59 range 549 public int getCurrentMinute() { 550 return getMinuteForDegrees(mSelectionDegrees[MINUTES]); 551 } 552 553 private int getMinuteForDegrees(int degrees) { 554 return degrees / DEGREES_FOR_ONE_MINUTE; 555 } 556 557 private int getDegreesForMinute(int minute) { 558 return minute * DEGREES_FOR_ONE_MINUTE; 559 } 560 561 public void setAmOrPm(int val) { 562 mAmOrPm = (val % 2); 563 invalidate(); 564 mTouchHelper.invalidateRoot(); 565 } 566 567 public int getAmOrPm() { 568 return mAmOrPm; 569 } 570 571 public void showHours(boolean animate) { 572 if (mShowHours) { 573 return; 574 } 575 576 mShowHours = true; 577 578 if (animate) { 579 startMinutesToHoursAnimation(); 580 } 581 582 initData(); 583 invalidate(); 584 mTouchHelper.invalidateRoot(); 585 } 586 587 public void showMinutes(boolean animate) { 588 if (!mShowHours) { 589 return; 590 } 591 592 mShowHours = false; 593 594 if (animate) { 595 startHoursToMinutesAnimation(); 596 } 597 598 initData(); 599 invalidate(); 600 mTouchHelper.invalidateRoot(); 601 } 602 603 private void initHoursAndMinutesText() { 604 // Initialize the hours and minutes numbers. 605 for (int i = 0; i < 12; i++) { 606 mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]); 607 mInnerHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]); 608 mOuterHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]); 609 mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]); 610 } 611 } 612 613 private void initData() { 614 if (mIs24HourMode) { 615 mOuterTextHours = mOuterHours24Texts; 616 mInnerTextHours = mInnerHours24Texts; 617 } else { 618 mOuterTextHours = mHours12Texts; 619 mInnerTextHours = mHours12Texts; 620 } 621 622 mMinutesText = mMinutesTexts; 623 624 final int hoursAlpha = mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT; 625 mAlpha[HOURS].setValue(hoursAlpha); 626 627 final int minutesAlpha = mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE; 628 mAlpha[MINUTES].setValue(minutesAlpha); 629 } 630 631 @Override 632 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 633 if (!changed) { 634 return; 635 } 636 637 mXCenter = getWidth() / 2; 638 mYCenter = getHeight() / 2; 639 mCircleRadius = Math.min(mXCenter, mYCenter); 640 641 mMinDistForInnerNumber = mCircleRadius - mTextInset[HOURS_INNER] - mSelectorRadius; 642 mMaxDistForOuterNumber = mCircleRadius - mTextInset[HOURS] + mSelectorRadius; 643 mHalfwayDist = mCircleRadius - (mTextInset[HOURS] + mTextInset[HOURS_INNER]) / 2; 644 645 calculatePositionsHours(); 646 calculatePositionsMinutes(); 647 648 mTouchHelper.invalidateRoot(); 649 } 650 651 @Override 652 public void onDraw(Canvas canvas) { 653 final float alphaMod = mInputEnabled ? 1 : mDisabledAlpha; 654 655 drawCircleBackground(canvas); 656 drawHours(canvas, alphaMod); 657 drawMinutes(canvas, alphaMod); 658 drawCenter(canvas, alphaMod); 659 } 660 661 private void drawCircleBackground(Canvas canvas) { 662 canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaintBackground); 663 } 664 665 private void drawHours(Canvas canvas, float alphaMod) { 666 final int hoursAlpha = (int) (mAlpha[HOURS].getValue() * alphaMod + 0.5f); 667 if (hoursAlpha > 0) { 668 // Draw the hour selector under the elements. 669 drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS, null, alphaMod); 670 671 // Draw outer hours. 672 drawTextElements(canvas, mTextSize[HOURS], mTypeface, mTextColor[HOURS], 673 mOuterTextHours, mOuterTextX[HOURS], mOuterTextY[HOURS], mPaint[HOURS], 674 hoursAlpha, !mIsOnInnerCircle, mSelectionDegrees[HOURS], false); 675 676 // Draw inner hours (13-00) for 24-hour time. 677 if (mIs24HourMode && mInnerTextHours != null) { 678 drawTextElements(canvas, mTextSize[HOURS_INNER], mTypeface, mTextColor[HOURS_INNER], 679 mInnerTextHours, mInnerTextX, mInnerTextY, mPaint[HOURS], hoursAlpha, 680 mIsOnInnerCircle, mSelectionDegrees[HOURS], false); 681 } 682 } 683 } 684 685 private void drawMinutes(Canvas canvas, float alphaMod) { 686 final int minutesAlpha = (int) (mAlpha[MINUTES].getValue() * alphaMod + 0.5f); 687 if (minutesAlpha > 0) { 688 // Draw the minute selector under the elements. 689 drawSelector(canvas, MINUTES, mSelectorPath, alphaMod); 690 691 // Exclude the selector region, then draw minutes with no 692 // activated states. 693 canvas.save(Canvas.CLIP_SAVE_FLAG); 694 canvas.clipPath(mSelectorPath, Region.Op.DIFFERENCE); 695 drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mTextColor[MINUTES], 696 mMinutesText, mOuterTextX[MINUTES], mOuterTextY[MINUTES], mPaint[MINUTES], 697 minutesAlpha, false, 0, false); 698 canvas.restore(); 699 700 // Intersect the selector region, then draw minutes with only 701 // activated states. 702 canvas.save(Canvas.CLIP_SAVE_FLAG); 703 canvas.clipPath(mSelectorPath, Region.Op.INTERSECT); 704 drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mTextColor[MINUTES], 705 mMinutesText, mOuterTextX[MINUTES], mOuterTextY[MINUTES], mPaint[MINUTES], 706 minutesAlpha, true, mSelectionDegrees[MINUTES], true); 707 canvas.restore(); 708 } 709 } 710 711 private void drawCenter(Canvas canvas, float alphaMod) { 712 mPaintCenter.setAlpha((int) (255 * alphaMod + 0.5f)); 713 canvas.drawCircle(mXCenter, mYCenter, mCenterDotRadius, mPaintCenter); 714 } 715 716 private int applyAlpha(int argb, int alpha) { 717 final int srcAlpha = (argb >> 24) & 0xFF; 718 final int dstAlpha = (int) (srcAlpha * (alpha / 255.0) + 0.5f); 719 return (0xFFFFFF & argb) | (dstAlpha << 24); 720 } 721 722 private int getMultipliedAlpha(int argb, int alpha) { 723 return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5); 724 } 725 726 private void drawSelector(Canvas canvas, int index, Path selectorPath, float alphaMod) { 727 final int alpha = (int) (mAlpha[index % 2].getValue() * alphaMod + 0.5f); 728 final int color = applyAlpha(mSelectorColor, alpha); 729 730 // Calculate the current radius at which to place the selection circle. 731 final int selRadius = mSelectorRadius; 732 final int selLength = mCircleRadius - mTextInset[index]; 733 final double selAngleRad = Math.toRadians(mSelectionDegrees[index % 2]); 734 final float selCenterX = mXCenter + selLength * (float) Math.sin(selAngleRad); 735 final float selCenterY = mYCenter - selLength * (float) Math.cos(selAngleRad); 736 737 // Draw the selection circle. 738 final Paint paint = mPaintSelector[index % 2][SELECTOR_CIRCLE]; 739 paint.setColor(color); 740 canvas.drawCircle(selCenterX, selCenterY, selRadius, paint); 741 742 // If needed, set up the clip path for later. 743 if (selectorPath != null) { 744 selectorPath.reset(); 745 selectorPath.addCircle(selCenterX, selCenterY, selRadius, Path.Direction.CCW); 746 } 747 748 // Draw the dot if we're between two items. 749 final boolean shouldDrawDot = mSelectionDegrees[index % 2] % 30 != 0; 750 if (shouldDrawDot) { 751 final Paint dotPaint = mPaintSelector[index % 2][SELECTOR_DOT]; 752 dotPaint.setColor(mSelectorDotColor); 753 canvas.drawCircle(selCenterX, selCenterY, mSelectorDotRadius, dotPaint); 754 } 755 756 // Shorten the line to only go from the edge of the center dot to the 757 // edge of the selection circle. 758 final double sin = Math.sin(selAngleRad); 759 final double cos = Math.cos(selAngleRad); 760 final int lineLength = selLength - selRadius; 761 final int centerX = mXCenter + (int) (mCenterDotRadius * sin); 762 final int centerY = mYCenter - (int) (mCenterDotRadius * cos); 763 final float linePointX = centerX + (int) (lineLength * sin); 764 final float linePointY = centerY - (int) (lineLength * cos); 765 766 // Draw the line. 767 final Paint linePaint = mPaintSelector[index % 2][SELECTOR_LINE]; 768 linePaint.setColor(color); 769 linePaint.setStrokeWidth(mSelectorStroke); 770 canvas.drawLine(mXCenter, mYCenter, linePointX, linePointY, linePaint); 771 } 772 773 private void calculatePositionsHours() { 774 // Calculate the text positions 775 final float numbersRadius = mCircleRadius - mTextInset[HOURS]; 776 777 // Calculate the positions for the 12 numbers in the main circle. 778 calculatePositions(mPaint[HOURS], numbersRadius, mXCenter, mYCenter, 779 mTextSize[HOURS], mOuterTextX[HOURS], mOuterTextY[HOURS]); 780 781 // If we have an inner circle, calculate those positions too. 782 if (mIs24HourMode) { 783 final int innerNumbersRadius = mCircleRadius - mTextInset[HOURS_INNER]; 784 calculatePositions(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter, 785 mTextSize[HOURS_INNER], mInnerTextX, mInnerTextY); 786 } 787 } 788 789 private void calculatePositionsMinutes() { 790 // Calculate the text positions 791 final float numbersRadius = mCircleRadius - mTextInset[MINUTES]; 792 793 // Calculate the positions for the 12 numbers in the main circle. 794 calculatePositions(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter, 795 mTextSize[MINUTES], mOuterTextX[MINUTES], mOuterTextY[MINUTES]); 796 } 797 798 /** 799 * Using the trigonometric Unit Circle, calculate the positions that the text will need to be 800 * drawn at based on the specified circle radius. Place the values in the textGridHeights and 801 * textGridWidths parameters. 802 */ 803 private static void calculatePositions(Paint paint, float radius, float xCenter, float yCenter, 804 float textSize, float[] x, float[] y) { 805 // Adjust yCenter to account for the text's baseline. 806 paint.setTextSize(textSize); 807 yCenter -= (paint.descent() + paint.ascent()) / 2; 808 809 for (int i = 0; i < NUM_POSITIONS; i++) { 810 x[i] = xCenter - radius * COS_30[i]; 811 y[i] = yCenter - radius * SIN_30[i]; 812 } 813 } 814 815 /** 816 * Draw the 12 text values at the positions specified by the textGrid parameters. 817 */ 818 private void drawTextElements(Canvas canvas, float textSize, Typeface typeface, 819 ColorStateList textColor, String[] texts, float[] textX, float[] textY, Paint paint, 820 int alpha, boolean showActivated, int activatedDegrees, boolean activatedOnly) { 821 paint.setTextSize(textSize); 822 paint.setTypeface(typeface); 823 824 // The activated index can touch a range of elements. 825 final float activatedIndex = activatedDegrees / (360.0f / NUM_POSITIONS); 826 final int activatedFloor = (int) activatedIndex; 827 final int activatedCeil = ((int) Math.ceil(activatedIndex)) % NUM_POSITIONS; 828 829 for (int i = 0; i < 12; i++) { 830 final boolean activated = (activatedFloor == i || activatedCeil == i); 831 if (activatedOnly && !activated) { 832 continue; 833 } 834 835 final int stateMask = StateSet.VIEW_STATE_ENABLED 836 | (showActivated && activated ? StateSet.VIEW_STATE_ACTIVATED : 0); 837 final int color = textColor.getColorForState(StateSet.get(stateMask), 0); 838 paint.setColor(color); 839 paint.setAlpha(getMultipliedAlpha(color, alpha)); 840 841 canvas.drawText(texts[i], textX[i], textY[i], paint); 842 } 843 } 844 845 private static ObjectAnimator getFadeOutAnimator(IntHolder target, int startAlpha, int endAlpha, 846 InvalidateUpdateListener updateListener) { 847 final ObjectAnimator animator = ObjectAnimator.ofInt(target, "value", startAlpha, endAlpha); 848 animator.setDuration(FADE_OUT_DURATION); 849 animator.addUpdateListener(updateListener); 850 return animator; 851 } 852 853 private static ObjectAnimator getFadeInAnimator(IntHolder target, int startAlpha, int endAlpha, 854 InvalidateUpdateListener updateListener) { 855 final float delayMultiplier = 0.25f; 856 final float transitionDurationMultiplier = 1f; 857 final float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; 858 final int totalDuration = (int) (FADE_IN_DURATION * totalDurationMultiplier); 859 final float delayPoint = (delayMultiplier * FADE_IN_DURATION) / totalDuration; 860 861 final Keyframe kf0, kf1, kf2; 862 kf0 = Keyframe.ofInt(0f, startAlpha); 863 kf1 = Keyframe.ofInt(delayPoint, startAlpha); 864 kf2 = Keyframe.ofInt(1f, endAlpha); 865 final PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("value", kf0, kf1, kf2); 866 867 final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(target, fadeIn); 868 animator.setDuration(totalDuration); 869 animator.addUpdateListener(updateListener); 870 return animator; 871 } 872 873 private class InvalidateUpdateListener implements ValueAnimator.AnimatorUpdateListener { 874 @Override 875 public void onAnimationUpdate(ValueAnimator animation) { 876 RadialTimePickerView.this.invalidate(); 877 } 878 } 879 880 private void startHoursToMinutesAnimation() { 881 if (mHoursToMinutesAnims.size() == 0) { 882 mHoursToMinutesAnims.add(getFadeOutAnimator(mAlpha[HOURS], 883 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 884 mHoursToMinutesAnims.add(getFadeInAnimator(mAlpha[MINUTES], 885 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); 886 } 887 888 if (mTransition != null && mTransition.isRunning()) { 889 mTransition.end(); 890 } 891 mTransition = new AnimatorSet(); 892 mTransition.playTogether(mHoursToMinutesAnims); 893 mTransition.start(); 894 } 895 896 private void startMinutesToHoursAnimation() { 897 if (mMinuteToHoursAnims.size() == 0) { 898 mMinuteToHoursAnims.add(getFadeOutAnimator(mAlpha[MINUTES], 899 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 900 mMinuteToHoursAnims.add(getFadeInAnimator(mAlpha[HOURS], 901 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); 902 } 903 904 if (mTransition != null && mTransition.isRunning()) { 905 mTransition.end(); 906 } 907 mTransition = new AnimatorSet(); 908 mTransition.playTogether(mMinuteToHoursAnims); 909 mTransition.start(); 910 } 911 912 private int getDegreesFromXY(float x, float y, boolean constrainOutside) { 913 // Ensure the point is inside the touchable area. 914 final int innerBound; 915 final int outerBound; 916 if (mIs24HourMode && mShowHours) { 917 innerBound = mMinDistForInnerNumber; 918 outerBound = mMaxDistForOuterNumber; 919 } else { 920 final int index = mShowHours ? HOURS : MINUTES; 921 final int center = mCircleRadius - mTextInset[index]; 922 innerBound = center - mSelectorRadius; 923 outerBound = center + mSelectorRadius; 924 } 925 926 final double dX = x - mXCenter; 927 final double dY = y - mYCenter; 928 final double distFromCenter = Math.sqrt(dX * dX + dY * dY); 929 if (distFromCenter < innerBound || constrainOutside && distFromCenter > outerBound) { 930 return -1; 931 } 932 933 // Convert to degrees. 934 final int degrees = (int) (Math.toDegrees(Math.atan2(dY, dX) + Math.PI / 2) + 0.5); 935 if (degrees < 0) { 936 return degrees + 360; 937 } else { 938 return degrees; 939 } 940 } 941 942 private boolean getInnerCircleFromXY(float x, float y) { 943 if (mIs24HourMode && mShowHours) { 944 final double dX = x - mXCenter; 945 final double dY = y - mYCenter; 946 final double distFromCenter = Math.sqrt(dX * dX + dY * dY); 947 return distFromCenter <= mHalfwayDist; 948 } 949 return false; 950 } 951 952 boolean mChangedDuringTouch = false; 953 954 @Override 955 public boolean onTouchEvent(MotionEvent event) { 956 if (!mInputEnabled) { 957 return true; 958 } 959 960 final int action = event.getActionMasked(); 961 if (action == MotionEvent.ACTION_MOVE 962 || action == MotionEvent.ACTION_UP 963 || action == MotionEvent.ACTION_DOWN) { 964 boolean forceSelection = false; 965 boolean autoAdvance = false; 966 967 if (action == MotionEvent.ACTION_DOWN) { 968 // This is a new event stream, reset whether the value changed. 969 mChangedDuringTouch = false; 970 } else if (action == MotionEvent.ACTION_UP) { 971 autoAdvance = true; 972 973 // If we saw a down/up pair without the value changing, assume 974 // this is a single-tap selection and force a change. 975 if (!mChangedDuringTouch) { 976 forceSelection = true; 977 } 978 } 979 980 mChangedDuringTouch |= handleTouchInput( 981 event.getX(), event.getY(), forceSelection, autoAdvance); 982 } 983 984 return true; 985 } 986 987 private boolean handleTouchInput( 988 float x, float y, boolean forceSelection, boolean autoAdvance) { 989 final boolean isOnInnerCircle = getInnerCircleFromXY(x, y); 990 final int degrees = getDegreesFromXY(x, y, false); 991 if (degrees == -1) { 992 return false; 993 } 994 995 final int type; 996 final int newValue; 997 final boolean valueChanged; 998 999 if (mShowHours) { 1000 final int snapDegrees = snapOnly30s(degrees, 0) % 360; 1001 valueChanged = mIsOnInnerCircle != isOnInnerCircle 1002 || mSelectionDegrees[HOURS] != snapDegrees; 1003 mIsOnInnerCircle = isOnInnerCircle; 1004 mSelectionDegrees[HOURS] = snapDegrees; 1005 type = HOURS; 1006 newValue = getCurrentHour(); 1007 } else { 1008 final int snapDegrees = snapPrefer30s(degrees) % 360; 1009 valueChanged = mSelectionDegrees[MINUTES] != snapDegrees; 1010 mSelectionDegrees[MINUTES] = snapDegrees; 1011 type = MINUTES; 1012 newValue = getCurrentMinute(); 1013 } 1014 1015 if (valueChanged || forceSelection || autoAdvance) { 1016 // Fire the listener even if we just need to auto-advance. 1017 if (mListener != null) { 1018 mListener.onValueSelected(type, newValue, autoAdvance); 1019 } 1020 1021 // Only provide feedback if the value actually changed. 1022 if (valueChanged || forceSelection) { 1023 performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); 1024 invalidate(); 1025 } 1026 return true; 1027 } 1028 1029 return false; 1030 } 1031 1032 @Override 1033 public boolean dispatchHoverEvent(MotionEvent event) { 1034 // First right-of-refusal goes the touch exploration helper. 1035 if (mTouchHelper.dispatchHoverEvent(event)) { 1036 return true; 1037 } 1038 return super.dispatchHoverEvent(event); 1039 } 1040 1041 public void setInputEnabled(boolean inputEnabled) { 1042 mInputEnabled = inputEnabled; 1043 invalidate(); 1044 } 1045 1046 private class RadialPickerTouchHelper extends ExploreByTouchHelper { 1047 private final Rect mTempRect = new Rect(); 1048 1049 private final int TYPE_HOUR = 1; 1050 private final int TYPE_MINUTE = 2; 1051 1052 private final int SHIFT_TYPE = 0; 1053 private final int MASK_TYPE = 0xF; 1054 1055 private final int SHIFT_VALUE = 8; 1056 private final int MASK_VALUE = 0xFF; 1057 1058 /** Increment in which virtual views are exposed for minutes. */ 1059 private final int MINUTE_INCREMENT = 5; 1060 1061 public RadialPickerTouchHelper() { 1062 super(RadialTimePickerView.this); 1063 } 1064 1065 @Override 1066 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 1067 super.onInitializeAccessibilityNodeInfo(host, info); 1068 1069 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); 1070 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); 1071 } 1072 1073 @Override 1074 public boolean performAccessibilityAction(View host, int action, Bundle arguments) { 1075 if (super.performAccessibilityAction(host, action, arguments)) { 1076 return true; 1077 } 1078 1079 switch (action) { 1080 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 1081 adjustPicker(1); 1082 return true; 1083 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: 1084 adjustPicker(-1); 1085 return true; 1086 } 1087 1088 return false; 1089 } 1090 1091 private void adjustPicker(int step) { 1092 final int stepSize; 1093 final int initialStep; 1094 final int maxValue; 1095 final int minValue; 1096 if (mShowHours) { 1097 stepSize = 1; 1098 1099 final int currentHour24 = getCurrentHour(); 1100 if (mIs24HourMode) { 1101 initialStep = currentHour24; 1102 minValue = 0; 1103 maxValue = 23; 1104 } else { 1105 initialStep = hour24To12(currentHour24); 1106 minValue = 1; 1107 maxValue = 12; 1108 } 1109 } else { 1110 stepSize = 5; 1111 initialStep = getCurrentMinute() / stepSize; 1112 minValue = 0; 1113 maxValue = 55; 1114 } 1115 1116 final int nextValue = (initialStep + step) * stepSize; 1117 final int clampedValue = MathUtils.constrain(nextValue, minValue, maxValue); 1118 if (mShowHours) { 1119 setCurrentHour(clampedValue); 1120 } else { 1121 setCurrentMinute(clampedValue); 1122 } 1123 } 1124 1125 @Override 1126 protected int getVirtualViewAt(float x, float y) { 1127 final int id; 1128 final int degrees = getDegreesFromXY(x, y, true); 1129 if (degrees != -1) { 1130 final int snapDegrees = snapOnly30s(degrees, 0) % 360; 1131 if (mShowHours) { 1132 final boolean isOnInnerCircle = getInnerCircleFromXY(x, y); 1133 final int hour24 = getHourForDegrees(snapDegrees, isOnInnerCircle); 1134 final int hour = mIs24HourMode ? hour24 : hour24To12(hour24); 1135 id = makeId(TYPE_HOUR, hour); 1136 } else { 1137 final int current = getCurrentMinute(); 1138 final int touched = getMinuteForDegrees(degrees); 1139 final int snapped = getMinuteForDegrees(snapDegrees); 1140 1141 // If the touched minute is closer to the current minute 1142 // than it is to the snapped minute, return current. 1143 final int currentOffset = getCircularDiff(current, touched, MINUTES_IN_CIRCLE); 1144 final int snappedOffset = getCircularDiff(snapped, touched, MINUTES_IN_CIRCLE); 1145 final int minute; 1146 if (currentOffset < snappedOffset) { 1147 minute = current; 1148 } else { 1149 minute = snapped; 1150 } 1151 id = makeId(TYPE_MINUTE, minute); 1152 } 1153 } else { 1154 id = INVALID_ID; 1155 } 1156 1157 return id; 1158 } 1159 1160 /** 1161 * Returns the difference in degrees between two values along a circle. 1162 * 1163 * @param first value in the range [0,max] 1164 * @param second value in the range [0,max] 1165 * @param max the maximum value along the circle 1166 * @return the difference in between the two values 1167 */ 1168 private int getCircularDiff(int first, int second, int max) { 1169 final int diff = Math.abs(first - second); 1170 final int midpoint = max / 2; 1171 return (diff > midpoint) ? (max - diff) : diff; 1172 } 1173 1174 @Override 1175 protected void getVisibleVirtualViews(IntArray virtualViewIds) { 1176 if (mShowHours) { 1177 final int min = mIs24HourMode ? 0 : 1; 1178 final int max = mIs24HourMode ? 23 : 12; 1179 for (int i = min; i <= max ; i++) { 1180 virtualViewIds.add(makeId(TYPE_HOUR, i)); 1181 } 1182 } else { 1183 final int current = getCurrentMinute(); 1184 for (int i = 0; i < MINUTES_IN_CIRCLE; i += MINUTE_INCREMENT) { 1185 virtualViewIds.add(makeId(TYPE_MINUTE, i)); 1186 1187 // If the current minute falls between two increments, 1188 // insert an extra node for it. 1189 if (current > i && current < i + MINUTE_INCREMENT) { 1190 virtualViewIds.add(makeId(TYPE_MINUTE, current)); 1191 } 1192 } 1193 } 1194 } 1195 1196 @Override 1197 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 1198 event.setClassName(getClass().getName()); 1199 1200 final int type = getTypeFromId(virtualViewId); 1201 final int value = getValueFromId(virtualViewId); 1202 final CharSequence description = getVirtualViewDescription(type, value); 1203 event.setContentDescription(description); 1204 } 1205 1206 @Override 1207 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { 1208 node.setClassName(getClass().getName()); 1209 node.addAction(AccessibilityAction.ACTION_CLICK); 1210 1211 final int type = getTypeFromId(virtualViewId); 1212 final int value = getValueFromId(virtualViewId); 1213 final CharSequence description = getVirtualViewDescription(type, value); 1214 node.setContentDescription(description); 1215 1216 getBoundsForVirtualView(virtualViewId, mTempRect); 1217 node.setBoundsInParent(mTempRect); 1218 1219 final boolean selected = isVirtualViewSelected(type, value); 1220 node.setSelected(selected); 1221 1222 final int nextId = getVirtualViewIdAfter(type, value); 1223 if (nextId != INVALID_ID) { 1224 node.setTraversalBefore(RadialTimePickerView.this, nextId); 1225 } 1226 } 1227 1228 private int getVirtualViewIdAfter(int type, int value) { 1229 if (type == TYPE_HOUR) { 1230 final int nextValue = value + 1; 1231 final int max = mIs24HourMode ? 23 : 12; 1232 if (nextValue <= max) { 1233 return makeId(type, nextValue); 1234 } 1235 } else if (type == TYPE_MINUTE) { 1236 final int current = getCurrentMinute(); 1237 final int snapValue = value - (value % MINUTE_INCREMENT); 1238 final int nextValue = snapValue + MINUTE_INCREMENT; 1239 if (value < current && nextValue > current) { 1240 // The current value is between two snap values. 1241 return makeId(type, current); 1242 } else if (nextValue < MINUTES_IN_CIRCLE) { 1243 return makeId(type, nextValue); 1244 } 1245 } 1246 return INVALID_ID; 1247 } 1248 1249 @Override 1250 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 1251 Bundle arguments) { 1252 if (action == AccessibilityNodeInfo.ACTION_CLICK) { 1253 final int type = getTypeFromId(virtualViewId); 1254 final int value = getValueFromId(virtualViewId); 1255 if (type == TYPE_HOUR) { 1256 final int hour = mIs24HourMode ? value : hour12To24(value, mAmOrPm); 1257 setCurrentHour(hour); 1258 return true; 1259 } else if (type == TYPE_MINUTE) { 1260 setCurrentMinute(value); 1261 return true; 1262 } 1263 } 1264 return false; 1265 } 1266 1267 private int hour12To24(int hour12, int amOrPm) { 1268 int hour24 = hour12; 1269 if (hour12 == 12) { 1270 if (amOrPm == AM) { 1271 hour24 = 0; 1272 } 1273 } else if (amOrPm == PM) { 1274 hour24 += 12; 1275 } 1276 return hour24; 1277 } 1278 1279 private int hour24To12(int hour24) { 1280 if (hour24 == 0) { 1281 return 12; 1282 } else if (hour24 > 12) { 1283 return hour24 - 12; 1284 } else { 1285 return hour24; 1286 } 1287 } 1288 1289 private void getBoundsForVirtualView(int virtualViewId, Rect bounds) { 1290 final float radius; 1291 final int type = getTypeFromId(virtualViewId); 1292 final int value = getValueFromId(virtualViewId); 1293 final float centerRadius; 1294 final float degrees; 1295 if (type == TYPE_HOUR) { 1296 final boolean innerCircle = getInnerCircleForHour(value); 1297 if (innerCircle) { 1298 centerRadius = mCircleRadius - mTextInset[HOURS_INNER]; 1299 radius = mSelectorRadius; 1300 } else { 1301 centerRadius = mCircleRadius - mTextInset[HOURS]; 1302 radius = mSelectorRadius; 1303 } 1304 1305 degrees = getDegreesForHour(value); 1306 } else if (type == TYPE_MINUTE) { 1307 centerRadius = mCircleRadius - mTextInset[MINUTES]; 1308 degrees = getDegreesForMinute(value); 1309 radius = mSelectorRadius; 1310 } else { 1311 // This should never happen. 1312 centerRadius = 0; 1313 degrees = 0; 1314 radius = 0; 1315 } 1316 1317 final double radians = Math.toRadians(degrees); 1318 final float xCenter = mXCenter + centerRadius * (float) Math.sin(radians); 1319 final float yCenter = mYCenter - centerRadius * (float) Math.cos(radians); 1320 1321 bounds.set((int) (xCenter - radius), (int) (yCenter - radius), 1322 (int) (xCenter + radius), (int) (yCenter + radius)); 1323 } 1324 1325 private CharSequence getVirtualViewDescription(int type, int value) { 1326 final CharSequence description; 1327 if (type == TYPE_HOUR || type == TYPE_MINUTE) { 1328 description = Integer.toString(value); 1329 } else { 1330 description = null; 1331 } 1332 return description; 1333 } 1334 1335 private boolean isVirtualViewSelected(int type, int value) { 1336 final boolean selected; 1337 if (type == TYPE_HOUR) { 1338 selected = getCurrentHour() == value; 1339 } else if (type == TYPE_MINUTE) { 1340 selected = getCurrentMinute() == value; 1341 } else { 1342 selected = false; 1343 } 1344 return selected; 1345 } 1346 1347 private int makeId(int type, int value) { 1348 return type << SHIFT_TYPE | value << SHIFT_VALUE; 1349 } 1350 1351 private int getTypeFromId(int id) { 1352 return id >>> SHIFT_TYPE & MASK_TYPE; 1353 } 1354 1355 private int getValueFromId(int id) { 1356 return id >>> SHIFT_VALUE & MASK_VALUE; 1357 } 1358 } 1359 1360 private static class IntHolder { 1361 private int mValue; 1362 1363 public IntHolder(int value) { 1364 mValue = value; 1365 } 1366 1367 public void setValue(int value) { 1368 mValue = value; 1369 } 1370 1371 public int getValue() { 1372 return mValue; 1373 } 1374 } 1375 } 1376