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