1 /* 2 * Copyright (C) 2007 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.annotation.Widget; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.content.res.TypedArray; 23 import android.os.Parcel; 24 import android.os.Parcelable; 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.accessibility.AccessibilityEvent; 30 import android.view.accessibility.AccessibilityNodeInfo; 31 import android.view.inputmethod.EditorInfo; 32 import android.view.inputmethod.InputMethodManager; 33 import android.widget.NumberPicker.OnValueChangeListener; 34 35 import com.android.internal.R; 36 37 import java.text.DateFormatSymbols; 38 import java.util.Calendar; 39 import java.util.Locale; 40 41 /** 42 * A view for selecting the time of day, in either 24 hour or AM/PM mode. The 43 * hour, each minute digit, and AM/PM (if applicable) can be conrolled by 44 * vertical spinners. The hour can be entered by keyboard input. Entering in two 45 * digit hours can be accomplished by hitting two digits within a timeout of 46 * about a second (e.g. '1' then '2' to select 12). The minutes can be entered 47 * by entering single digits. Under AM/PM mode, the user can hit 'a', 'A", 'p' 48 * or 'P' to pick. For a dialog using this view, see 49 * {@link android.app.TimePickerDialog}. 50 *<p> 51 * See the <a href="{@docRoot}resources/tutorials/views/hello-timepicker.html">Time Picker 52 * tutorial</a>. 53 * </p> 54 */ 55 @Widget 56 public class TimePicker extends FrameLayout { 57 58 private static final boolean DEFAULT_ENABLED_STATE = true; 59 60 private static final int HOURS_IN_HALF_DAY = 12; 61 62 /** 63 * A no-op callback used in the constructor to avoid null checks later in 64 * the code. 65 */ 66 private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER = new OnTimeChangedListener() { 67 public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { 68 } 69 }; 70 71 // state 72 private boolean mIs24HourView; 73 74 private boolean mIsAm; 75 76 // ui components 77 private final NumberPicker mHourSpinner; 78 79 private final NumberPicker mMinuteSpinner; 80 81 private final NumberPicker mAmPmSpinner; 82 83 private final EditText mHourSpinnerInput; 84 85 private final EditText mMinuteSpinnerInput; 86 87 private final EditText mAmPmSpinnerInput; 88 89 private final TextView mDivider; 90 91 // Note that the legacy implementation of the TimePicker is 92 // using a button for toggling between AM/PM while the new 93 // version uses a NumberPicker spinner. Therefore the code 94 // accommodates these two cases to be backwards compatible. 95 private final Button mAmPmButton; 96 97 private final String[] mAmPmStrings; 98 99 private boolean mIsEnabled = DEFAULT_ENABLED_STATE; 100 101 // callbacks 102 private OnTimeChangedListener mOnTimeChangedListener; 103 104 private Calendar mTempCalendar; 105 106 private Locale mCurrentLocale; 107 108 /** 109 * The callback interface used to indicate the time has been adjusted. 110 */ 111 public interface OnTimeChangedListener { 112 113 /** 114 * @param view The view associated with this listener. 115 * @param hourOfDay The current hour. 116 * @param minute The current minute. 117 */ onTimeChanged(TimePicker view, int hourOfDay, int minute)118 void onTimeChanged(TimePicker view, int hourOfDay, int minute); 119 } 120 TimePicker(Context context)121 public TimePicker(Context context) { 122 this(context, null); 123 } 124 TimePicker(Context context, AttributeSet attrs)125 public TimePicker(Context context, AttributeSet attrs) { 126 this(context, attrs, R.attr.timePickerStyle); 127 } 128 TimePicker(Context context, AttributeSet attrs, int defStyle)129 public TimePicker(Context context, AttributeSet attrs, int defStyle) { 130 super(context, attrs, defStyle); 131 132 // initialization based on locale 133 setCurrentLocale(Locale.getDefault()); 134 135 // process style attributes 136 TypedArray attributesArray = context.obtainStyledAttributes( 137 attrs, R.styleable.TimePicker, defStyle, 0); 138 int layoutResourceId = attributesArray.getResourceId( 139 R.styleable.TimePicker_internalLayout, R.layout.time_picker); 140 attributesArray.recycle(); 141 142 LayoutInflater inflater = (LayoutInflater) context.getSystemService( 143 Context.LAYOUT_INFLATER_SERVICE); 144 inflater.inflate(layoutResourceId, this, true); 145 146 // hour 147 mHourSpinner = (NumberPicker) findViewById(R.id.hour); 148 mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { 149 public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { 150 updateInputState(); 151 if (!is24HourView()) { 152 if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) 153 || (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { 154 mIsAm = !mIsAm; 155 updateAmPmControl(); 156 } 157 } 158 onTimeChanged(); 159 } 160 }); 161 mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); 162 mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); 163 164 // divider (only for the new widget style) 165 mDivider = (TextView) findViewById(R.id.divider); 166 if (mDivider != null) { 167 mDivider.setText(R.string.time_picker_separator); 168 } 169 170 // minute 171 mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); 172 mMinuteSpinner.setMinValue(0); 173 mMinuteSpinner.setMaxValue(59); 174 mMinuteSpinner.setOnLongPressUpdateInterval(100); 175 mMinuteSpinner.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); 176 mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { 177 public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { 178 updateInputState(); 179 int minValue = mMinuteSpinner.getMinValue(); 180 int maxValue = mMinuteSpinner.getMaxValue(); 181 if (oldVal == maxValue && newVal == minValue) { 182 int newHour = mHourSpinner.getValue() + 1; 183 if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { 184 mIsAm = !mIsAm; 185 updateAmPmControl(); 186 } 187 mHourSpinner.setValue(newHour); 188 } else if (oldVal == minValue && newVal == maxValue) { 189 int newHour = mHourSpinner.getValue() - 1; 190 if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { 191 mIsAm = !mIsAm; 192 updateAmPmControl(); 193 } 194 mHourSpinner.setValue(newHour); 195 } 196 onTimeChanged(); 197 } 198 }); 199 mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); 200 mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); 201 202 /* Get the localized am/pm strings and use them in the spinner */ 203 mAmPmStrings = new DateFormatSymbols().getAmPmStrings(); 204 205 // am/pm 206 View amPmView = findViewById(R.id.amPm); 207 if (amPmView instanceof Button) { 208 mAmPmSpinner = null; 209 mAmPmSpinnerInput = null; 210 mAmPmButton = (Button) amPmView; 211 mAmPmButton.setOnClickListener(new OnClickListener() { 212 public void onClick(View button) { 213 button.requestFocus(); 214 mIsAm = !mIsAm; 215 updateAmPmControl(); 216 onTimeChanged(); 217 } 218 }); 219 } else { 220 mAmPmButton = null; 221 mAmPmSpinner = (NumberPicker) amPmView; 222 mAmPmSpinner.setMinValue(0); 223 mAmPmSpinner.setMaxValue(1); 224 mAmPmSpinner.setDisplayedValues(mAmPmStrings); 225 mAmPmSpinner.setOnValueChangedListener(new OnValueChangeListener() { 226 public void onValueChange(NumberPicker picker, int oldVal, int newVal) { 227 updateInputState(); 228 picker.requestFocus(); 229 mIsAm = !mIsAm; 230 updateAmPmControl(); 231 onTimeChanged(); 232 } 233 }); 234 mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); 235 mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); 236 } 237 238 // update controls to initial state 239 updateHourControl(); 240 updateAmPmControl(); 241 242 setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); 243 244 // set to current time 245 setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); 246 setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); 247 248 if (!isEnabled()) { 249 setEnabled(false); 250 } 251 252 // set the content descriptions 253 setContentDescriptions(); 254 255 // If not explicitly specified this view is important for accessibility. 256 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 257 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 258 } 259 } 260 261 @Override setEnabled(boolean enabled)262 public void setEnabled(boolean enabled) { 263 if (mIsEnabled == enabled) { 264 return; 265 } 266 super.setEnabled(enabled); 267 mMinuteSpinner.setEnabled(enabled); 268 if (mDivider != null) { 269 mDivider.setEnabled(enabled); 270 } 271 mHourSpinner.setEnabled(enabled); 272 if (mAmPmSpinner != null) { 273 mAmPmSpinner.setEnabled(enabled); 274 } else { 275 mAmPmButton.setEnabled(enabled); 276 } 277 mIsEnabled = enabled; 278 } 279 280 @Override isEnabled()281 public boolean isEnabled() { 282 return mIsEnabled; 283 } 284 285 @Override onConfigurationChanged(Configuration newConfig)286 protected void onConfigurationChanged(Configuration newConfig) { 287 super.onConfigurationChanged(newConfig); 288 setCurrentLocale(newConfig.locale); 289 } 290 291 /** 292 * Sets the current locale. 293 * 294 * @param locale The current locale. 295 */ setCurrentLocale(Locale locale)296 private void setCurrentLocale(Locale locale) { 297 if (locale.equals(mCurrentLocale)) { 298 return; 299 } 300 mCurrentLocale = locale; 301 mTempCalendar = Calendar.getInstance(locale); 302 } 303 304 /** 305 * Used to save / restore state of time picker 306 */ 307 private static class SavedState extends BaseSavedState { 308 309 private final int mHour; 310 311 private final int mMinute; 312 SavedState(Parcelable superState, int hour, int minute)313 private SavedState(Parcelable superState, int hour, int minute) { 314 super(superState); 315 mHour = hour; 316 mMinute = minute; 317 } 318 SavedState(Parcel in)319 private SavedState(Parcel in) { 320 super(in); 321 mHour = in.readInt(); 322 mMinute = in.readInt(); 323 } 324 getHour()325 public int getHour() { 326 return mHour; 327 } 328 getMinute()329 public int getMinute() { 330 return mMinute; 331 } 332 333 @Override writeToParcel(Parcel dest, int flags)334 public void writeToParcel(Parcel dest, int flags) { 335 super.writeToParcel(dest, flags); 336 dest.writeInt(mHour); 337 dest.writeInt(mMinute); 338 } 339 340 @SuppressWarnings({"unused", "hiding"}) 341 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { 342 public SavedState createFromParcel(Parcel in) { 343 return new SavedState(in); 344 } 345 346 public SavedState[] newArray(int size) { 347 return new SavedState[size]; 348 } 349 }; 350 } 351 352 @Override onSaveInstanceState()353 protected Parcelable onSaveInstanceState() { 354 Parcelable superState = super.onSaveInstanceState(); 355 return new SavedState(superState, getCurrentHour(), getCurrentMinute()); 356 } 357 358 @Override onRestoreInstanceState(Parcelable state)359 protected void onRestoreInstanceState(Parcelable state) { 360 SavedState ss = (SavedState) state; 361 super.onRestoreInstanceState(ss.getSuperState()); 362 setCurrentHour(ss.getHour()); 363 setCurrentMinute(ss.getMinute()); 364 } 365 366 /** 367 * Set the callback that indicates the time has been adjusted by the user. 368 * 369 * @param onTimeChangedListener the callback, should not be null. 370 */ setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener)371 public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { 372 mOnTimeChangedListener = onTimeChangedListener; 373 } 374 375 /** 376 * @return The current hour in the range (0-23). 377 */ getCurrentHour()378 public Integer getCurrentHour() { 379 int currentHour = mHourSpinner.getValue(); 380 if (is24HourView()) { 381 return currentHour; 382 } else if (mIsAm) { 383 return currentHour % HOURS_IN_HALF_DAY; 384 } else { 385 return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; 386 } 387 } 388 389 /** 390 * Set the current hour. 391 */ setCurrentHour(Integer currentHour)392 public void setCurrentHour(Integer currentHour) { 393 // why was Integer used in the first place? 394 if (currentHour == null || currentHour == getCurrentHour()) { 395 return; 396 } 397 if (!is24HourView()) { 398 // convert [0,23] ordinal to wall clock display 399 if (currentHour >= HOURS_IN_HALF_DAY) { 400 mIsAm = false; 401 if (currentHour > HOURS_IN_HALF_DAY) { 402 currentHour = currentHour - HOURS_IN_HALF_DAY; 403 } 404 } else { 405 mIsAm = true; 406 if (currentHour == 0) { 407 currentHour = HOURS_IN_HALF_DAY; 408 } 409 } 410 updateAmPmControl(); 411 } 412 mHourSpinner.setValue(currentHour); 413 onTimeChanged(); 414 } 415 416 /** 417 * Set whether in 24 hour or AM/PM mode. 418 * 419 * @param is24HourView True = 24 hour mode. False = AM/PM. 420 */ setIs24HourView(Boolean is24HourView)421 public void setIs24HourView(Boolean is24HourView) { 422 if (mIs24HourView == is24HourView) { 423 return; 424 } 425 mIs24HourView = is24HourView; 426 // cache the current hour since spinner range changes 427 int currentHour = getCurrentHour(); 428 updateHourControl(); 429 // set value after spinner range is updated 430 setCurrentHour(currentHour); 431 updateAmPmControl(); 432 } 433 434 /** 435 * @return true if this is in 24 hour view else false. 436 */ is24HourView()437 public boolean is24HourView() { 438 return mIs24HourView; 439 } 440 441 /** 442 * @return The current minute. 443 */ getCurrentMinute()444 public Integer getCurrentMinute() { 445 return mMinuteSpinner.getValue(); 446 } 447 448 /** 449 * Set the current minute (0-59). 450 */ setCurrentMinute(Integer currentMinute)451 public void setCurrentMinute(Integer currentMinute) { 452 if (currentMinute == getCurrentMinute()) { 453 return; 454 } 455 mMinuteSpinner.setValue(currentMinute); 456 onTimeChanged(); 457 } 458 459 @Override getBaseline()460 public int getBaseline() { 461 return mHourSpinner.getBaseline(); 462 } 463 464 @Override dispatchPopulateAccessibilityEvent(AccessibilityEvent event)465 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 466 onPopulateAccessibilityEvent(event); 467 return true; 468 } 469 470 @Override onPopulateAccessibilityEvent(AccessibilityEvent event)471 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 472 super.onPopulateAccessibilityEvent(event); 473 474 int flags = DateUtils.FORMAT_SHOW_TIME; 475 if (mIs24HourView) { 476 flags |= DateUtils.FORMAT_24HOUR; 477 } else { 478 flags |= DateUtils.FORMAT_12HOUR; 479 } 480 mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); 481 mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); 482 String selectedDateUtterance = DateUtils.formatDateTime(mContext, 483 mTempCalendar.getTimeInMillis(), flags); 484 event.getText().add(selectedDateUtterance); 485 } 486 487 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)488 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 489 super.onInitializeAccessibilityEvent(event); 490 event.setClassName(TimePicker.class.getName()); 491 } 492 493 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)494 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 495 super.onInitializeAccessibilityNodeInfo(info); 496 info.setClassName(TimePicker.class.getName()); 497 } 498 updateHourControl()499 private void updateHourControl() { 500 if (is24HourView()) { 501 mHourSpinner.setMinValue(0); 502 mHourSpinner.setMaxValue(23); 503 mHourSpinner.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); 504 } else { 505 mHourSpinner.setMinValue(1); 506 mHourSpinner.setMaxValue(12); 507 mHourSpinner.setFormatter(null); 508 } 509 } 510 updateAmPmControl()511 private void updateAmPmControl() { 512 if (is24HourView()) { 513 if (mAmPmSpinner != null) { 514 mAmPmSpinner.setVisibility(View.GONE); 515 } else { 516 mAmPmButton.setVisibility(View.GONE); 517 } 518 } else { 519 int index = mIsAm ? Calendar.AM : Calendar.PM; 520 if (mAmPmSpinner != null) { 521 mAmPmSpinner.setValue(index); 522 mAmPmSpinner.setVisibility(View.VISIBLE); 523 } else { 524 mAmPmButton.setText(mAmPmStrings[index]); 525 mAmPmButton.setVisibility(View.VISIBLE); 526 } 527 } 528 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 529 } 530 onTimeChanged()531 private void onTimeChanged() { 532 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 533 if (mOnTimeChangedListener != null) { 534 mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); 535 } 536 } 537 setContentDescriptions()538 private void setContentDescriptions() { 539 // Minute 540 trySetContentDescription(mMinuteSpinner, R.id.increment, 541 R.string.time_picker_increment_minute_button); 542 trySetContentDescription(mMinuteSpinner, R.id.decrement, 543 R.string.time_picker_decrement_minute_button); 544 // Hour 545 trySetContentDescription(mHourSpinner, R.id.increment, 546 R.string.time_picker_increment_hour_button); 547 trySetContentDescription(mHourSpinner, R.id.decrement, 548 R.string.time_picker_decrement_hour_button); 549 // AM/PM 550 if (mAmPmSpinner != null) { 551 trySetContentDescription(mAmPmSpinner, R.id.increment, 552 R.string.time_picker_increment_set_pm_button); 553 trySetContentDescription(mAmPmSpinner, R.id.decrement, 554 R.string.time_picker_decrement_set_am_button); 555 } 556 } 557 trySetContentDescription(View root, int viewId, int contDescResId)558 private void trySetContentDescription(View root, int viewId, int contDescResId) { 559 View target = root.findViewById(viewId); 560 if (target != null) { 561 target.setContentDescription(mContext.getString(contDescResId)); 562 } 563 } 564 updateInputState()565 private void updateInputState() { 566 // Make sure that if the user changes the value and the IME is active 567 // for one of the inputs if this widget, the IME is closed. If the user 568 // changed the value via the IME and there is a next input the IME will 569 // be shown, otherwise the user chose another means of changing the 570 // value and having the IME up makes no sense. 571 InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); 572 if (inputMethodManager != null) { 573 if (inputMethodManager.isActive(mHourSpinnerInput)) { 574 mHourSpinnerInput.clearFocus(); 575 inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 576 } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { 577 mMinuteSpinnerInput.clearFocus(); 578 inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 579 } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { 580 mAmPmSpinnerInput.clearFocus(); 581 inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 582 } 583 } 584 } 585 } 586