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