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