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