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