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