DatePicker.java revision 4243dc394d89a93cb207efa36e9755c2424d688b
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.TextUtils; 27import android.text.format.DateFormat; 28import android.text.format.DateUtils; 29import android.util.AttributeSet; 30import android.util.Log; 31import android.util.SparseArray; 32import android.view.LayoutInflater; 33import android.widget.NumberPicker.OnValueChangedListener; 34 35import java.text.ParseException; 36import java.text.SimpleDateFormat; 37import java.util.Calendar; 38import java.util.Locale; 39import java.util.TimeZone; 40 41/** 42 * This class is a widget for selecting a date. The date can be selected by a 43 * year, month, and day spinners or a {@link CalendarView}. The set of spinners 44 * and the calendar view are automatically synchronized. The client can 45 * customize whether only the spinners, or only the calendar view, or both to be 46 * displayed. Also the minimal and maximal date from which dates to be selected 47 * can be customized. 48 * <p> 49 * See the <a href="{@docRoot}resources/tutorials/views/hello-datepicker.html">Date 50 * Picker tutorial</a>. 51 * </p> 52 * <p> 53 * For a dialog using this view, see {@link android.app.DatePickerDialog}. 54 * </p> 55 * 56 * @attr ref android.R.styleable#DatePicker_startYear 57 * @attr ref android.R.styleable#DatePicker_endYear 58 * @attr ref android.R.styleable#DatePicker_maxDate 59 * @attr ref android.R.styleable#DatePicker_minDate 60 * @attr ref android.R.styleable#DatePicker_spinnersShown 61 * @attr ref android.R.styleable#DatePicker_calendarViewShown 62 */ 63@Widget 64public class DatePicker extends FrameLayout { 65 66 private static final String LOG_TAG = DatePicker.class.getSimpleName(); 67 68 private static final String DATE_FORMAT = "MM/dd/yyyy"; 69 70 private static final int DEFAULT_START_YEAR = 1900; 71 72 private static final int DEFAULT_END_YEAR = 2100; 73 74 private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true; 75 76 private static final boolean DEFAULT_SPINNERS_SHOWN = true; 77 78 private static final boolean DEFAULT_ENABLED_STATE = true; 79 80 private final NumberPicker mDaySpinner; 81 82 private final LinearLayout mSpinners; 83 84 private final NumberPicker mMonthSpinner; 85 86 private final NumberPicker mYearSpinner; 87 88 private final CalendarView mCalendarView; 89 90 private OnDateChangedListener mOnDateChangedListener; 91 92 private Locale mMonthLocale; 93 94 private final Calendar mTempDate = Calendar.getInstance(); 95 96 private final int mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; 97 98 private final String[] mShortMonths = new String[mNumberOfMonths]; 99 100 private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); 101 102 private final Calendar mMinDate = Calendar.getInstance(); 103 104 private final Calendar mMaxDate = Calendar.getInstance(); 105 106 private final Calendar mCurrentDate = Calendar.getInstance(); 107 108 private boolean mIsEnabled = DEFAULT_ENABLED_STATE; 109 110 /** 111 * The callback used to indicate the user changes\d the date. 112 */ 113 public interface OnDateChangedListener { 114 115 /** 116 * Called upon a date change. 117 * 118 * @param view The view associated with this listener. 119 * @param year The year that was set. 120 * @param monthOfYear The month that was set (0-11) for compatibility 121 * with {@link java.util.Calendar}. 122 * @param dayOfMonth The day of the month that was set. 123 */ 124 void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth); 125 } 126 127 public DatePicker(Context context) { 128 this(context, null); 129 } 130 131 public DatePicker(Context context, AttributeSet attrs) { 132 this(context, attrs, R.attr.datePickerStyle); 133 } 134 135 public DatePicker(Context context, AttributeSet attrs, int defStyle) { 136 super(context, attrs, defStyle); 137 138 TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, 139 defStyle, 0); 140 boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown, 141 DEFAULT_SPINNERS_SHOWN); 142 boolean calendarViewShown = attributesArray.getBoolean( 143 R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN); 144 int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear, 145 DEFAULT_START_YEAR); 146 int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); 147 String minDate = attributesArray.getString(R.styleable.DatePicker_minDate); 148 String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate); 149 int layoutResourceId = attributesArray.getResourceId(R.styleable.DatePicker_layout, 150 R.layout.date_picker); 151 attributesArray.recycle(); 152 153 LayoutInflater inflater = (LayoutInflater) context 154 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 155 inflater.inflate(layoutResourceId, this, true); 156 157 OnValueChangedListener onChangeListener = new OnValueChangedListener() { 158 public void onValueChange(NumberPicker picker, int oldVal, int newVal) { 159 updateDate(mYearSpinner.getValue(), mMonthSpinner.getValue(), mDaySpinner 160 .getValue()); 161 } 162 }; 163 164 mSpinners = (LinearLayout) findViewById(R.id.pickers); 165 166 // calendar view day-picker 167 mCalendarView = (CalendarView) findViewById(R.id.calendar_view); 168 mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { 169 public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) { 170 updateDate(year, month, monthDay); 171 } 172 }); 173 174 // day 175 mDaySpinner = (NumberPicker) findViewById(R.id.day); 176 mDaySpinner.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); 177 mDaySpinner.setOnLongPressUpdateInterval(100); 178 mDaySpinner.setOnValueChangedListener(onChangeListener); 179 180 // month 181 mMonthSpinner = (NumberPicker) findViewById(R.id.month); 182 mMonthSpinner.setMinValue(0); 183 mMonthSpinner.setMaxValue(mNumberOfMonths - 1); 184 mMonthSpinner.setDisplayedValues(getShortMonths()); 185 mMonthSpinner.setOnLongPressUpdateInterval(200); 186 mMonthSpinner.setOnValueChangedListener(onChangeListener); 187 188 // year 189 mYearSpinner = (NumberPicker) findViewById(R.id.year); 190 mYearSpinner.setOnLongPressUpdateInterval(100); 191 mYearSpinner.setOnValueChangedListener(onChangeListener); 192 193 // show only what the user required but make sure we 194 // show something and the spinners have higher priority 195 if (!spinnersShown && !calendarViewShown) { 196 setSpinnersShown(true); 197 } else { 198 setSpinnersShown(spinnersShown); 199 setCalendarViewShown(calendarViewShown); 200 201 // set the min date giving priority of the minDate over startYear 202 mTempDate.clear(); 203 if (!TextUtils.isEmpty(minDate)) { 204 if (!parseDate(minDate, mTempDate)) { 205 mTempDate.set(startYear, 0, 1); 206 } 207 } else { 208 mTempDate.set(startYear, 0, 1); 209 } 210 mMinDate.clear(); 211 setMinDate(mTempDate.getTimeInMillis()); 212 213 // set the max date giving priority of the minDate over startYear 214 mTempDate.clear(); 215 if (!TextUtils.isEmpty(maxDate)) { 216 if (!parseDate(maxDate, mTempDate)) { 217 mTempDate.set(endYear, 11, 31); 218 } 219 } else { 220 mTempDate.set(endYear, 11, 31); 221 } 222 mMaxDate.clear(); 223 setMaxDate(mTempDate.getTimeInMillis()); 224 225 // initialize to current date 226 mCurrentDate.setTimeInMillis(System.currentTimeMillis()); 227 init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate 228 .get(Calendar.DAY_OF_MONTH), null); 229 } 230 231 // re-order the number spinners to match the current date format 232 reorderSpinners(); 233 } 234 235 /** 236 * Gets the minimal date supported by this {@link DatePicker} in 237 * milliseconds since January 1, 1970 00:00:00 in 238 * {@link TimeZone#getDefault()} time zone. 239 * <p> 240 * Note: The default minimal date is 01/01/1900. 241 * <p> 242 * 243 * @return The minimal supported date. 244 */ 245 public long getMinDate() { 246 return mCalendarView.getMinDate(); 247 } 248 249 /** 250 * Sets the minimal date supported by this {@link NumberPicker} in 251 * milliseconds since January 1, 1970 00:00:00 in 252 * {@link TimeZone#getDefault()} time zone. 253 * 254 * @param minDate The minimal supported date. 255 */ 256 public void setMinDate(long minDate) { 257 mTempDate.setTimeInMillis(minDate); 258 if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) 259 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { 260 return; 261 } 262 mMinDate.setTimeInMillis(minDate); 263 mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); 264 mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); 265 mCalendarView.setMinDate(minDate); 266 updateSpinners(mYearSpinner.getValue(), mMonthSpinner.getValue(), mDaySpinner.getValue()); 267 } 268 269 /** 270 * Gets the maximal date supported by this {@link DatePicker} in 271 * milliseconds since January 1, 1970 00:00:00 in 272 * {@link TimeZone#getDefault()} time zone. 273 * <p> 274 * Note: The default maximal date is 12/31/2100. 275 * <p> 276 * 277 * @return The maximal supported date. 278 */ 279 public long getMaxDate() { 280 return mCalendarView.getMaxDate(); 281 } 282 283 /** 284 * Sets the maximal date supported by this {@link DatePicker} in 285 * milliseconds since January 1, 1970 00:00:00 in 286 * {@link TimeZone#getDefault()} time zone. 287 * 288 * @param maxDate The maximal supported date. 289 */ 290 public void setMaxDate(long maxDate) { 291 mTempDate.setTimeInMillis(maxDate); 292 if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) 293 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { 294 return; 295 } 296 mMaxDate.setTimeInMillis(maxDate); 297 mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); 298 mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); 299 mCalendarView.setMaxDate(maxDate); 300 updateSpinners(mYearSpinner.getValue(), mMonthSpinner.getValue(), mDaySpinner.getValue()); 301 } 302 303 @Override 304 public void setEnabled(boolean enabled) { 305 if (mIsEnabled == enabled) { 306 return; 307 } 308 super.setEnabled(enabled); 309 mDaySpinner.setEnabled(enabled); 310 mMonthSpinner.setEnabled(enabled); 311 mYearSpinner.setEnabled(enabled); 312 mCalendarView.setEnabled(enabled); 313 mIsEnabled = enabled; 314 } 315 316 @Override 317 public boolean isEnabled() { 318 return mIsEnabled; 319 } 320 321 /** 322 * Gets whether the {@link CalendarView} is shown. 323 * 324 * @return True if the calendar view is shown. 325 */ 326 public boolean getCalendarViewShown() { 327 return mCalendarView.isShown(); 328 } 329 330 /** 331 * Sets whether the {@link CalendarView} is shown. 332 * 333 * @param shown True if the calendar view is to be shown. 334 */ 335 public void setCalendarViewShown(boolean shown) { 336 mCalendarView.setVisibility(shown ? VISIBLE : GONE); 337 } 338 339 /** 340 * Gets whether the spinners are shown. 341 * 342 * @return True if the spinners are shown. 343 */ 344 public boolean getSpinnersShown() { 345 return mSpinners.isShown(); 346 } 347 348 /** 349 * Sets whether the spinners are shown. 350 * 351 * @param shown True if the spinners are to be shown. 352 */ 353 public void setSpinnersShown(boolean shown) { 354 mSpinners.setVisibility(shown ? VISIBLE : GONE); 355 } 356 357 /** 358 * Reorders the spinners according to the date format in the current 359 * {@link Locale}. 360 */ 361 private void reorderSpinners() { 362 java.text.DateFormat format; 363 String order; 364 365 /* 366 * If the user is in a locale where the medium date format is still 367 * numeric (Japanese and Czech, for example), respect the date format 368 * order setting. Otherwise, use the order that the locale says is 369 * appropriate for a spelled-out date. 370 */ 371 372 if (getShortMonths()[0].startsWith("1")) { 373 format = DateFormat.getDateFormat(getContext()); 374 } else { 375 format = DateFormat.getMediumDateFormat(getContext()); 376 } 377 378 if (format instanceof SimpleDateFormat) { 379 order = ((SimpleDateFormat) format).toPattern(); 380 } else { 381 // Shouldn't happen, but just in case. 382 order = new String(DateFormat.getDateFormatOrder(getContext())); 383 } 384 385 /* 386 * Remove the 3 spinners from their parent and then add them back in the 387 * required order. 388 */ 389 LinearLayout parent = mSpinners; 390 parent.removeAllViews(); 391 392 boolean quoted = false; 393 boolean didDay = false, didMonth = false, didYear = false; 394 395 for (int i = 0; i < order.length(); i++) { 396 char c = order.charAt(i); 397 398 if (c == '\'') { 399 quoted = !quoted; 400 } 401 402 if (!quoted) { 403 if (c == DateFormat.DATE && !didDay) { 404 parent.addView(mDaySpinner); 405 didDay = true; 406 } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) { 407 parent.addView(mMonthSpinner); 408 didMonth = true; 409 } else if (c == DateFormat.YEAR && !didYear) { 410 parent.addView(mYearSpinner); 411 didYear = true; 412 } 413 } 414 } 415 416 // Shouldn't happen, but just in case. 417 if (!didMonth) { 418 parent.addView(mMonthSpinner); 419 } 420 if (!didDay) { 421 parent.addView(mDaySpinner); 422 } 423 if (!didYear) { 424 parent.addView(mYearSpinner); 425 } 426 } 427 428 /** 429 * Updates the current date. 430 * 431 * @param year The year. 432 * @param month The month which is <strong>starting from zero</strong>. 433 * @param dayOfMonth The day of the month. 434 */ 435 public void updateDate(int year, int month, int dayOfMonth) { 436 if (mCurrentDate.get(Calendar.YEAR) != year 437 || mCurrentDate.get(Calendar.MONTH) != dayOfMonth 438 || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month) { 439 updateSpinners(year, month, dayOfMonth); 440 updateCalendarView(); 441 notifyDateChanged(); 442 } 443 } 444 445 // Override so we are in complete control of save / restore for this widget. 446 @Override 447 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 448 dispatchThawSelfOnly(container); 449 } 450 451 @Override 452 protected Parcelable onSaveInstanceState() { 453 Parcelable superState = super.onSaveInstanceState(); 454 return new SavedState(superState, mYearSpinner.getValue(), mMonthSpinner.getValue(), 455 mDaySpinner.getValue()); 456 } 457 458 @Override 459 protected void onRestoreInstanceState(Parcelable state) { 460 SavedState ss = (SavedState) state; 461 super.onRestoreInstanceState(ss.getSuperState()); 462 updateSpinners(ss.mYear, ss.mMonth, ss.mDay); 463 } 464 465 /** 466 * Initialize the state. If the provided values designate an inconsistent 467 * date the values are normalized before updating the spinners. 468 * 469 * @param year The initial year. 470 * @param monthOfYear The initial month <strong>starting from zero</strong>. 471 * @param dayOfMonth The initial day of the month. 472 * @param onDateChangedListener How user is notified date is changed by 473 * user, can be null. 474 */ 475 public void init(int year, int monthOfYear, int dayOfMonth, 476 OnDateChangedListener onDateChangedListener) { 477 // make sure there is no callback 478 mOnDateChangedListener = null; 479 updateDate(year, monthOfYear, dayOfMonth); 480 // register the callback after updating the date 481 mOnDateChangedListener = onDateChangedListener; 482 } 483 484 /** 485 * Parses the given <code>date</code> and in case of success sets the result 486 * to the <code>outDate</code>. 487 * 488 * @return True if the date was parsed. 489 */ 490 private boolean parseDate(String date, Calendar outDate) { 491 try { 492 outDate.setTime(mDateFormat.parse(date)); 493 return true; 494 } catch (ParseException e) { 495 Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); 496 return false; 497 } 498 } 499 500 /** 501 * @return The short month abbreviations. 502 */ 503 private String[] getShortMonths() { 504 final Locale currentLocale = Locale.getDefault(); 505 if (currentLocale.equals(mMonthLocale)) { 506 return mShortMonths; 507 } else { 508 for (int i = 0; i < mNumberOfMonths; i++) { 509 mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i, 510 DateUtils.LENGTH_MEDIUM); 511 } 512 mMonthLocale = currentLocale; 513 return mShortMonths; 514 } 515 } 516 517 /** 518 * Updates the spinners with the given <code>year</code>, <code>month</code> 519 * , and <code>dayOfMonth</code>. If the provided values designate an 520 * inconsistent date the values are normalized before updating the spinners. 521 */ 522 private void updateSpinners(int year, int month, int dayOfMonth) { 523 // compute the deltas before modifying the current date 524 int deltaMonths = getDelataMonth(month); 525 int deltaDays = getDelataDayOfMonth(dayOfMonth); 526 mCurrentDate.set(Calendar.YEAR, year); 527 mCurrentDate.add(Calendar.MONTH, deltaMonths); 528 mCurrentDate.add(Calendar.DAY_OF_MONTH, deltaDays); 529 530 if (mCurrentDate.before(mMinDate)) { 531 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); 532 } else if (mCurrentDate.after(mMaxDate)) { 533 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); 534 } 535 536 mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); 537 mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); 538 mDaySpinner.setMinValue(1); 539 mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); 540 mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); 541 } 542 543 /** 544 * @return The delta days of moth from the current date and the given 545 * <code>dayOfMonth</code>. 546 */ 547 private int getDelataDayOfMonth(int dayOfMonth) { 548 int prevDayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH); 549 if (prevDayOfMonth == dayOfMonth) { 550 return 0; 551 } 552 int maxDayOfMonth = mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH); 553 if (dayOfMonth == 1 && prevDayOfMonth == maxDayOfMonth) { 554 return 1; 555 } 556 if (dayOfMonth == maxDayOfMonth && prevDayOfMonth == 1) { 557 return -1; 558 } 559 return dayOfMonth - prevDayOfMonth; 560 } 561 562 /** 563 * @return The delta months from the current date and the given 564 * <code>month</code>. 565 */ 566 private int getDelataMonth(int month) { 567 int prevMonth = mCurrentDate.get(Calendar.MONTH); 568 if (prevMonth == month) { 569 return 0; 570 } 571 if (month == 0 && prevMonth == 11) { 572 return 1; 573 } 574 if (month == 11 && prevMonth == 0) { 575 return -1; 576 } 577 return month - prevMonth; 578 } 579 580 /** 581 * Updates the calendar view with the given year, month, and day selected by 582 * the number spinners. 583 */ 584 private void updateCalendarView() { 585 mTempDate.setTimeInMillis(mCalendarView.getDate()); 586 if (mTempDate.get(Calendar.YEAR) != mYearSpinner.getValue() 587 || mTempDate.get(Calendar.MONTH) != mMonthSpinner.getValue() 588 || mTempDate.get(Calendar.DAY_OF_MONTH) != mDaySpinner.getValue()) { 589 mTempDate.clear(); 590 mTempDate.set(mYearSpinner.getValue(), mMonthSpinner.getValue(), 591 mDaySpinner.getValue()); 592 mCalendarView.setDate(mTempDate.getTimeInMillis(), false, false); 593 } 594 } 595 596 /** 597 * @return The selected year. 598 */ 599 public int getYear() { 600 return mYearSpinner.getValue(); 601 } 602 603 /** 604 * @return The selected month. 605 */ 606 public int getMonth() { 607 return mMonthSpinner.getValue(); 608 } 609 610 /** 611 * @return The selected day of month. 612 */ 613 public int getDayOfMonth() { 614 return mDaySpinner.getValue(); 615 } 616 617 /** 618 * Notifies the listener, if such, for a change in the selected date. 619 */ 620 private void notifyDateChanged() { 621 if (mOnDateChangedListener != null) { 622 mOnDateChangedListener.onDateChanged(DatePicker.this, mYearSpinner.getValue(), 623 mMonthSpinner.getValue(), mDaySpinner.getValue()); 624 } 625 } 626 627 /** 628 * Class for managing state storing/restoring. 629 */ 630 private static class SavedState extends BaseSavedState { 631 632 private final int mYear; 633 634 private final int mMonth; 635 636 private final int mDay; 637 638 /** 639 * Constructor called from {@link DatePicker#onSaveInstanceState()} 640 */ 641 private SavedState(Parcelable superState, int year, int month, int day) { 642 super(superState); 643 mYear = year; 644 mMonth = month; 645 mDay = day; 646 } 647 648 /** 649 * Constructor called from {@link #CREATOR} 650 */ 651 private SavedState(Parcel in) { 652 super(in); 653 mYear = in.readInt(); 654 mMonth = in.readInt(); 655 mDay = in.readInt(); 656 } 657 658 @Override 659 public void writeToParcel(Parcel dest, int flags) { 660 super.writeToParcel(dest, flags); 661 dest.writeInt(mYear); 662 dest.writeInt(mMonth); 663 dest.writeInt(mDay); 664 } 665 666 @SuppressWarnings("all") 667 // suppress unused and hiding 668 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { 669 670 public SavedState createFromParcel(Parcel in) { 671 return new SavedState(in); 672 } 673 674 public SavedState[] newArray(int size) { 675 return new SavedState[size]; 676 } 677 }; 678 } 679} 680