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