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