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