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.Nullable; 20 import android.annotation.Widget; 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.content.res.TypedArray; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.text.TextUtils; 27 import android.text.InputType; 28 import android.text.format.DateFormat; 29 import android.text.format.DateUtils; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.util.SparseArray; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.accessibility.AccessibilityEvent; 36 import android.view.accessibility.AccessibilityNodeInfo; 37 import android.view.inputmethod.EditorInfo; 38 import android.view.inputmethod.InputMethodManager; 39 import android.widget.NumberPicker.OnValueChangeListener; 40 41 import com.android.internal.R; 42 43 import java.text.DateFormatSymbols; 44 import java.text.ParseException; 45 import java.text.SimpleDateFormat; 46 import java.util.Arrays; 47 import java.util.Calendar; 48 import java.util.Locale; 49 import java.util.TimeZone; 50 51 import libcore.icu.ICU; 52 53 /** 54 * This class is a widget for selecting a date. The date can be selected by a 55 * year, month, and day spinners or a {@link CalendarView}. The set of spinners 56 * and the calendar view are automatically synchronized. The client can 57 * customize whether only the spinners, or only the calendar view, or both to be 58 * displayed. Also the minimal and maximal date from which dates to be selected 59 * can be customized. 60 * <p> 61 * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a> 62 * guide. 63 * </p> 64 * <p> 65 * For a dialog using this view, see {@link android.app.DatePickerDialog}. 66 * </p> 67 * 68 * @attr ref android.R.styleable#DatePicker_startYear 69 * @attr ref android.R.styleable#DatePicker_endYear 70 * @attr ref android.R.styleable#DatePicker_maxDate 71 * @attr ref android.R.styleable#DatePicker_minDate 72 * @attr ref android.R.styleable#DatePicker_spinnersShown 73 * @attr ref android.R.styleable#DatePicker_calendarViewShown 74 * @attr ref android.R.styleable#DatePicker_dayOfWeekBackground 75 * @attr ref android.R.styleable#DatePicker_dayOfWeekTextAppearance 76 * @attr ref android.R.styleable#DatePicker_headerBackground 77 * @attr ref android.R.styleable#DatePicker_headerMonthTextAppearance 78 * @attr ref android.R.styleable#DatePicker_headerDayOfMonthTextAppearance 79 * @attr ref android.R.styleable#DatePicker_headerYearTextAppearance 80 * @attr ref android.R.styleable#DatePicker_yearListItemTextAppearance 81 * @attr ref android.R.styleable#DatePicker_yearListSelectorColor 82 * @attr ref android.R.styleable#DatePicker_calendarTextColor 83 */ 84 @Widget 85 public class DatePicker extends FrameLayout { 86 private static final String LOG_TAG = DatePicker.class.getSimpleName(); 87 88 private static final int MODE_SPINNER = 1; 89 private static final int MODE_CALENDAR = 2; 90 91 private final DatePickerDelegate mDelegate; 92 93 /** 94 * The callback used to indicate the user changes\d the date. 95 */ 96 public interface OnDateChangedListener { 97 98 /** 99 * Called upon a date change. 100 * 101 * @param view The view associated with this listener. 102 * @param year The year that was set. 103 * @param monthOfYear The month that was set (0-11) for compatibility 104 * with {@link java.util.Calendar}. 105 * @param dayOfMonth The day of the month that was set. 106 */ 107 void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth); 108 } 109 110 public DatePicker(Context context) { 111 this(context, null); 112 } 113 114 public DatePicker(Context context, AttributeSet attrs) { 115 this(context, attrs, R.attr.datePickerStyle); 116 } 117 118 public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) { 119 this(context, attrs, defStyleAttr, 0); 120 } 121 122 public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 123 super(context, attrs, defStyleAttr, defStyleRes); 124 125 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, 126 defStyleAttr, defStyleRes); 127 final int mode = a.getInt(R.styleable.DatePicker_datePickerMode, MODE_SPINNER); 128 final int firstDayOfWeek = a.getInt(R.styleable.DatePicker_firstDayOfWeek, 0); 129 a.recycle(); 130 131 switch (mode) { 132 case MODE_CALENDAR: 133 mDelegate = createCalendarUIDelegate(context, attrs, defStyleAttr, defStyleRes); 134 break; 135 case MODE_SPINNER: 136 default: 137 mDelegate = createSpinnerUIDelegate(context, attrs, defStyleAttr, defStyleRes); 138 break; 139 } 140 141 if (firstDayOfWeek != 0) { 142 setFirstDayOfWeek(firstDayOfWeek); 143 } 144 } 145 146 private DatePickerDelegate createSpinnerUIDelegate(Context context, AttributeSet attrs, 147 int defStyleAttr, int defStyleRes) { 148 return new DatePickerSpinnerDelegate(this, context, attrs, defStyleAttr, defStyleRes); 149 } 150 151 private DatePickerDelegate createCalendarUIDelegate(Context context, AttributeSet attrs, 152 int defStyleAttr, int defStyleRes) { 153 return new DatePickerCalendarDelegate(this, context, attrs, defStyleAttr, 154 defStyleRes); 155 } 156 157 /** 158 * Initialize the state. If the provided values designate an inconsistent 159 * date the values are normalized before updating the spinners. 160 * 161 * @param year The initial year. 162 * @param monthOfYear The initial month <strong>starting from zero</strong>. 163 * @param dayOfMonth The initial day of the month. 164 * @param onDateChangedListener How user is notified date is changed by 165 * user, can be null. 166 */ 167 public void init(int year, int monthOfYear, int dayOfMonth, 168 OnDateChangedListener onDateChangedListener) { 169 mDelegate.init(year, monthOfYear, dayOfMonth, onDateChangedListener); 170 } 171 172 /** 173 * Update the current date. 174 * 175 * @param year The year. 176 * @param month The month which is <strong>starting from zero</strong>. 177 * @param dayOfMonth The day of the month. 178 */ 179 public void updateDate(int year, int month, int dayOfMonth) { 180 mDelegate.updateDate(year, month, dayOfMonth); 181 } 182 183 /** 184 * @return The selected year. 185 */ 186 public int getYear() { 187 return mDelegate.getYear(); 188 } 189 190 /** 191 * @return The selected month. 192 */ 193 public int getMonth() { 194 return mDelegate.getMonth(); 195 } 196 197 /** 198 * @return The selected day of month. 199 */ 200 public int getDayOfMonth() { 201 return mDelegate.getDayOfMonth(); 202 } 203 204 /** 205 * Gets the minimal date supported by this {@link DatePicker} in 206 * milliseconds since January 1, 1970 00:00:00 in 207 * {@link TimeZone#getDefault()} time zone. 208 * <p> 209 * Note: The default minimal date is 01/01/1900. 210 * <p> 211 * 212 * @return The minimal supported date. 213 */ 214 public long getMinDate() { 215 return mDelegate.getMinDate().getTimeInMillis(); 216 } 217 218 /** 219 * Sets the minimal date supported by this {@link NumberPicker} in 220 * milliseconds since January 1, 1970 00:00:00 in 221 * {@link TimeZone#getDefault()} time zone. 222 * 223 * @param minDate The minimal supported date. 224 */ 225 public void setMinDate(long minDate) { 226 mDelegate.setMinDate(minDate); 227 } 228 229 /** 230 * Gets the maximal date supported by this {@link DatePicker} in 231 * milliseconds since January 1, 1970 00:00:00 in 232 * {@link TimeZone#getDefault()} time zone. 233 * <p> 234 * Note: The default maximal date is 12/31/2100. 235 * <p> 236 * 237 * @return The maximal supported date. 238 */ 239 public long getMaxDate() { 240 return mDelegate.getMaxDate().getTimeInMillis(); 241 } 242 243 /** 244 * Sets the maximal date supported by this {@link DatePicker} in 245 * milliseconds since January 1, 1970 00:00:00 in 246 * {@link TimeZone#getDefault()} time zone. 247 * 248 * @param maxDate The maximal supported date. 249 */ 250 public void setMaxDate(long maxDate) { 251 mDelegate.setMaxDate(maxDate); 252 } 253 254 /** 255 * Sets the callback that indicates the current date is valid. 256 * 257 * @param callback the callback, may be null 258 * @hide 259 */ 260 public void setValidationCallback(@Nullable ValidationCallback callback) { 261 mDelegate.setValidationCallback(callback); 262 } 263 264 @Override 265 public void setEnabled(boolean enabled) { 266 if (mDelegate.isEnabled() == enabled) { 267 return; 268 } 269 super.setEnabled(enabled); 270 mDelegate.setEnabled(enabled); 271 } 272 273 @Override 274 public boolean isEnabled() { 275 return mDelegate.isEnabled(); 276 } 277 278 @Override 279 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 280 return mDelegate.dispatchPopulateAccessibilityEvent(event); 281 } 282 283 @Override 284 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 285 super.onPopulateAccessibilityEvent(event); 286 mDelegate.onPopulateAccessibilityEvent(event); 287 } 288 289 @Override 290 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 291 super.onInitializeAccessibilityEvent(event); 292 mDelegate.onInitializeAccessibilityEvent(event); 293 } 294 295 @Override 296 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 297 super.onInitializeAccessibilityNodeInfo(info); 298 mDelegate.onInitializeAccessibilityNodeInfo(info); 299 } 300 301 @Override 302 protected void onConfigurationChanged(Configuration newConfig) { 303 super.onConfigurationChanged(newConfig); 304 mDelegate.onConfigurationChanged(newConfig); 305 } 306 307 /** 308 * Sets the first day of week. 309 * 310 * @param firstDayOfWeek The first day of the week conforming to the 311 * {@link CalendarView} APIs. 312 * @see Calendar#SUNDAY 313 * @see Calendar#MONDAY 314 * @see Calendar#TUESDAY 315 * @see Calendar#WEDNESDAY 316 * @see Calendar#THURSDAY 317 * @see Calendar#FRIDAY 318 * @see Calendar#SATURDAY 319 * 320 * @attr ref android.R.styleable#DatePicker_firstDayOfWeek 321 */ 322 public void setFirstDayOfWeek(int firstDayOfWeek) { 323 if (firstDayOfWeek < Calendar.SUNDAY || firstDayOfWeek > Calendar.SATURDAY) { 324 throw new IllegalArgumentException("firstDayOfWeek must be between 1 and 7"); 325 } 326 mDelegate.setFirstDayOfWeek(firstDayOfWeek); 327 } 328 329 /** 330 * Gets the first day of week. 331 * 332 * @return The first day of the week conforming to the {@link CalendarView} 333 * APIs. 334 * @see Calendar#SUNDAY 335 * @see Calendar#MONDAY 336 * @see Calendar#TUESDAY 337 * @see Calendar#WEDNESDAY 338 * @see Calendar#THURSDAY 339 * @see Calendar#FRIDAY 340 * @see Calendar#SATURDAY 341 * 342 * @attr ref android.R.styleable#DatePicker_firstDayOfWeek 343 */ 344 public int getFirstDayOfWeek() { 345 return mDelegate.getFirstDayOfWeek(); 346 } 347 348 /** 349 * Gets whether the {@link CalendarView} is shown. 350 * 351 * @return True if the calendar view is shown. 352 * @see #getCalendarView() 353 */ 354 public boolean getCalendarViewShown() { 355 return mDelegate.getCalendarViewShown(); 356 } 357 358 /** 359 * Gets the {@link CalendarView}. 360 * 361 * @return The calendar view. 362 * @see #getCalendarViewShown() 363 */ 364 public CalendarView getCalendarView() { 365 return mDelegate.getCalendarView(); 366 } 367 368 /** 369 * Sets whether the {@link CalendarView} is shown. 370 * 371 * @param shown True if the calendar view is to be shown. 372 */ 373 public void setCalendarViewShown(boolean shown) { 374 mDelegate.setCalendarViewShown(shown); 375 } 376 377 /** 378 * Gets whether the spinners are shown. 379 * 380 * @return True if the spinners are shown. 381 */ 382 public boolean getSpinnersShown() { 383 return mDelegate.getSpinnersShown(); 384 } 385 386 /** 387 * Sets whether the spinners are shown. 388 * 389 * @param shown True if the spinners are to be shown. 390 */ 391 public void setSpinnersShown(boolean shown) { 392 mDelegate.setSpinnersShown(shown); 393 } 394 395 @Override 396 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 397 dispatchThawSelfOnly(container); 398 } 399 400 @Override 401 protected Parcelable onSaveInstanceState() { 402 Parcelable superState = super.onSaveInstanceState(); 403 return mDelegate.onSaveInstanceState(superState); 404 } 405 406 @Override 407 protected void onRestoreInstanceState(Parcelable state) { 408 BaseSavedState ss = (BaseSavedState) state; 409 super.onRestoreInstanceState(ss.getSuperState()); 410 mDelegate.onRestoreInstanceState(ss); 411 } 412 413 /** 414 * A delegate interface that defined the public API of the DatePicker. Allows different 415 * DatePicker implementations. This would need to be implemented by the DatePicker delegates 416 * for the real behavior. 417 * 418 * @hide 419 */ 420 interface DatePickerDelegate { 421 void init(int year, int monthOfYear, int dayOfMonth, 422 OnDateChangedListener onDateChangedListener); 423 424 void updateDate(int year, int month, int dayOfMonth); 425 426 int getYear(); 427 int getMonth(); 428 int getDayOfMonth(); 429 430 void setFirstDayOfWeek(int firstDayOfWeek); 431 int getFirstDayOfWeek(); 432 433 void setMinDate(long minDate); 434 Calendar getMinDate(); 435 436 void setMaxDate(long maxDate); 437 Calendar getMaxDate(); 438 439 void setEnabled(boolean enabled); 440 boolean isEnabled(); 441 442 CalendarView getCalendarView(); 443 444 void setCalendarViewShown(boolean shown); 445 boolean getCalendarViewShown(); 446 447 void setSpinnersShown(boolean shown); 448 boolean getSpinnersShown(); 449 450 void setValidationCallback(ValidationCallback callback); 451 452 void onConfigurationChanged(Configuration newConfig); 453 454 Parcelable onSaveInstanceState(Parcelable superState); 455 void onRestoreInstanceState(Parcelable state); 456 457 boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); 458 void onPopulateAccessibilityEvent(AccessibilityEvent event); 459 void onInitializeAccessibilityEvent(AccessibilityEvent event); 460 void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); 461 } 462 463 /** 464 * An abstract class which can be used as a start for DatePicker implementations 465 */ 466 abstract static class AbstractDatePickerDelegate implements DatePickerDelegate { 467 // The delegator 468 protected DatePicker mDelegator; 469 470 // The context 471 protected Context mContext; 472 473 // The current locale 474 protected Locale mCurrentLocale; 475 476 // Callbacks 477 protected OnDateChangedListener mOnDateChangedListener; 478 protected ValidationCallback mValidationCallback; 479 480 public AbstractDatePickerDelegate(DatePicker delegator, Context context) { 481 mDelegator = delegator; 482 mContext = context; 483 484 // initialization based on locale 485 setCurrentLocale(Locale.getDefault()); 486 } 487 488 protected void setCurrentLocale(Locale locale) { 489 if (locale.equals(mCurrentLocale)) { 490 return; 491 } 492 mCurrentLocale = locale; 493 } 494 495 @Override 496 public void setValidationCallback(ValidationCallback callback) { 497 mValidationCallback = callback; 498 } 499 500 protected void onValidationChanged(boolean valid) { 501 if (mValidationCallback != null) { 502 mValidationCallback.onValidationChanged(valid); 503 } 504 } 505 } 506 507 /** 508 * A callback interface for updating input validity when the date picker 509 * when included into a dialog. 510 * 511 * @hide 512 */ 513 public static interface ValidationCallback { 514 void onValidationChanged(boolean valid); 515 } 516 517 /** 518 * A delegate implementing the basic DatePicker 519 */ 520 private static class DatePickerSpinnerDelegate extends AbstractDatePickerDelegate { 521 522 private static final String DATE_FORMAT = "MM/dd/yyyy"; 523 524 private static final int DEFAULT_START_YEAR = 1900; 525 526 private static final int DEFAULT_END_YEAR = 2100; 527 528 private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true; 529 530 private static final boolean DEFAULT_SPINNERS_SHOWN = true; 531 532 private static final boolean DEFAULT_ENABLED_STATE = true; 533 534 private final LinearLayout mSpinners; 535 536 private final NumberPicker mDaySpinner; 537 538 private final NumberPicker mMonthSpinner; 539 540 private final NumberPicker mYearSpinner; 541 542 private final EditText mDaySpinnerInput; 543 544 private final EditText mMonthSpinnerInput; 545 546 private final EditText mYearSpinnerInput; 547 548 private final CalendarView mCalendarView; 549 550 private String[] mShortMonths; 551 552 private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); 553 554 private int mNumberOfMonths; 555 556 private Calendar mTempDate; 557 558 private Calendar mMinDate; 559 560 private Calendar mMaxDate; 561 562 private Calendar mCurrentDate; 563 564 private boolean mIsEnabled = DEFAULT_ENABLED_STATE; 565 566 DatePickerSpinnerDelegate(DatePicker delegator, Context context, AttributeSet attrs, 567 int defStyleAttr, int defStyleRes) { 568 super(delegator, context); 569 570 mDelegator = delegator; 571 mContext = context; 572 573 // initialization based on locale 574 setCurrentLocale(Locale.getDefault()); 575 576 final TypedArray attributesArray = context.obtainStyledAttributes(attrs, 577 R.styleable.DatePicker, defStyleAttr, defStyleRes); 578 boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown, 579 DEFAULT_SPINNERS_SHOWN); 580 boolean calendarViewShown = attributesArray.getBoolean( 581 R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN); 582 int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear, 583 DEFAULT_START_YEAR); 584 int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); 585 String minDate = attributesArray.getString(R.styleable.DatePicker_minDate); 586 String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate); 587 int layoutResourceId = attributesArray.getResourceId( 588 R.styleable.DatePicker_legacyLayout, R.layout.date_picker_legacy); 589 attributesArray.recycle(); 590 591 LayoutInflater inflater = (LayoutInflater) context 592 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 593 inflater.inflate(layoutResourceId, mDelegator, true); 594 595 OnValueChangeListener onChangeListener = new OnValueChangeListener() { 596 public void onValueChange(NumberPicker picker, int oldVal, int newVal) { 597 updateInputState(); 598 mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); 599 // take care of wrapping of days and months to update greater fields 600 if (picker == mDaySpinner) { 601 int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH); 602 if (oldVal == maxDayOfMonth && newVal == 1) { 603 mTempDate.add(Calendar.DAY_OF_MONTH, 1); 604 } else if (oldVal == 1 && newVal == maxDayOfMonth) { 605 mTempDate.add(Calendar.DAY_OF_MONTH, -1); 606 } else { 607 mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal); 608 } 609 } else if (picker == mMonthSpinner) { 610 if (oldVal == 11 && newVal == 0) { 611 mTempDate.add(Calendar.MONTH, 1); 612 } else if (oldVal == 0 && newVal == 11) { 613 mTempDate.add(Calendar.MONTH, -1); 614 } else { 615 mTempDate.add(Calendar.MONTH, newVal - oldVal); 616 } 617 } else if (picker == mYearSpinner) { 618 mTempDate.set(Calendar.YEAR, newVal); 619 } else { 620 throw new IllegalArgumentException(); 621 } 622 // now set the date to the adjusted one 623 setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH), 624 mTempDate.get(Calendar.DAY_OF_MONTH)); 625 updateSpinners(); 626 updateCalendarView(); 627 notifyDateChanged(); 628 } 629 }; 630 631 mSpinners = (LinearLayout) mDelegator.findViewById(R.id.pickers); 632 633 // calendar view day-picker 634 mCalendarView = (CalendarView) mDelegator.findViewById(R.id.calendar_view); 635 mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { 636 public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) { 637 setDate(year, month, monthDay); 638 updateSpinners(); 639 notifyDateChanged(); 640 } 641 }); 642 643 // day 644 mDaySpinner = (NumberPicker) mDelegator.findViewById(R.id.day); 645 mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); 646 mDaySpinner.setOnLongPressUpdateInterval(100); 647 mDaySpinner.setOnValueChangedListener(onChangeListener); 648 mDaySpinnerInput = (EditText) mDaySpinner.findViewById(R.id.numberpicker_input); 649 650 // month 651 mMonthSpinner = (NumberPicker) mDelegator.findViewById(R.id.month); 652 mMonthSpinner.setMinValue(0); 653 mMonthSpinner.setMaxValue(mNumberOfMonths - 1); 654 mMonthSpinner.setDisplayedValues(mShortMonths); 655 mMonthSpinner.setOnLongPressUpdateInterval(200); 656 mMonthSpinner.setOnValueChangedListener(onChangeListener); 657 mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(R.id.numberpicker_input); 658 659 // year 660 mYearSpinner = (NumberPicker) mDelegator.findViewById(R.id.year); 661 mYearSpinner.setOnLongPressUpdateInterval(100); 662 mYearSpinner.setOnValueChangedListener(onChangeListener); 663 mYearSpinnerInput = (EditText) mYearSpinner.findViewById(R.id.numberpicker_input); 664 665 // show only what the user required but make sure we 666 // show something and the spinners have higher priority 667 if (!spinnersShown && !calendarViewShown) { 668 setSpinnersShown(true); 669 } else { 670 setSpinnersShown(spinnersShown); 671 setCalendarViewShown(calendarViewShown); 672 } 673 674 // set the min date giving priority of the minDate over startYear 675 mTempDate.clear(); 676 if (!TextUtils.isEmpty(minDate)) { 677 if (!parseDate(minDate, mTempDate)) { 678 mTempDate.set(startYear, 0, 1); 679 } 680 } else { 681 mTempDate.set(startYear, 0, 1); 682 } 683 setMinDate(mTempDate.getTimeInMillis()); 684 685 // set the max date giving priority of the maxDate over endYear 686 mTempDate.clear(); 687 if (!TextUtils.isEmpty(maxDate)) { 688 if (!parseDate(maxDate, mTempDate)) { 689 mTempDate.set(endYear, 11, 31); 690 } 691 } else { 692 mTempDate.set(endYear, 11, 31); 693 } 694 setMaxDate(mTempDate.getTimeInMillis()); 695 696 // initialize to current date 697 mCurrentDate.setTimeInMillis(System.currentTimeMillis()); 698 init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate 699 .get(Calendar.DAY_OF_MONTH), null); 700 701 // re-order the number spinners to match the current date format 702 reorderSpinners(); 703 704 // accessibility 705 setContentDescriptions(); 706 707 // If not explicitly specified this view is important for accessibility. 708 if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 709 mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 710 } 711 } 712 713 @Override 714 public void init(int year, int monthOfYear, int dayOfMonth, 715 OnDateChangedListener onDateChangedListener) { 716 setDate(year, monthOfYear, dayOfMonth); 717 updateSpinners(); 718 updateCalendarView(); 719 mOnDateChangedListener = onDateChangedListener; 720 } 721 722 @Override 723 public void updateDate(int year, int month, int dayOfMonth) { 724 if (!isNewDate(year, month, dayOfMonth)) { 725 return; 726 } 727 setDate(year, month, dayOfMonth); 728 updateSpinners(); 729 updateCalendarView(); 730 notifyDateChanged(); 731 } 732 733 @Override 734 public int getYear() { 735 return mCurrentDate.get(Calendar.YEAR); 736 } 737 738 @Override 739 public int getMonth() { 740 return mCurrentDate.get(Calendar.MONTH); 741 } 742 743 @Override 744 public int getDayOfMonth() { 745 return mCurrentDate.get(Calendar.DAY_OF_MONTH); 746 } 747 748 @Override 749 public void setFirstDayOfWeek(int firstDayOfWeek) { 750 mCalendarView.setFirstDayOfWeek(firstDayOfWeek); 751 } 752 753 @Override 754 public int getFirstDayOfWeek() { 755 return mCalendarView.getFirstDayOfWeek(); 756 } 757 758 @Override 759 public void setMinDate(long minDate) { 760 mTempDate.setTimeInMillis(minDate); 761 if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) 762 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { 763 return; 764 } 765 mMinDate.setTimeInMillis(minDate); 766 mCalendarView.setMinDate(minDate); 767 if (mCurrentDate.before(mMinDate)) { 768 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); 769 updateCalendarView(); 770 } 771 updateSpinners(); 772 } 773 774 @Override 775 public Calendar getMinDate() { 776 final Calendar minDate = Calendar.getInstance(); 777 minDate.setTimeInMillis(mCalendarView.getMinDate()); 778 return minDate; 779 } 780 781 @Override 782 public void setMaxDate(long maxDate) { 783 mTempDate.setTimeInMillis(maxDate); 784 if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) 785 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { 786 return; 787 } 788 mMaxDate.setTimeInMillis(maxDate); 789 mCalendarView.setMaxDate(maxDate); 790 if (mCurrentDate.after(mMaxDate)) { 791 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); 792 updateCalendarView(); 793 } 794 updateSpinners(); 795 } 796 797 @Override 798 public Calendar getMaxDate() { 799 final Calendar maxDate = Calendar.getInstance(); 800 maxDate.setTimeInMillis(mCalendarView.getMaxDate()); 801 return maxDate; 802 } 803 804 @Override 805 public void setEnabled(boolean enabled) { 806 mDaySpinner.setEnabled(enabled); 807 mMonthSpinner.setEnabled(enabled); 808 mYearSpinner.setEnabled(enabled); 809 mCalendarView.setEnabled(enabled); 810 mIsEnabled = enabled; 811 } 812 813 @Override 814 public boolean isEnabled() { 815 return mIsEnabled; 816 } 817 818 @Override 819 public CalendarView getCalendarView() { 820 return mCalendarView; 821 } 822 823 @Override 824 public void setCalendarViewShown(boolean shown) { 825 mCalendarView.setVisibility(shown ? VISIBLE : GONE); 826 } 827 828 @Override 829 public boolean getCalendarViewShown() { 830 return (mCalendarView.getVisibility() == View.VISIBLE); 831 } 832 833 @Override 834 public void setSpinnersShown(boolean shown) { 835 mSpinners.setVisibility(shown ? VISIBLE : GONE); 836 } 837 838 @Override 839 public boolean getSpinnersShown() { 840 return mSpinners.isShown(); 841 } 842 843 @Override 844 public void onConfigurationChanged(Configuration newConfig) { 845 setCurrentLocale(newConfig.locale); 846 } 847 848 @Override 849 public Parcelable onSaveInstanceState(Parcelable superState) { 850 return new SavedState(superState, getYear(), getMonth(), getDayOfMonth()); 851 } 852 853 @Override 854 public void onRestoreInstanceState(Parcelable state) { 855 SavedState ss = (SavedState) state; 856 setDate(ss.mYear, ss.mMonth, ss.mDay); 857 updateSpinners(); 858 updateCalendarView(); 859 } 860 861 @Override 862 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 863 onPopulateAccessibilityEvent(event); 864 return true; 865 } 866 867 @Override 868 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 869 final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 870 String selectedDateUtterance = DateUtils.formatDateTime(mContext, 871 mCurrentDate.getTimeInMillis(), flags); 872 event.getText().add(selectedDateUtterance); 873 } 874 875 @Override 876 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 877 event.setClassName(DatePicker.class.getName()); 878 } 879 880 @Override 881 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 882 info.setClassName(DatePicker.class.getName()); 883 } 884 885 /** 886 * Sets the current locale. 887 * 888 * @param locale The current locale. 889 */ 890 @Override 891 protected void setCurrentLocale(Locale locale) { 892 super.setCurrentLocale(locale); 893 894 mTempDate = getCalendarForLocale(mTempDate, locale); 895 mMinDate = getCalendarForLocale(mMinDate, locale); 896 mMaxDate = getCalendarForLocale(mMaxDate, locale); 897 mCurrentDate = getCalendarForLocale(mCurrentDate, locale); 898 899 mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; 900 mShortMonths = new DateFormatSymbols().getShortMonths(); 901 902 if (usingNumericMonths()) { 903 // We're in a locale where a date should either be all-numeric, or all-text. 904 // All-text would require custom NumberPicker formatters for day and year. 905 mShortMonths = new String[mNumberOfMonths]; 906 for (int i = 0; i < mNumberOfMonths; ++i) { 907 mShortMonths[i] = String.format("%d", i + 1); 908 } 909 } 910 } 911 912 /** 913 * Tests whether the current locale is one where there are no real month names, 914 * such as Chinese, Japanese, or Korean locales. 915 */ 916 private boolean usingNumericMonths() { 917 return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0)); 918 } 919 920 /** 921 * Gets a calendar for locale bootstrapped with the value of a given calendar. 922 * 923 * @param oldCalendar The old calendar. 924 * @param locale The locale. 925 */ 926 private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { 927 if (oldCalendar == null) { 928 return Calendar.getInstance(locale); 929 } else { 930 final long currentTimeMillis = oldCalendar.getTimeInMillis(); 931 Calendar newCalendar = Calendar.getInstance(locale); 932 newCalendar.setTimeInMillis(currentTimeMillis); 933 return newCalendar; 934 } 935 } 936 937 /** 938 * Reorders the spinners according to the date format that is 939 * explicitly set by the user and if no such is set fall back 940 * to the current locale's default format. 941 */ 942 private void reorderSpinners() { 943 mSpinners.removeAllViews(); 944 // We use numeric spinners for year and day, but textual months. Ask icu4c what 945 // order the user's locale uses for that combination. http://b/7207103. 946 String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyyMMMdd"); 947 char[] order = ICU.getDateFormatOrder(pattern); 948 final int spinnerCount = order.length; 949 for (int i = 0; i < spinnerCount; i++) { 950 switch (order[i]) { 951 case 'd': 952 mSpinners.addView(mDaySpinner); 953 setImeOptions(mDaySpinner, spinnerCount, i); 954 break; 955 case 'M': 956 mSpinners.addView(mMonthSpinner); 957 setImeOptions(mMonthSpinner, spinnerCount, i); 958 break; 959 case 'y': 960 mSpinners.addView(mYearSpinner); 961 setImeOptions(mYearSpinner, spinnerCount, i); 962 break; 963 default: 964 throw new IllegalArgumentException(Arrays.toString(order)); 965 } 966 } 967 } 968 969 /** 970 * Parses the given <code>date</code> and in case of success sets the result 971 * to the <code>outDate</code>. 972 * 973 * @return True if the date was parsed. 974 */ 975 private boolean parseDate(String date, Calendar outDate) { 976 try { 977 outDate.setTime(mDateFormat.parse(date)); 978 return true; 979 } catch (ParseException e) { 980 Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); 981 return false; 982 } 983 } 984 985 private boolean isNewDate(int year, int month, int dayOfMonth) { 986 return (mCurrentDate.get(Calendar.YEAR) != year 987 || mCurrentDate.get(Calendar.MONTH) != dayOfMonth 988 || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month); 989 } 990 991 private void setDate(int year, int month, int dayOfMonth) { 992 mCurrentDate.set(year, month, dayOfMonth); 993 if (mCurrentDate.before(mMinDate)) { 994 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); 995 } else if (mCurrentDate.after(mMaxDate)) { 996 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); 997 } 998 } 999 1000 private void updateSpinners() { 1001 // set the spinner ranges respecting the min and max dates 1002 if (mCurrentDate.equals(mMinDate)) { 1003 mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); 1004 mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); 1005 mDaySpinner.setWrapSelectorWheel(false); 1006 mMonthSpinner.setDisplayedValues(null); 1007 mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH)); 1008 mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH)); 1009 mMonthSpinner.setWrapSelectorWheel(false); 1010 } else if (mCurrentDate.equals(mMaxDate)) { 1011 mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH)); 1012 mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); 1013 mDaySpinner.setWrapSelectorWheel(false); 1014 mMonthSpinner.setDisplayedValues(null); 1015 mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH)); 1016 mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH)); 1017 mMonthSpinner.setWrapSelectorWheel(false); 1018 } else { 1019 mDaySpinner.setMinValue(1); 1020 mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); 1021 mDaySpinner.setWrapSelectorWheel(true); 1022 mMonthSpinner.setDisplayedValues(null); 1023 mMonthSpinner.setMinValue(0); 1024 mMonthSpinner.setMaxValue(11); 1025 mMonthSpinner.setWrapSelectorWheel(true); 1026 } 1027 1028 // make sure the month names are a zero based array 1029 // with the months in the month spinner 1030 String[] displayedValues = Arrays.copyOfRange(mShortMonths, 1031 mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); 1032 mMonthSpinner.setDisplayedValues(displayedValues); 1033 1034 // year spinner range does not change based on the current date 1035 mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); 1036 mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); 1037 mYearSpinner.setWrapSelectorWheel(false); 1038 1039 // set the spinner values 1040 mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); 1041 mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); 1042 mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); 1043 1044 if (usingNumericMonths()) { 1045 mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER); 1046 } 1047 } 1048 1049 /** 1050 * Updates the calendar view with the current date. 1051 */ 1052 private void updateCalendarView() { 1053 mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false); 1054 } 1055 1056 1057 /** 1058 * Notifies the listener, if such, for a change in the selected date. 1059 */ 1060 private void notifyDateChanged() { 1061 mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 1062 if (mOnDateChangedListener != null) { 1063 mOnDateChangedListener.onDateChanged(mDelegator, getYear(), getMonth(), 1064 getDayOfMonth()); 1065 } 1066 } 1067 1068 /** 1069 * Sets the IME options for a spinner based on its ordering. 1070 * 1071 * @param spinner The spinner. 1072 * @param spinnerCount The total spinner count. 1073 * @param spinnerIndex The index of the given spinner. 1074 */ 1075 private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) { 1076 final int imeOptions; 1077 if (spinnerIndex < spinnerCount - 1) { 1078 imeOptions = EditorInfo.IME_ACTION_NEXT; 1079 } else { 1080 imeOptions = EditorInfo.IME_ACTION_DONE; 1081 } 1082 TextView input = (TextView) spinner.findViewById(R.id.numberpicker_input); 1083 input.setImeOptions(imeOptions); 1084 } 1085 1086 private void setContentDescriptions() { 1087 // Day 1088 trySetContentDescription(mDaySpinner, R.id.increment, 1089 R.string.date_picker_increment_day_button); 1090 trySetContentDescription(mDaySpinner, R.id.decrement, 1091 R.string.date_picker_decrement_day_button); 1092 // Month 1093 trySetContentDescription(mMonthSpinner, R.id.increment, 1094 R.string.date_picker_increment_month_button); 1095 trySetContentDescription(mMonthSpinner, R.id.decrement, 1096 R.string.date_picker_decrement_month_button); 1097 // Year 1098 trySetContentDescription(mYearSpinner, R.id.increment, 1099 R.string.date_picker_increment_year_button); 1100 trySetContentDescription(mYearSpinner, R.id.decrement, 1101 R.string.date_picker_decrement_year_button); 1102 } 1103 1104 private void trySetContentDescription(View root, int viewId, int contDescResId) { 1105 View target = root.findViewById(viewId); 1106 if (target != null) { 1107 target.setContentDescription(mContext.getString(contDescResId)); 1108 } 1109 } 1110 1111 private void updateInputState() { 1112 // Make sure that if the user changes the value and the IME is active 1113 // for one of the inputs if this widget, the IME is closed. If the user 1114 // changed the value via the IME and there is a next input the IME will 1115 // be shown, otherwise the user chose another means of changing the 1116 // value and having the IME up makes no sense. 1117 InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); 1118 if (inputMethodManager != null) { 1119 if (inputMethodManager.isActive(mYearSpinnerInput)) { 1120 mYearSpinnerInput.clearFocus(); 1121 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); 1122 } else if (inputMethodManager.isActive(mMonthSpinnerInput)) { 1123 mMonthSpinnerInput.clearFocus(); 1124 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); 1125 } else if (inputMethodManager.isActive(mDaySpinnerInput)) { 1126 mDaySpinnerInput.clearFocus(); 1127 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); 1128 } 1129 } 1130 } 1131 } 1132 1133 /** 1134 * Class for managing state storing/restoring. 1135 */ 1136 private static class SavedState extends BaseSavedState { 1137 1138 private final int mYear; 1139 1140 private final int mMonth; 1141 1142 private final int mDay; 1143 1144 /** 1145 * Constructor called from {@link DatePicker#onSaveInstanceState()} 1146 */ 1147 private SavedState(Parcelable superState, int year, int month, int day) { 1148 super(superState); 1149 mYear = year; 1150 mMonth = month; 1151 mDay = day; 1152 } 1153 1154 /** 1155 * Constructor called from {@link #CREATOR} 1156 */ 1157 private SavedState(Parcel in) { 1158 super(in); 1159 mYear = in.readInt(); 1160 mMonth = in.readInt(); 1161 mDay = in.readInt(); 1162 } 1163 1164 @Override 1165 public void writeToParcel(Parcel dest, int flags) { 1166 super.writeToParcel(dest, flags); 1167 dest.writeInt(mYear); 1168 dest.writeInt(mMonth); 1169 dest.writeInt(mDay); 1170 } 1171 1172 @SuppressWarnings("all") 1173 // suppress unused and hiding 1174 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { 1175 1176 public SavedState createFromParcel(Parcel in) { 1177 return new SavedState(in); 1178 } 1179 1180 public SavedState[] newArray(int size) { 1181 return new SavedState[size]; 1182 } 1183 }; 1184 } 1185 } 1186