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