TimePickerClockDelegate.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.content.Context; 20import android.content.res.Configuration; 21import android.content.res.Resources; 22import android.content.res.TypedArray; 23import android.os.Parcel; 24import android.os.Parcelable; 25import android.text.format.DateFormat; 26import android.text.format.DateUtils; 27import android.util.AttributeSet; 28import android.view.LayoutInflater; 29import android.view.View; 30import android.view.ViewGroup; 31import android.view.accessibility.AccessibilityEvent; 32import android.view.accessibility.AccessibilityNodeInfo; 33import android.view.inputmethod.EditorInfo; 34import android.view.inputmethod.InputMethodManager; 35import com.android.internal.R; 36 37import java.util.Calendar; 38import java.util.Locale; 39 40import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; 41import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; 42 43/** 44 * A delegate implementing the basic TimePicker 45 */ 46class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { 47 private static final boolean DEFAULT_ENABLED_STATE = true; 48 private static final int HOURS_IN_HALF_DAY = 12; 49 50 // state 51 private boolean mIs24HourView; 52 private boolean mIsAm; 53 54 // ui components 55 private final NumberPicker mHourSpinner; 56 private final NumberPicker mMinuteSpinner; 57 private final NumberPicker mAmPmSpinner; 58 private final EditText mHourSpinnerInput; 59 private final EditText mMinuteSpinnerInput; 60 private final EditText mAmPmSpinnerInput; 61 private final TextView mDivider; 62 63 // Note that the legacy implementation of the TimePicker is 64 // using a button for toggling between AM/PM while the new 65 // version uses a NumberPicker spinner. Therefore the code 66 // accommodates these two cases to be backwards compatible. 67 private final Button mAmPmButton; 68 69 private final String[] mAmPmStrings; 70 71 private boolean mIsEnabled = DEFAULT_ENABLED_STATE; 72 private Calendar mTempCalendar; 73 private boolean mHourWithTwoDigit; 74 private char mHourFormat; 75 76 /** 77 * A no-op callback used in the constructor to avoid null checks later in 78 * the code. 79 */ 80 private static final TimePicker.OnTimeChangedListener NO_OP_CHANGE_LISTENER = 81 new TimePicker.OnTimeChangedListener() { 82 public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { 83 } 84 }; 85 86 public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs, 87 int defStyleAttr, int defStyleRes) { 88 super(delegator, context); 89 90 // process style attributes 91 final TypedArray a = mContext.obtainStyledAttributes( 92 attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes); 93 final int layoutResourceId = a.getResourceId( 94 R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy); 95 a.recycle(); 96 97 final LayoutInflater inflater = LayoutInflater.from(mContext); 98 inflater.inflate(layoutResourceId, mDelegator, true); 99 100 // hour 101 mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour); 102 mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { 103 public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { 104 updateInputState(); 105 if (!is24HourView()) { 106 if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) || 107 (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { 108 mIsAm = !mIsAm; 109 updateAmPmControl(); 110 } 111 } 112 onTimeChanged(); 113 } 114 }); 115 mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); 116 mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); 117 118 // divider (only for the new widget style) 119 mDivider = (TextView) mDelegator.findViewById(R.id.divider); 120 if (mDivider != null) { 121 setDividerText(); 122 } 123 124 // minute 125 mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute); 126 mMinuteSpinner.setMinValue(0); 127 mMinuteSpinner.setMaxValue(59); 128 mMinuteSpinner.setOnLongPressUpdateInterval(100); 129 mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); 130 mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { 131 public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { 132 updateInputState(); 133 int minValue = mMinuteSpinner.getMinValue(); 134 int maxValue = mMinuteSpinner.getMaxValue(); 135 if (oldVal == maxValue && newVal == minValue) { 136 int newHour = mHourSpinner.getValue() + 1; 137 if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { 138 mIsAm = !mIsAm; 139 updateAmPmControl(); 140 } 141 mHourSpinner.setValue(newHour); 142 } else if (oldVal == minValue && newVal == maxValue) { 143 int newHour = mHourSpinner.getValue() - 1; 144 if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { 145 mIsAm = !mIsAm; 146 updateAmPmControl(); 147 } 148 mHourSpinner.setValue(newHour); 149 } 150 onTimeChanged(); 151 } 152 }); 153 mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); 154 mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); 155 156 /* Get the localized am/pm strings and use them in the spinner */ 157 final Resources res = context.getResources(); 158 final String amText = res.getString(R.string.time_picker_am_label); 159 final String pmText = res.getString(R.string.time_picker_pm_label); 160 mAmPmStrings = new String[] {amText, pmText}; 161 162 // am/pm 163 View amPmView = mDelegator.findViewById(R.id.amPm); 164 if (amPmView instanceof Button) { 165 mAmPmSpinner = null; 166 mAmPmSpinnerInput = null; 167 mAmPmButton = (Button) amPmView; 168 mAmPmButton.setOnClickListener(new View.OnClickListener() { 169 public void onClick(View button) { 170 button.requestFocus(); 171 mIsAm = !mIsAm; 172 updateAmPmControl(); 173 onTimeChanged(); 174 } 175 }); 176 } else { 177 mAmPmButton = null; 178 mAmPmSpinner = (NumberPicker) amPmView; 179 mAmPmSpinner.setMinValue(0); 180 mAmPmSpinner.setMaxValue(1); 181 mAmPmSpinner.setDisplayedValues(mAmPmStrings); 182 mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { 183 public void onValueChange(NumberPicker picker, int oldVal, int newVal) { 184 updateInputState(); 185 picker.requestFocus(); 186 mIsAm = !mIsAm; 187 updateAmPmControl(); 188 onTimeChanged(); 189 } 190 }); 191 mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); 192 mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); 193 } 194 195 if (isAmPmAtStart()) { 196 // Move the am/pm view to the beginning 197 ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout); 198 amPmParent.removeView(amPmView); 199 amPmParent.addView(amPmView, 0); 200 // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme 201 // for example and not for Holo Theme) 202 ViewGroup.MarginLayoutParams lp = 203 (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); 204 final int startMargin = lp.getMarginStart(); 205 final int endMargin = lp.getMarginEnd(); 206 if (startMargin != endMargin) { 207 lp.setMarginStart(endMargin); 208 lp.setMarginEnd(startMargin); 209 } 210 } 211 212 getHourFormatData(); 213 214 // update controls to initial state 215 updateHourControl(); 216 updateMinuteControl(); 217 updateAmPmControl(); 218 219 setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); 220 221 // set to current time 222 setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); 223 setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); 224 225 if (!isEnabled()) { 226 setEnabled(false); 227 } 228 229 // set the content descriptions 230 setContentDescriptions(); 231 232 // If not explicitly specified this view is important for accessibility. 233 if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 234 mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 235 } 236 } 237 238 private void getHourFormatData() { 239 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, 240 (mIs24HourView) ? "Hm" : "hm"); 241 final int lengthPattern = bestDateTimePattern.length(); 242 mHourWithTwoDigit = false; 243 char hourFormat = '\0'; 244 // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save 245 // the hour format that we found. 246 for (int i = 0; i < lengthPattern; i++) { 247 final char c = bestDateTimePattern.charAt(i); 248 if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { 249 mHourFormat = c; 250 if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { 251 mHourWithTwoDigit = true; 252 } 253 break; 254 } 255 } 256 } 257 258 private boolean isAmPmAtStart() { 259 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, 260 "hm" /* skeleton */); 261 262 return bestDateTimePattern.startsWith("a"); 263 } 264 265 /** 266 * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". 267 * 268 * See http://unicode.org/cldr/trac/browser/trunk/common/main 269 * 270 * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the 271 * separator as the character which is just after the hour marker in the returned pattern. 272 */ 273 private void setDividerText() { 274 final String skeleton = (mIs24HourView) ? "Hm" : "hm"; 275 final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, 276 skeleton); 277 final String separatorText; 278 int hourIndex = bestDateTimePattern.lastIndexOf('H'); 279 if (hourIndex == -1) { 280 hourIndex = bestDateTimePattern.lastIndexOf('h'); 281 } 282 if (hourIndex == -1) { 283 // Default case 284 separatorText = ":"; 285 } else { 286 int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); 287 if (minuteIndex == -1) { 288 separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); 289 } else { 290 separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); 291 } 292 } 293 mDivider.setText(separatorText); 294 } 295 296 @Override 297 public void setCurrentHour(Integer currentHour) { 298 setCurrentHour(currentHour, true); 299 } 300 301 private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { 302 // why was Integer used in the first place? 303 if (currentHour == null || currentHour == getCurrentHour()) { 304 return; 305 } 306 if (!is24HourView()) { 307 // convert [0,23] ordinal to wall clock display 308 if (currentHour >= HOURS_IN_HALF_DAY) { 309 mIsAm = false; 310 if (currentHour > HOURS_IN_HALF_DAY) { 311 currentHour = currentHour - HOURS_IN_HALF_DAY; 312 } 313 } else { 314 mIsAm = true; 315 if (currentHour == 0) { 316 currentHour = HOURS_IN_HALF_DAY; 317 } 318 } 319 updateAmPmControl(); 320 } 321 mHourSpinner.setValue(currentHour); 322 if (notifyTimeChanged) { 323 onTimeChanged(); 324 } 325 } 326 327 @Override 328 public Integer getCurrentHour() { 329 int currentHour = mHourSpinner.getValue(); 330 if (is24HourView()) { 331 return currentHour; 332 } else if (mIsAm) { 333 return currentHour % HOURS_IN_HALF_DAY; 334 } else { 335 return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; 336 } 337 } 338 339 @Override 340 public void setCurrentMinute(Integer currentMinute) { 341 if (currentMinute == getCurrentMinute()) { 342 return; 343 } 344 mMinuteSpinner.setValue(currentMinute); 345 onTimeChanged(); 346 } 347 348 @Override 349 public Integer getCurrentMinute() { 350 return mMinuteSpinner.getValue(); 351 } 352 353 @Override 354 public void setIs24HourView(Boolean is24HourView) { 355 if (mIs24HourView == is24HourView) { 356 return; 357 } 358 // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! 359 int currentHour = getCurrentHour(); 360 // Order is important here. 361 mIs24HourView = is24HourView; 362 getHourFormatData(); 363 updateHourControl(); 364 // set value after spinner range is updated 365 setCurrentHour(currentHour, false); 366 updateMinuteControl(); 367 updateAmPmControl(); 368 } 369 370 @Override 371 public boolean is24HourView() { 372 return mIs24HourView; 373 } 374 375 @Override 376 public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) { 377 mOnTimeChangedListener = onTimeChangedListener; 378 } 379 380 @Override 381 public void setEnabled(boolean enabled) { 382 mMinuteSpinner.setEnabled(enabled); 383 if (mDivider != null) { 384 mDivider.setEnabled(enabled); 385 } 386 mHourSpinner.setEnabled(enabled); 387 if (mAmPmSpinner != null) { 388 mAmPmSpinner.setEnabled(enabled); 389 } else { 390 mAmPmButton.setEnabled(enabled); 391 } 392 mIsEnabled = enabled; 393 } 394 395 @Override 396 public boolean isEnabled() { 397 return mIsEnabled; 398 } 399 400 @Override 401 public int getBaseline() { 402 return mHourSpinner.getBaseline(); 403 } 404 405 @Override 406 public void onConfigurationChanged(Configuration newConfig) { 407 setCurrentLocale(newConfig.locale); 408 } 409 410 @Override 411 public Parcelable onSaveInstanceState(Parcelable superState) { 412 return new SavedState(superState, getCurrentHour(), getCurrentMinute()); 413 } 414 415 @Override 416 public void onRestoreInstanceState(Parcelable state) { 417 SavedState ss = (SavedState) state; 418 setCurrentHour(ss.getHour()); 419 setCurrentMinute(ss.getMinute()); 420 } 421 422 @Override 423 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 424 onPopulateAccessibilityEvent(event); 425 return true; 426 } 427 428 @Override 429 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 430 int flags = DateUtils.FORMAT_SHOW_TIME; 431 if (mIs24HourView) { 432 flags |= DateUtils.FORMAT_24HOUR; 433 } else { 434 flags |= DateUtils.FORMAT_12HOUR; 435 } 436 mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); 437 mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); 438 String selectedDateUtterance = DateUtils.formatDateTime(mContext, 439 mTempCalendar.getTimeInMillis(), flags); 440 event.getText().add(selectedDateUtterance); 441 } 442 443 @Override 444 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 445 event.setClassName(TimePicker.class.getName()); 446 } 447 448 @Override 449 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 450 info.setClassName(TimePicker.class.getName()); 451 } 452 453 private void updateInputState() { 454 // Make sure that if the user changes the value and the IME is active 455 // for one of the inputs if this widget, the IME is closed. If the user 456 // changed the value via the IME and there is a next input the IME will 457 // be shown, otherwise the user chose another means of changing the 458 // value and having the IME up makes no sense. 459 InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); 460 if (inputMethodManager != null) { 461 if (inputMethodManager.isActive(mHourSpinnerInput)) { 462 mHourSpinnerInput.clearFocus(); 463 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); 464 } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { 465 mMinuteSpinnerInput.clearFocus(); 466 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); 467 } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { 468 mAmPmSpinnerInput.clearFocus(); 469 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); 470 } 471 } 472 } 473 474 private void updateAmPmControl() { 475 if (is24HourView()) { 476 if (mAmPmSpinner != null) { 477 mAmPmSpinner.setVisibility(View.GONE); 478 } else { 479 mAmPmButton.setVisibility(View.GONE); 480 } 481 } else { 482 int index = mIsAm ? Calendar.AM : Calendar.PM; 483 if (mAmPmSpinner != null) { 484 mAmPmSpinner.setValue(index); 485 mAmPmSpinner.setVisibility(View.VISIBLE); 486 } else { 487 mAmPmButton.setText(mAmPmStrings[index]); 488 mAmPmButton.setVisibility(View.VISIBLE); 489 } 490 } 491 mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 492 } 493 494 /** 495 * Sets the current locale. 496 * 497 * @param locale The current locale. 498 */ 499 @Override 500 public void setCurrentLocale(Locale locale) { 501 super.setCurrentLocale(locale); 502 mTempCalendar = Calendar.getInstance(locale); 503 } 504 505 private void onTimeChanged() { 506 mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 507 if (mOnTimeChangedListener != null) { 508 mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), 509 getCurrentMinute()); 510 } 511 } 512 513 private void updateHourControl() { 514 if (is24HourView()) { 515 // 'k' means 1-24 hour 516 if (mHourFormat == 'k') { 517 mHourSpinner.setMinValue(1); 518 mHourSpinner.setMaxValue(24); 519 } else { 520 mHourSpinner.setMinValue(0); 521 mHourSpinner.setMaxValue(23); 522 } 523 } else { 524 // 'K' means 0-11 hour 525 if (mHourFormat == 'K') { 526 mHourSpinner.setMinValue(0); 527 mHourSpinner.setMaxValue(11); 528 } else { 529 mHourSpinner.setMinValue(1); 530 mHourSpinner.setMaxValue(12); 531 } 532 } 533 mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); 534 } 535 536 private void updateMinuteControl() { 537 if (is24HourView()) { 538 mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); 539 } else { 540 mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); 541 } 542 } 543 544 private void setContentDescriptions() { 545 // Minute 546 trySetContentDescription(mMinuteSpinner, R.id.increment, 547 R.string.time_picker_increment_minute_button); 548 trySetContentDescription(mMinuteSpinner, R.id.decrement, 549 R.string.time_picker_decrement_minute_button); 550 // Hour 551 trySetContentDescription(mHourSpinner, R.id.increment, 552 R.string.time_picker_increment_hour_button); 553 trySetContentDescription(mHourSpinner, R.id.decrement, 554 R.string.time_picker_decrement_hour_button); 555 // AM/PM 556 if (mAmPmSpinner != null) { 557 trySetContentDescription(mAmPmSpinner, R.id.increment, 558 R.string.time_picker_increment_set_pm_button); 559 trySetContentDescription(mAmPmSpinner, R.id.decrement, 560 R.string.time_picker_decrement_set_am_button); 561 } 562 } 563 564 private void trySetContentDescription(View root, int viewId, int contDescResId) { 565 View target = root.findViewById(viewId); 566 if (target != null) { 567 target.setContentDescription(mContext.getString(contDescResId)); 568 } 569 } 570 571 /** 572 * Used to save / restore state of time picker 573 */ 574 private static class SavedState extends View.BaseSavedState { 575 private final int mHour; 576 private final int mMinute; 577 578 private SavedState(Parcelable superState, int hour, int minute) { 579 super(superState); 580 mHour = hour; 581 mMinute = minute; 582 } 583 584 private SavedState(Parcel in) { 585 super(in); 586 mHour = in.readInt(); 587 mMinute = in.readInt(); 588 } 589 590 public int getHour() { 591 return mHour; 592 } 593 594 public int getMinute() { 595 return mMinute; 596 } 597 598 @Override 599 public void writeToParcel(Parcel dest, int flags) { 600 super.writeToParcel(dest, flags); 601 dest.writeInt(mHour); 602 dest.writeInt(mMinute); 603 } 604 605 @SuppressWarnings({"unused", "hiding"}) 606 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { 607 public SavedState createFromParcel(Parcel in) { 608 return new SavedState(in); 609 } 610 611 public SavedState[] newArray(int size) { 612 return new SavedState[size]; 613 } 614 }; 615 } 616} 617 618