TimePickerClockDelegate.java revision f2525f6802219ad5e983128dc2f7b5a89a507d9c
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 android.widget; 18 19import android.annotation.Nullable; 20import android.content.Context; 21import android.content.res.ColorStateList; 22import android.content.res.Configuration; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.graphics.drawable.Drawable; 26import android.os.Parcel; 27import android.os.Parcelable; 28import android.text.format.DateFormat; 29import android.text.format.DateUtils; 30import android.util.AttributeSet; 31import android.util.Log; 32import android.util.StateSet; 33import android.util.TypedValue; 34import android.view.HapticFeedbackConstants; 35import android.view.KeyCharacterMap; 36import android.view.KeyEvent; 37import android.view.LayoutInflater; 38import android.view.View; 39import android.view.View.AccessibilityDelegate; 40import android.view.accessibility.AccessibilityEvent; 41import android.view.accessibility.AccessibilityNodeInfo; 42import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 43 44import com.android.internal.R; 45 46import java.util.ArrayList; 47import java.util.Calendar; 48import java.util.Locale; 49 50/** 51 * A delegate implementing the radial clock-based TimePicker. 52 */ 53class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate implements 54 RadialTimePickerView.OnValueSelectedListener { 55 private static final String TAG = "TimePickerClockDelegate"; 56 57 // Index used by RadialPickerLayout 58 private static final int HOUR_INDEX = 0; 59 private static final int MINUTE_INDEX = 1; 60 61 // NOT a real index for the purpose of what's showing. 62 private static final int AMPM_INDEX = 2; 63 64 // Also NOT a real index, just used for keyboard mode. 65 private static final int ENABLE_PICKER_INDEX = 3; 66 67 private static final int[] ATTRS_TEXT_COLOR = new int[] { 68 com.android.internal.R.attr.textColor}; 69 private static final int[] ATTRS_DISABLED_ALPHA = new int[] { 70 com.android.internal.R.attr.disabledAlpha}; 71 72 // LayoutLib relies on these constants. Change TimePickerClockDelegate_Delegate if 73 // modifying these. 74 static final int AM = 0; 75 static final int PM = 1; 76 77 private static final int HOURS_IN_HALF_DAY = 12; 78 79 private final View mHeaderView; 80 private final TextView mHourView; 81 private final TextView mMinuteView; 82 private final View mAmPmLayout; 83 private final CheckedTextView mAmLabel; 84 private final CheckedTextView mPmLabel; 85 private final RadialTimePickerView mRadialTimePickerView; 86 private final TextView mSeparatorView; 87 88 private final String mAmText; 89 private final String mPmText; 90 91 private boolean mIsEnabled = true; 92 private boolean mAllowAutoAdvance; 93 private int mInitialHourOfDay; 94 private int mInitialMinute; 95 private boolean mIs24HourView; 96 private boolean mIsAmPmAtStart; 97 98 // For hardware IME input. 99 private char mPlaceholderText; 100 private String mDoublePlaceholderText; 101 private String mDeletedKeyFormat; 102 private boolean mInKbMode; 103 private ArrayList<Integer> mTypedTimes = new ArrayList<Integer>(); 104 private Node mLegalTimesTree; 105 private int mAmKeyCode; 106 private int mPmKeyCode; 107 108 // Accessibility strings. 109 private String mSelectHours; 110 private String mSelectMinutes; 111 112 // Most recent time announcement values for accessibility. 113 private CharSequence mLastAnnouncedText; 114 private boolean mLastAnnouncedIsHour; 115 116 private Calendar mTempCalendar; 117 118 public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs, 119 int defStyleAttr, int defStyleRes) { 120 super(delegator, context); 121 122 // process style attributes 123 final TypedArray a = mContext.obtainStyledAttributes(attrs, 124 R.styleable.TimePicker, defStyleAttr, defStyleRes); 125 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 126 Context.LAYOUT_INFLATER_SERVICE); 127 final Resources res = mContext.getResources(); 128 129 mSelectHours = res.getString(R.string.select_hours); 130 mSelectMinutes = res.getString(R.string.select_minutes); 131 132 String[] amPmStrings = TimePickerSpinnerDelegate.getAmPmStrings(context); 133 mAmText = amPmStrings[0]; 134 mPmText = amPmStrings[1]; 135 136 final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout, 137 R.layout.time_picker_material); 138 final View mainView = inflater.inflate(layoutResourceId, delegator); 139 140 mHeaderView = mainView.findViewById(R.id.time_header); 141 142 // Set up hour/minute labels. 143 mHourView = (TextView) mainView.findViewById(R.id.hours); 144 mHourView.setOnClickListener(mClickListener); 145 mHourView.setAccessibilityDelegate( 146 new ClickActionDelegate(context, R.string.select_hours)); 147 mSeparatorView = (TextView) mainView.findViewById(R.id.separator); 148 mMinuteView = (TextView) mainView.findViewById(R.id.minutes); 149 mMinuteView.setOnClickListener(mClickListener); 150 mMinuteView.setAccessibilityDelegate( 151 new ClickActionDelegate(context, R.string.select_minutes)); 152 153 // Now that we have text appearances out of the way, make sure the hour 154 // and minute views are correctly sized. 155 mHourView.setMinWidth(computeStableWidth(mHourView, 24)); 156 mMinuteView.setMinWidth(computeStableWidth(mMinuteView, 60)); 157 158 // Set up AM/PM labels. 159 mAmPmLayout = mainView.findViewById(R.id.ampm_layout); 160 mAmLabel = (CheckedTextView) mAmPmLayout.findViewById(R.id.am_label); 161 mAmLabel.setText(amPmStrings[0]); 162 mAmLabel.setOnClickListener(mClickListener); 163 mPmLabel = (CheckedTextView) mAmPmLayout.findViewById(R.id.pm_label); 164 mPmLabel.setText(amPmStrings[1]); 165 mPmLabel.setOnClickListener(mClickListener); 166 167 // For the sake of backwards compatibility, attempt to extract the text 168 // color from the header time text appearance. If it's set, we'll let 169 // that override the "real" header text color. 170 ColorStateList headerTextColor = null; 171 172 @SuppressWarnings("deprecation") 173 final int timeHeaderTextAppearance = a.getResourceId( 174 R.styleable.TimePicker_headerTimeTextAppearance, 0); 175 if (timeHeaderTextAppearance != 0) { 176 final TypedArray textAppearance = mContext.obtainStyledAttributes(null, 177 ATTRS_TEXT_COLOR, 0, timeHeaderTextAppearance); 178 final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0); 179 headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor); 180 textAppearance.recycle(); 181 } 182 183 if (headerTextColor == null) { 184 headerTextColor = a.getColorStateList(R.styleable.TimePicker_headerTextColor); 185 } 186 187 if (headerTextColor != null) { 188 mHourView.setTextColor(headerTextColor); 189 mSeparatorView.setTextColor(headerTextColor); 190 mMinuteView.setTextColor(headerTextColor); 191 mAmLabel.setTextColor(headerTextColor); 192 mPmLabel.setTextColor(headerTextColor); 193 } 194 195 // Set up header background, if available. 196 if (a.hasValueOrEmpty(R.styleable.TimePicker_headerBackground)) { 197 mHeaderView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground)); 198 } 199 200 a.recycle(); 201 202 mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById( 203 R.id.radial_picker); 204 205 setupListeners(); 206 207 mAllowAutoAdvance = true; 208 209 // Set up for keyboard mode. 210 mDoublePlaceholderText = res.getString(R.string.time_placeholder); 211 mDeletedKeyFormat = res.getString(R.string.deleted_key); 212 mPlaceholderText = mDoublePlaceholderText.charAt(0); 213 mAmKeyCode = mPmKeyCode = -1; 214 generateLegalTimesTree(); 215 216 // Initialize with current time 217 final Calendar calendar = Calendar.getInstance(mCurrentLocale); 218 final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); 219 final int currentMinute = calendar.get(Calendar.MINUTE); 220 initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX); 221 } 222 223 /** 224 * The legacy text color might have been poorly defined. Ensures that it 225 * has an appropriate activated state, using the selected state if one 226 * exists or modifying the default text color otherwise. 227 * 228 * @param color a legacy text color, or {@code null} 229 * @return a color state list with an appropriate activated state, or 230 * {@code null} if a valid activated state could not be generated 231 */ 232 @Nullable 233 private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) { 234 if (color == null || color.hasState(R.attr.state_activated)) { 235 return color; 236 } 237 238 final int activatedColor; 239 final int defaultColor; 240 if (color.hasState(R.attr.state_selected)) { 241 activatedColor = color.getColorForState(StateSet.get( 242 StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0); 243 defaultColor = color.getColorForState(StateSet.get( 244 StateSet.VIEW_STATE_ENABLED), 0); 245 } else { 246 activatedColor = color.getDefaultColor(); 247 248 // Generate a non-activated color using the disabled alpha. 249 final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA); 250 final float disabledAlpha = ta.getFloat(0, 0.30f); 251 defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha); 252 } 253 254 if (activatedColor == 0 || defaultColor == 0) { 255 // We somehow failed to obtain the colors. 256 return null; 257 } 258 259 final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}}; 260 final int[] colors = new int[] { activatedColor, defaultColor }; 261 return new ColorStateList(stateSet, colors); 262 } 263 264 private int multiplyAlphaComponent(int color, float alphaMod) { 265 final int srcRgb = color & 0xFFFFFF; 266 final int srcAlpha = (color >> 24) & 0xFF; 267 final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f); 268 return srcRgb | (dstAlpha << 24); 269 } 270 271 private static class ClickActionDelegate extends AccessibilityDelegate { 272 private final AccessibilityAction mClickAction; 273 274 public ClickActionDelegate(Context context, int resId) { 275 mClickAction = new AccessibilityAction( 276 AccessibilityNodeInfo.ACTION_CLICK, context.getString(resId)); 277 } 278 279 @Override 280 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 281 super.onInitializeAccessibilityNodeInfo(host, info); 282 283 info.addAction(mClickAction); 284 } 285 } 286 287 private int computeStableWidth(TextView v, int maxNumber) { 288 int maxWidth = 0; 289 290 for (int i = 0; i < maxNumber; i++) { 291 final String text = String.format("%02d", i); 292 v.setText(text); 293 v.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 294 295 final int width = v.getMeasuredWidth(); 296 if (width > maxWidth) { 297 maxWidth = width; 298 } 299 } 300 301 return maxWidth; 302 } 303 304 private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) { 305 mInitialHourOfDay = hourOfDay; 306 mInitialMinute = minute; 307 mIs24HourView = is24HourView; 308 mInKbMode = false; 309 updateUI(index); 310 } 311 312 private void setupListeners() { 313 mHeaderView.setOnKeyListener(mKeyListener); 314 mHeaderView.setOnFocusChangeListener(mFocusListener); 315 mHeaderView.setFocusable(true); 316 317 mRadialTimePickerView.setOnValueSelectedListener(this); 318 } 319 320 private void updateUI(int index) { 321 // Update RadialPicker values 322 updateRadialPicker(index); 323 // Enable or disable the AM/PM view. 324 updateHeaderAmPm(); 325 // Update Hour and Minutes 326 updateHeaderHour(mInitialHourOfDay, false); 327 // Update time separator 328 updateHeaderSeparator(); 329 // Update Minutes 330 updateHeaderMinute(mInitialMinute, false); 331 // Invalidate everything 332 mDelegator.invalidate(); 333 } 334 335 private void updateRadialPicker(int index) { 336 mRadialTimePickerView.initialize(mInitialHourOfDay, mInitialMinute, mIs24HourView); 337 setCurrentItemShowing(index, false, true); 338 } 339 340 private void updateHeaderAmPm() { 341 342 if (mIs24HourView) { 343 mAmPmLayout.setVisibility(View.GONE); 344 } else { 345 // Ensure that AM/PM layout is in the correct position. 346 final String dateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, "hm"); 347 final boolean isAmPmAtStart = dateTimePattern.startsWith("a"); 348 setAmPmAtStart(isAmPmAtStart); 349 350 updateAmPmLabelStates(mInitialHourOfDay < 12 ? AM : PM); 351 } 352 } 353 354 private void setAmPmAtStart(boolean isAmPmAtStart) { 355 if (mIsAmPmAtStart != isAmPmAtStart) { 356 mIsAmPmAtStart = isAmPmAtStart; 357 358 final RelativeLayout.LayoutParams params = 359 (RelativeLayout.LayoutParams) mAmPmLayout.getLayoutParams(); 360 if (params.getRule(RelativeLayout.RIGHT_OF) != 0 || 361 params.getRule(RelativeLayout.LEFT_OF) != 0) { 362 if (isAmPmAtStart) { 363 params.removeRule(RelativeLayout.RIGHT_OF); 364 params.addRule(RelativeLayout.LEFT_OF, mHourView.getId()); 365 } else { 366 params.removeRule(RelativeLayout.LEFT_OF); 367 params.addRule(RelativeLayout.RIGHT_OF, mMinuteView.getId()); 368 } 369 } 370 371 mAmPmLayout.setLayoutParams(params); 372 } 373 } 374 375 /** 376 * Set the current hour. 377 */ 378 @Override 379 public void setCurrentHour(Integer currentHour) { 380 if (mInitialHourOfDay == currentHour) { 381 return; 382 } 383 mInitialHourOfDay = currentHour; 384 updateHeaderHour(currentHour, true); 385 updateHeaderAmPm(); 386 mRadialTimePickerView.setCurrentHour(currentHour); 387 mRadialTimePickerView.setAmOrPm(mInitialHourOfDay < 12 ? AM : PM); 388 mDelegator.invalidate(); 389 onTimeChanged(); 390 } 391 392 /** 393 * @return The current hour in the range (0-23). 394 */ 395 @Override 396 public Integer getCurrentHour() { 397 int currentHour = mRadialTimePickerView.getCurrentHour(); 398 if (mIs24HourView) { 399 return currentHour; 400 } else { 401 switch(mRadialTimePickerView.getAmOrPm()) { 402 case PM: 403 return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; 404 case AM: 405 default: 406 return currentHour % HOURS_IN_HALF_DAY; 407 } 408 } 409 } 410 411 /** 412 * Set the current minute (0-59). 413 */ 414 @Override 415 public void setCurrentMinute(Integer currentMinute) { 416 if (mInitialMinute == currentMinute) { 417 return; 418 } 419 mInitialMinute = currentMinute; 420 updateHeaderMinute(currentMinute, true); 421 mRadialTimePickerView.setCurrentMinute(currentMinute); 422 mDelegator.invalidate(); 423 onTimeChanged(); 424 } 425 426 /** 427 * @return The current minute. 428 */ 429 @Override 430 public Integer getCurrentMinute() { 431 return mRadialTimePickerView.getCurrentMinute(); 432 } 433 434 /** 435 * Set whether in 24 hour or AM/PM mode. 436 * 437 * @param is24HourView True = 24 hour mode. False = AM/PM. 438 */ 439 @Override 440 public void setIs24HourView(Boolean is24HourView) { 441 if (is24HourView == mIs24HourView) { 442 return; 443 } 444 mIs24HourView = is24HourView; 445 generateLegalTimesTree(); 446 int hour = mRadialTimePickerView.getCurrentHour(); 447 mInitialHourOfDay = hour; 448 updateHeaderHour(hour, false); 449 updateHeaderAmPm(); 450 updateRadialPicker(mRadialTimePickerView.getCurrentItemShowing()); 451 mDelegator.invalidate(); 452 } 453 454 /** 455 * @return true if this is in 24 hour view else false. 456 */ 457 @Override 458 public boolean is24HourView() { 459 return mIs24HourView; 460 } 461 462 @Override 463 public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener callback) { 464 mOnTimeChangedListener = callback; 465 } 466 467 @Override 468 public void setEnabled(boolean enabled) { 469 mHourView.setEnabled(enabled); 470 mMinuteView.setEnabled(enabled); 471 mAmLabel.setEnabled(enabled); 472 mPmLabel.setEnabled(enabled); 473 mRadialTimePickerView.setEnabled(enabled); 474 mIsEnabled = enabled; 475 } 476 477 @Override 478 public boolean isEnabled() { 479 return mIsEnabled; 480 } 481 482 @Override 483 public int getBaseline() { 484 // does not support baseline alignment 485 return -1; 486 } 487 488 @Override 489 public void onConfigurationChanged(Configuration newConfig) { 490 updateUI(mRadialTimePickerView.getCurrentItemShowing()); 491 } 492 493 @Override 494 public Parcelable onSaveInstanceState(Parcelable superState) { 495 return new SavedState(superState, getCurrentHour(), getCurrentMinute(), 496 is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing()); 497 } 498 499 @Override 500 public void onRestoreInstanceState(Parcelable state) { 501 SavedState ss = (SavedState) state; 502 setInKbMode(ss.inKbMode()); 503 setTypedTimes(ss.getTypesTimes()); 504 initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing()); 505 mRadialTimePickerView.invalidate(); 506 if (mInKbMode) { 507 tryStartingKbMode(-1); 508 mHourView.invalidate(); 509 } 510 } 511 512 @Override 513 public void setCurrentLocale(Locale locale) { 514 super.setCurrentLocale(locale); 515 mTempCalendar = Calendar.getInstance(locale); 516 } 517 518 @Override 519 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 520 onPopulateAccessibilityEvent(event); 521 return true; 522 } 523 524 @Override 525 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 526 int flags = DateUtils.FORMAT_SHOW_TIME; 527 if (mIs24HourView) { 528 flags |= DateUtils.FORMAT_24HOUR; 529 } else { 530 flags |= DateUtils.FORMAT_12HOUR; 531 } 532 mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); 533 mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); 534 String selectedDate = DateUtils.formatDateTime(mContext, 535 mTempCalendar.getTimeInMillis(), flags); 536 event.getText().add(selectedDate); 537 } 538 539 /** 540 * Set whether in keyboard mode or not. 541 * 542 * @param inKbMode True means in keyboard mode. 543 */ 544 private void setInKbMode(boolean inKbMode) { 545 mInKbMode = inKbMode; 546 } 547 548 /** 549 * @return true if in keyboard mode 550 */ 551 private boolean inKbMode() { 552 return mInKbMode; 553 } 554 555 private void setTypedTimes(ArrayList<Integer> typeTimes) { 556 mTypedTimes = typeTimes; 557 } 558 559 /** 560 * @return an array of typed times 561 */ 562 private ArrayList<Integer> getTypedTimes() { 563 return mTypedTimes; 564 } 565 566 /** 567 * @return the index of the current item showing 568 */ 569 private int getCurrentItemShowing() { 570 return mRadialTimePickerView.getCurrentItemShowing(); 571 } 572 573 /** 574 * Propagate the time change 575 */ 576 private void onTimeChanged() { 577 mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 578 if (mOnTimeChangedListener != null) { 579 mOnTimeChangedListener.onTimeChanged(mDelegator, 580 getCurrentHour(), getCurrentMinute()); 581 } 582 } 583 584 /** 585 * Used to save / restore state of time picker 586 */ 587 private static class SavedState extends View.BaseSavedState { 588 589 private final int mHour; 590 private final int mMinute; 591 private final boolean mIs24HourMode; 592 private final boolean mInKbMode; 593 private final ArrayList<Integer> mTypedTimes; 594 private final int mCurrentItemShowing; 595 596 private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode, 597 boolean isKbMode, ArrayList<Integer> typedTimes, 598 int currentItemShowing) { 599 super(superState); 600 mHour = hour; 601 mMinute = minute; 602 mIs24HourMode = is24HourMode; 603 mInKbMode = isKbMode; 604 mTypedTimes = typedTimes; 605 mCurrentItemShowing = currentItemShowing; 606 } 607 608 private SavedState(Parcel in) { 609 super(in); 610 mHour = in.readInt(); 611 mMinute = in.readInt(); 612 mIs24HourMode = (in.readInt() == 1); 613 mInKbMode = (in.readInt() == 1); 614 mTypedTimes = in.readArrayList(getClass().getClassLoader()); 615 mCurrentItemShowing = in.readInt(); 616 } 617 618 public int getHour() { 619 return mHour; 620 } 621 622 public int getMinute() { 623 return mMinute; 624 } 625 626 public boolean is24HourMode() { 627 return mIs24HourMode; 628 } 629 630 public boolean inKbMode() { 631 return mInKbMode; 632 } 633 634 public ArrayList<Integer> getTypesTimes() { 635 return mTypedTimes; 636 } 637 638 public int getCurrentItemShowing() { 639 return mCurrentItemShowing; 640 } 641 642 @Override 643 public void writeToParcel(Parcel dest, int flags) { 644 super.writeToParcel(dest, flags); 645 dest.writeInt(mHour); 646 dest.writeInt(mMinute); 647 dest.writeInt(mIs24HourMode ? 1 : 0); 648 dest.writeInt(mInKbMode ? 1 : 0); 649 dest.writeList(mTypedTimes); 650 dest.writeInt(mCurrentItemShowing); 651 } 652 653 @SuppressWarnings({"unused", "hiding"}) 654 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { 655 public SavedState createFromParcel(Parcel in) { 656 return new SavedState(in); 657 } 658 659 public SavedState[] newArray(int size) { 660 return new SavedState[size]; 661 } 662 }; 663 } 664 665 private void tryVibrate() { 666 mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); 667 } 668 669 private void updateAmPmLabelStates(int amOrPm) { 670 final boolean isAm = amOrPm == AM; 671 mAmLabel.setActivated(isAm); 672 mAmLabel.setChecked(isAm); 673 674 final boolean isPm = amOrPm == PM; 675 mPmLabel.setActivated(isPm); 676 mPmLabel.setChecked(isPm); 677 } 678 679 /** 680 * Called by the picker for updating the header display. 681 */ 682 @Override 683 public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { 684 switch (pickerIndex) { 685 case HOUR_INDEX: 686 if (mAllowAutoAdvance && autoAdvance) { 687 updateHeaderHour(newValue, false); 688 setCurrentItemShowing(MINUTE_INDEX, true, false); 689 mDelegator.announceForAccessibility(newValue + ". " + mSelectMinutes); 690 } else { 691 updateHeaderHour(newValue, true); 692 } 693 break; 694 case MINUTE_INDEX: 695 updateHeaderMinute(newValue, true); 696 break; 697 case AMPM_INDEX: 698 updateAmPmLabelStates(newValue); 699 break; 700 case ENABLE_PICKER_INDEX: 701 if (!isTypedTimeFullyLegal()) { 702 mTypedTimes.clear(); 703 } 704 finishKbMode(); 705 break; 706 } 707 708 if (mOnTimeChangedListener != null) { 709 mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), getCurrentMinute()); 710 } 711 } 712 713 private void updateHeaderHour(int value, boolean announce) { 714 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, 715 (mIs24HourView) ? "Hm" : "hm"); 716 final int lengthPattern = bestDateTimePattern.length(); 717 boolean hourWithTwoDigit = false; 718 char hourFormat = '\0'; 719 // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save 720 // the hour format that we found. 721 for (int i = 0; i < lengthPattern; i++) { 722 final char c = bestDateTimePattern.charAt(i); 723 if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { 724 hourFormat = c; 725 if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { 726 hourWithTwoDigit = true; 727 } 728 break; 729 } 730 } 731 final String format; 732 if (hourWithTwoDigit) { 733 format = "%02d"; 734 } else { 735 format = "%d"; 736 } 737 if (mIs24HourView) { 738 // 'k' means 1-24 hour 739 if (hourFormat == 'k' && value == 0) { 740 value = 24; 741 } 742 } else { 743 // 'K' means 0-11 hour 744 value = modulo12(value, hourFormat == 'K'); 745 } 746 CharSequence text = String.format(format, value); 747 mHourView.setText(text); 748 if (announce) { 749 tryAnnounceForAccessibility(text, true); 750 } 751 } 752 753 private void tryAnnounceForAccessibility(CharSequence text, boolean isHour) { 754 if (mLastAnnouncedIsHour != isHour || !text.equals(mLastAnnouncedText)) { 755 // TODO: Find a better solution, potentially live regions? 756 mDelegator.announceForAccessibility(text); 757 mLastAnnouncedText = text; 758 mLastAnnouncedIsHour = isHour; 759 } 760 } 761 762 private static int modulo12(int n, boolean startWithZero) { 763 int value = n % 12; 764 if (value == 0 && !startWithZero) { 765 value = 12; 766 } 767 return value; 768 } 769 770 /** 771 * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". 772 * 773 * See http://unicode.org/cldr/trac/browser/trunk/common/main 774 * 775 * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the 776 * separator as the character which is just after the hour marker in the returned pattern. 777 */ 778 private void updateHeaderSeparator() { 779 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, 780 (mIs24HourView) ? "Hm" : "hm"); 781 final String separatorText; 782 // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats 783 final char[] hourFormats = {'H', 'h', 'K', 'k'}; 784 int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats); 785 if (hIndex == -1) { 786 // Default case 787 separatorText = ":"; 788 } else { 789 separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1)); 790 } 791 mSeparatorView.setText(separatorText); 792 } 793 794 static private int lastIndexOfAny(String str, char[] any) { 795 final int lengthAny = any.length; 796 if (lengthAny > 0) { 797 for (int i = str.length() - 1; i >= 0; i--) { 798 char c = str.charAt(i); 799 for (int j = 0; j < lengthAny; j++) { 800 if (c == any[j]) { 801 return i; 802 } 803 } 804 } 805 } 806 return -1; 807 } 808 809 private void updateHeaderMinute(int value, boolean announceForAccessibility) { 810 if (value == 60) { 811 value = 0; 812 } 813 final CharSequence text = String.format(mCurrentLocale, "%02d", value); 814 mMinuteView.setText(text); 815 if (announceForAccessibility) { 816 tryAnnounceForAccessibility(text, false); 817 } 818 } 819 820 /** 821 * Show either Hours or Minutes. 822 */ 823 private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) { 824 mRadialTimePickerView.setCurrentItemShowing(index, animateCircle); 825 826 if (index == HOUR_INDEX) { 827 if (announce) { 828 mDelegator.announceForAccessibility(mSelectHours); 829 } 830 } else { 831 if (announce) { 832 mDelegator.announceForAccessibility(mSelectMinutes); 833 } 834 } 835 836 mHourView.setActivated(index == HOUR_INDEX); 837 mMinuteView.setActivated(index == MINUTE_INDEX); 838 } 839 840 private void setAmOrPm(int amOrPm) { 841 updateAmPmLabelStates(amOrPm); 842 mRadialTimePickerView.setAmOrPm(amOrPm); 843 } 844 845 /** 846 * For keyboard mode, processes key events. 847 * 848 * @param keyCode the pressed key. 849 * 850 * @return true if the key was successfully processed, false otherwise. 851 */ 852 private boolean processKeyUp(int keyCode) { 853 if (keyCode == KeyEvent.KEYCODE_DEL) { 854 if (mInKbMode) { 855 if (!mTypedTimes.isEmpty()) { 856 int deleted = deleteLastTypedKey(); 857 String deletedKeyStr; 858 if (deleted == getAmOrPmKeyCode(AM)) { 859 deletedKeyStr = mAmText; 860 } else if (deleted == getAmOrPmKeyCode(PM)) { 861 deletedKeyStr = mPmText; 862 } else { 863 deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); 864 } 865 mDelegator.announceForAccessibility( 866 String.format(mDeletedKeyFormat, deletedKeyStr)); 867 updateDisplay(true); 868 } 869 } 870 } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 871 || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 872 || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 873 || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 874 || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 875 || (!mIs24HourView && 876 (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { 877 if (!mInKbMode) { 878 if (mRadialTimePickerView == null) { 879 // Something's wrong, because time picker should definitely not be null. 880 Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null."); 881 return true; 882 } 883 mTypedTimes.clear(); 884 tryStartingKbMode(keyCode); 885 return true; 886 } 887 // We're already in keyboard mode. 888 if (addKeyIfLegal(keyCode)) { 889 updateDisplay(false); 890 } 891 return true; 892 } 893 return false; 894 } 895 896 /** 897 * Try to start keyboard mode with the specified key. 898 * 899 * @param keyCode The key to use as the first press. Keyboard mode will not be started if the 900 * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting 901 * key. 902 */ 903 private void tryStartingKbMode(int keyCode) { 904 if (keyCode == -1 || addKeyIfLegal(keyCode)) { 905 mInKbMode = true; 906 onValidationChanged(false); 907 updateDisplay(false); 908 mRadialTimePickerView.setInputEnabled(false); 909 } 910 } 911 912 private boolean addKeyIfLegal(int keyCode) { 913 // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, 914 // we'll need to see if AM/PM have been typed. 915 if ((mIs24HourView && mTypedTimes.size() == 4) || 916 (!mIs24HourView && isTypedTimeFullyLegal())) { 917 return false; 918 } 919 920 mTypedTimes.add(keyCode); 921 if (!isTypedTimeLegalSoFar()) { 922 deleteLastTypedKey(); 923 return false; 924 } 925 926 int val = getValFromKeyCode(keyCode); 927 mDelegator.announceForAccessibility(String.format("%d", val)); 928 // Automatically fill in 0's if AM or PM was legally entered. 929 if (isTypedTimeFullyLegal()) { 930 if (!mIs24HourView && mTypedTimes.size() <= 3) { 931 mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); 932 mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); 933 } 934 onValidationChanged(true); 935 } 936 937 return true; 938 } 939 940 /** 941 * Traverse the tree to see if the keys that have been typed so far are legal as is, 942 * or may become legal as more keys are typed (excluding backspace). 943 */ 944 private boolean isTypedTimeLegalSoFar() { 945 Node node = mLegalTimesTree; 946 for (int keyCode : mTypedTimes) { 947 node = node.canReach(keyCode); 948 if (node == null) { 949 return false; 950 } 951 } 952 return true; 953 } 954 955 /** 956 * Check if the time that has been typed so far is completely legal, as is. 957 */ 958 private boolean isTypedTimeFullyLegal() { 959 if (mIs24HourView) { 960 // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: 961 // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. 962 int[] values = getEnteredTime(null); 963 return (values[0] >= 0 && values[1] >= 0 && values[1] < 60); 964 } else { 965 // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be 966 // legally added at specific times based on the tree's algorithm. 967 return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || 968 mTypedTimes.contains(getAmOrPmKeyCode(PM))); 969 } 970 } 971 972 private int deleteLastTypedKey() { 973 int deleted = mTypedTimes.remove(mTypedTimes.size() - 1); 974 if (!isTypedTimeFullyLegal()) { 975 onValidationChanged(false); 976 } 977 return deleted; 978 } 979 980 /** 981 * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. 982 */ 983 private void finishKbMode() { 984 mInKbMode = false; 985 if (!mTypedTimes.isEmpty()) { 986 int values[] = getEnteredTime(null); 987 mRadialTimePickerView.setCurrentHour(values[0]); 988 mRadialTimePickerView.setCurrentMinute(values[1]); 989 if (!mIs24HourView) { 990 mRadialTimePickerView.setAmOrPm(values[2]); 991 } 992 mTypedTimes.clear(); 993 } 994 updateDisplay(false); 995 mRadialTimePickerView.setInputEnabled(true); 996 } 997 998 /** 999 * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is 1000 * empty, either show an empty display (filled with the placeholder text), or update from the 1001 * timepicker's values. 1002 * 1003 * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. 1004 * Otherwise, revert to the timepicker's values. 1005 */ 1006 private void updateDisplay(boolean allowEmptyDisplay) { 1007 if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { 1008 int hour = mRadialTimePickerView.getCurrentHour(); 1009 int minute = mRadialTimePickerView.getCurrentMinute(); 1010 updateHeaderHour(hour, false); 1011 updateHeaderMinute(minute, false); 1012 if (!mIs24HourView) { 1013 updateAmPmLabelStates(hour < 12 ? AM : PM); 1014 } 1015 setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true); 1016 onValidationChanged(true); 1017 } else { 1018 boolean[] enteredZeros = {false, false}; 1019 int[] values = getEnteredTime(enteredZeros); 1020 String hourFormat = enteredZeros[0] ? "%02d" : "%2d"; 1021 String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d"; 1022 String hourStr = (values[0] == -1) ? mDoublePlaceholderText : 1023 String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); 1024 String minuteStr = (values[1] == -1) ? mDoublePlaceholderText : 1025 String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); 1026 mHourView.setText(hourStr); 1027 mHourView.setActivated(false); 1028 mMinuteView.setText(minuteStr); 1029 mMinuteView.setActivated(false); 1030 if (!mIs24HourView) { 1031 updateAmPmLabelStates(values[2]); 1032 } 1033 } 1034 } 1035 1036 private int getValFromKeyCode(int keyCode) { 1037 switch (keyCode) { 1038 case KeyEvent.KEYCODE_0: 1039 return 0; 1040 case KeyEvent.KEYCODE_1: 1041 return 1; 1042 case KeyEvent.KEYCODE_2: 1043 return 2; 1044 case KeyEvent.KEYCODE_3: 1045 return 3; 1046 case KeyEvent.KEYCODE_4: 1047 return 4; 1048 case KeyEvent.KEYCODE_5: 1049 return 5; 1050 case KeyEvent.KEYCODE_6: 1051 return 6; 1052 case KeyEvent.KEYCODE_7: 1053 return 7; 1054 case KeyEvent.KEYCODE_8: 1055 return 8; 1056 case KeyEvent.KEYCODE_9: 1057 return 9; 1058 default: 1059 return -1; 1060 } 1061 } 1062 1063 /** 1064 * Get the currently-entered time, as integer values of the hours and minutes typed. 1065 * 1066 * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which 1067 * may then be used for the caller to know whether zeros had been explicitly entered as either 1068 * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. 1069 * 1070 * @return A size-3 int array. The first value will be the hours, the second value will be the 1071 * minutes, and the third will be either AM or PM. 1072 */ 1073 private int[] getEnteredTime(boolean[] enteredZeros) { 1074 int amOrPm = -1; 1075 int startIndex = 1; 1076 if (!mIs24HourView && isTypedTimeFullyLegal()) { 1077 int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); 1078 if (keyCode == getAmOrPmKeyCode(AM)) { 1079 amOrPm = AM; 1080 } else if (keyCode == getAmOrPmKeyCode(PM)){ 1081 amOrPm = PM; 1082 } 1083 startIndex = 2; 1084 } 1085 int minute = -1; 1086 int hour = -1; 1087 for (int i = startIndex; i <= mTypedTimes.size(); i++) { 1088 int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); 1089 if (i == startIndex) { 1090 minute = val; 1091 } else if (i == startIndex+1) { 1092 minute += 10 * val; 1093 if (enteredZeros != null && val == 0) { 1094 enteredZeros[1] = true; 1095 } 1096 } else if (i == startIndex+2) { 1097 hour = val; 1098 } else if (i == startIndex+3) { 1099 hour += 10 * val; 1100 if (enteredZeros != null && val == 0) { 1101 enteredZeros[0] = true; 1102 } 1103 } 1104 } 1105 1106 return new int[] { hour, minute, amOrPm }; 1107 } 1108 1109 /** 1110 * Get the keycode value for AM and PM in the current language. 1111 */ 1112 private int getAmOrPmKeyCode(int amOrPm) { 1113 // Cache the codes. 1114 if (mAmKeyCode == -1 || mPmKeyCode == -1) { 1115 // Find the first character in the AM/PM text that is unique. 1116 final KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); 1117 final CharSequence amText = mAmText.toLowerCase(mCurrentLocale); 1118 final CharSequence pmText = mPmText.toLowerCase(mCurrentLocale); 1119 final int N = Math.min(amText.length(), pmText.length()); 1120 for (int i = 0; i < N; i++) { 1121 final char amChar = amText.charAt(i); 1122 final char pmChar = pmText.charAt(i); 1123 if (amChar != pmChar) { 1124 // There should be 4 events: a down and up for both AM and PM. 1125 final KeyEvent[] events = kcm.getEvents(new char[] { amChar, pmChar }); 1126 if (events != null && events.length == 4) { 1127 mAmKeyCode = events[0].getKeyCode(); 1128 mPmKeyCode = events[2].getKeyCode(); 1129 } else { 1130 Log.e(TAG, "Unable to find keycodes for AM and PM."); 1131 } 1132 break; 1133 } 1134 } 1135 } 1136 1137 if (amOrPm == AM) { 1138 return mAmKeyCode; 1139 } else if (amOrPm == PM) { 1140 return mPmKeyCode; 1141 } 1142 1143 return -1; 1144 } 1145 1146 /** 1147 * Create a tree for deciding what keys can legally be typed. 1148 */ 1149 private void generateLegalTimesTree() { 1150 // Create a quick cache of numbers to their keycodes. 1151 final int k0 = KeyEvent.KEYCODE_0; 1152 final int k1 = KeyEvent.KEYCODE_1; 1153 final int k2 = KeyEvent.KEYCODE_2; 1154 final int k3 = KeyEvent.KEYCODE_3; 1155 final int k4 = KeyEvent.KEYCODE_4; 1156 final int k5 = KeyEvent.KEYCODE_5; 1157 final int k6 = KeyEvent.KEYCODE_6; 1158 final int k7 = KeyEvent.KEYCODE_7; 1159 final int k8 = KeyEvent.KEYCODE_8; 1160 final int k9 = KeyEvent.KEYCODE_9; 1161 1162 // The root of the tree doesn't contain any numbers. 1163 mLegalTimesTree = new Node(); 1164 if (mIs24HourView) { 1165 // We'll be re-using these nodes, so we'll save them. 1166 Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5); 1167 Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); 1168 // The first digit must be followed by the second digit. 1169 minuteFirstDigit.addChild(minuteSecondDigit); 1170 1171 // The first digit may be 0-1. 1172 Node firstDigit = new Node(k0, k1); 1173 mLegalTimesTree.addChild(firstDigit); 1174 1175 // When the first digit is 0-1, the second digit may be 0-5. 1176 Node secondDigit = new Node(k0, k1, k2, k3, k4, k5); 1177 firstDigit.addChild(secondDigit); 1178 // We may now be followed by the first minute digit. E.g. 00:09, 15:58. 1179 secondDigit.addChild(minuteFirstDigit); 1180 1181 // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9. 1182 Node thirdDigit = new Node(k6, k7, k8, k9); 1183 // The time must now be finished. E.g. 0:55, 1:08. 1184 secondDigit.addChild(thirdDigit); 1185 1186 // When the first digit is 0-1, the second digit may be 6-9. 1187 secondDigit = new Node(k6, k7, k8, k9); 1188 firstDigit.addChild(secondDigit); 1189 // We must now be followed by the first minute digit. E.g. 06:50, 18:20. 1190 secondDigit.addChild(minuteFirstDigit); 1191 1192 // The first digit may be 2. 1193 firstDigit = new Node(k2); 1194 mLegalTimesTree.addChild(firstDigit); 1195 1196 // When the first digit is 2, the second digit may be 0-3. 1197 secondDigit = new Node(k0, k1, k2, k3); 1198 firstDigit.addChild(secondDigit); 1199 // We must now be followed by the first minute digit. E.g. 20:50, 23:09. 1200 secondDigit.addChild(minuteFirstDigit); 1201 1202 // When the first digit is 2, the second digit may be 4-5. 1203 secondDigit = new Node(k4, k5); 1204 firstDigit.addChild(secondDigit); 1205 // We must now be followd by the last minute digit. E.g. 2:40, 2:53. 1206 secondDigit.addChild(minuteSecondDigit); 1207 1208 // The first digit may be 3-9. 1209 firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9); 1210 mLegalTimesTree.addChild(firstDigit); 1211 // We must now be followed by the first minute digit. E.g. 3:57, 8:12. 1212 firstDigit.addChild(minuteFirstDigit); 1213 } else { 1214 // We'll need to use the AM/PM node a lot. 1215 // Set up AM and PM to respond to "a" and "p". 1216 Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM)); 1217 1218 // The first hour digit may be 1. 1219 Node firstDigit = new Node(k1); 1220 mLegalTimesTree.addChild(firstDigit); 1221 // We'll allow quick input of on-the-hour times. E.g. 1pm. 1222 firstDigit.addChild(ampm); 1223 1224 // When the first digit is 1, the second digit may be 0-2. 1225 Node secondDigit = new Node(k0, k1, k2); 1226 firstDigit.addChild(secondDigit); 1227 // Also for quick input of on-the-hour times. E.g. 10pm, 12am. 1228 secondDigit.addChild(ampm); 1229 1230 // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5. 1231 Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5); 1232 secondDigit.addChild(thirdDigit); 1233 // The time may be finished now. E.g. 1:02pm, 1:25am. 1234 thirdDigit.addChild(ampm); 1235 1236 // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5, 1237 // the fourth digit may be 0-9. 1238 Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); 1239 thirdDigit.addChild(fourthDigit); 1240 // The time must be finished now. E.g. 10:49am, 12:40pm. 1241 fourthDigit.addChild(ampm); 1242 1243 // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9. 1244 thirdDigit = new Node(k6, k7, k8, k9); 1245 secondDigit.addChild(thirdDigit); 1246 // The time must be finished now. E.g. 1:08am, 1:26pm. 1247 thirdDigit.addChild(ampm); 1248 1249 // When the first digit is 1, the second digit may be 3-5. 1250 secondDigit = new Node(k3, k4, k5); 1251 firstDigit.addChild(secondDigit); 1252 1253 // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9. 1254 thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); 1255 secondDigit.addChild(thirdDigit); 1256 // The time must be finished now. E.g. 1:39am, 1:50pm. 1257 thirdDigit.addChild(ampm); 1258 1259 // The hour digit may be 2-9. 1260 firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9); 1261 mLegalTimesTree.addChild(firstDigit); 1262 // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm. 1263 firstDigit.addChild(ampm); 1264 1265 // When the first digit is 2-9, the second digit may be 0-5. 1266 secondDigit = new Node(k0, k1, k2, k3, k4, k5); 1267 firstDigit.addChild(secondDigit); 1268 1269 // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9. 1270 thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); 1271 secondDigit.addChild(thirdDigit); 1272 // The time must be finished now. E.g. 2:57am, 9:30pm. 1273 thirdDigit.addChild(ampm); 1274 } 1275 } 1276 1277 /** 1278 * Simple node class to be used for traversal to check for legal times. 1279 * mLegalKeys represents the keys that can be typed to get to the node. 1280 * mChildren are the children that can be reached from this node. 1281 */ 1282 private class Node { 1283 private int[] mLegalKeys; 1284 private ArrayList<Node> mChildren; 1285 1286 public Node(int... legalKeys) { 1287 mLegalKeys = legalKeys; 1288 mChildren = new ArrayList<Node>(); 1289 } 1290 1291 public void addChild(Node child) { 1292 mChildren.add(child); 1293 } 1294 1295 public boolean containsKey(int key) { 1296 for (int i = 0; i < mLegalKeys.length; i++) { 1297 if (mLegalKeys[i] == key) { 1298 return true; 1299 } 1300 } 1301 return false; 1302 } 1303 1304 public Node canReach(int key) { 1305 if (mChildren == null) { 1306 return null; 1307 } 1308 for (Node child : mChildren) { 1309 if (child.containsKey(key)) { 1310 return child; 1311 } 1312 } 1313 return null; 1314 } 1315 } 1316 1317 private final View.OnClickListener mClickListener = new View.OnClickListener() { 1318 @Override 1319 public void onClick(View v) { 1320 1321 final int amOrPm; 1322 switch (v.getId()) { 1323 case R.id.am_label: 1324 setAmOrPm(AM); 1325 break; 1326 case R.id.pm_label: 1327 setAmOrPm(PM); 1328 break; 1329 case R.id.hours: 1330 setCurrentItemShowing(HOUR_INDEX, true, true); 1331 break; 1332 case R.id.minutes: 1333 setCurrentItemShowing(MINUTE_INDEX, true, true); 1334 break; 1335 default: 1336 // Failed to handle this click, don't vibrate. 1337 return; 1338 } 1339 1340 tryVibrate(); 1341 } 1342 }; 1343 1344 private final View.OnKeyListener mKeyListener = new View.OnKeyListener() { 1345 @Override 1346 public boolean onKey(View v, int keyCode, KeyEvent event) { 1347 if (event.getAction() == KeyEvent.ACTION_UP) { 1348 return processKeyUp(keyCode); 1349 } 1350 return false; 1351 } 1352 }; 1353 1354 private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() { 1355 @Override 1356 public void onFocusChange(View v, boolean hasFocus) { 1357 if (!hasFocus && mInKbMode && isTypedTimeFullyLegal()) { 1358 finishKbMode(); 1359 1360 if (mOnTimeChangedListener != null) { 1361 mOnTimeChangedListener.onTimeChanged(mDelegator, 1362 mRadialTimePickerView.getCurrentHour(), 1363 mRadialTimePickerView.getCurrentMinute()); 1364 } 1365 } 1366 } 1367 }; 1368} 1369