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