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 com.android.internal.R; 20 21 import android.annotation.IntDef; 22 import android.annotation.Nullable; 23 import android.annotation.TestApi; 24 import android.annotation.Widget; 25 import android.content.Context; 26 import android.content.res.Configuration; 27 import android.content.res.TypedArray; 28 import android.icu.util.Calendar; 29 import android.icu.util.TimeZone; 30 import android.os.Parcel; 31 import android.os.Parcelable; 32 import android.util.AttributeSet; 33 import android.util.SparseArray; 34 import android.view.View; 35 import android.view.accessibility.AccessibilityEvent; 36 37 import java.lang.annotation.Retention; 38 import java.lang.annotation.RetentionPolicy; 39 import java.util.Locale; 40 41 /** 42 * Provides a widget for selecting a date. 43 * <p> 44 * When the {@link android.R.styleable#DatePicker_datePickerMode} attribute is 45 * set to {@code spinner}, the date can be selected using year, month, and day 46 * spinners or a {@link CalendarView}. The set of spinners and the calendar 47 * view are automatically synchronized. The client can customize whether only 48 * the spinners, or only the calendar view, or both to be displayed. 49 * </p> 50 * <p> 51 * When the {@link android.R.styleable#DatePicker_datePickerMode} attribute is 52 * set to {@code calendar}, the month and day can be selected using a 53 * calendar-style view while the year can be selected separately using a list. 54 * </p> 55 * <p> 56 * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a> 57 * guide. 58 * </p> 59 * <p> 60 * For a dialog using this view, see {@link android.app.DatePickerDialog}. 61 * </p> 62 * 63 * @attr ref android.R.styleable#DatePicker_startYear 64 * @attr ref android.R.styleable#DatePicker_endYear 65 * @attr ref android.R.styleable#DatePicker_maxDate 66 * @attr ref android.R.styleable#DatePicker_minDate 67 * @attr ref android.R.styleable#DatePicker_spinnersShown 68 * @attr ref android.R.styleable#DatePicker_calendarViewShown 69 * @attr ref android.R.styleable#DatePicker_dayOfWeekBackground 70 * @attr ref android.R.styleable#DatePicker_dayOfWeekTextAppearance 71 * @attr ref android.R.styleable#DatePicker_headerBackground 72 * @attr ref android.R.styleable#DatePicker_headerMonthTextAppearance 73 * @attr ref android.R.styleable#DatePicker_headerDayOfMonthTextAppearance 74 * @attr ref android.R.styleable#DatePicker_headerYearTextAppearance 75 * @attr ref android.R.styleable#DatePicker_yearListItemTextAppearance 76 * @attr ref android.R.styleable#DatePicker_yearListSelectorColor 77 * @attr ref android.R.styleable#DatePicker_calendarTextColor 78 * @attr ref android.R.styleable#DatePicker_datePickerMode 79 */ 80 @Widget 81 public class DatePicker extends FrameLayout { 82 /** 83 * Presentation mode for the Holo-style date picker that uses a set of 84 * {@link android.widget.NumberPicker}s. 85 * 86 * @see #getMode() 87 * @hide Visible for testing only. 88 */ 89 @TestApi 90 public static final int MODE_SPINNER = 1; 91 92 /** 93 * Presentation mode for the Material-style date picker that uses a 94 * calendar. 95 * 96 * @see #getMode() 97 * @hide Visible for testing only. 98 */ 99 @TestApi 100 public static final int MODE_CALENDAR = 2; 101 102 /** @hide */ 103 @IntDef({MODE_SPINNER, MODE_CALENDAR}) 104 @Retention(RetentionPolicy.SOURCE) 105 public @interface DatePickerMode {} 106 107 private final DatePickerDelegate mDelegate; 108 109 @DatePickerMode 110 private final int mMode; 111 112 /** 113 * The callback used to indicate the user changed the date. 114 */ 115 public interface OnDateChangedListener { 116 117 /** 118 * Called upon a date change. 119 * 120 * @param view The view associated with this listener. 121 * @param year The year that was set. 122 * @param monthOfYear The month that was set (0-11) for compatibility 123 * with {@link java.util.Calendar}. 124 * @param dayOfMonth The day of the month that was set. 125 */ 126 void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth); 127 } 128 129 public DatePicker(Context context) { 130 this(context, null); 131 } 132 133 public DatePicker(Context context, AttributeSet attrs) { 134 this(context, attrs, R.attr.datePickerStyle); 135 } 136 137 public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) { 138 this(context, attrs, defStyleAttr, 0); 139 } 140 141 public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 142 super(context, attrs, defStyleAttr, defStyleRes); 143 144 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, 145 defStyleAttr, defStyleRes); 146 final boolean isDialogMode = a.getBoolean(R.styleable.DatePicker_dialogMode, false); 147 final int requestedMode = a.getInt(R.styleable.DatePicker_datePickerMode, MODE_SPINNER); 148 final int firstDayOfWeek = a.getInt(R.styleable.DatePicker_firstDayOfWeek, 0); 149 a.recycle(); 150 151 if (requestedMode == MODE_CALENDAR && isDialogMode) { 152 // You want MODE_CALENDAR? YOU CAN'T HANDLE MODE_CALENDAR! Well, 153 // maybe you can depending on your screen size. Let's check... 154 mMode = context.getResources().getInteger(R.integer.date_picker_mode); 155 } else { 156 mMode = requestedMode; 157 } 158 159 switch (mMode) { 160 case MODE_CALENDAR: 161 mDelegate = createCalendarUIDelegate(context, attrs, defStyleAttr, defStyleRes); 162 break; 163 case MODE_SPINNER: 164 default: 165 mDelegate = createSpinnerUIDelegate(context, attrs, defStyleAttr, defStyleRes); 166 break; 167 } 168 169 if (firstDayOfWeek != 0) { 170 setFirstDayOfWeek(firstDayOfWeek); 171 } 172 } 173 174 private DatePickerDelegate createSpinnerUIDelegate(Context context, AttributeSet attrs, 175 int defStyleAttr, int defStyleRes) { 176 return new DatePickerSpinnerDelegate(this, context, attrs, defStyleAttr, defStyleRes); 177 } 178 179 private DatePickerDelegate createCalendarUIDelegate(Context context, AttributeSet attrs, 180 int defStyleAttr, int defStyleRes) { 181 return new DatePickerCalendarDelegate(this, context, attrs, defStyleAttr, 182 defStyleRes); 183 } 184 185 /** 186 * @return the picker's presentation mode, one of {@link #MODE_CALENDAR} or 187 * {@link #MODE_SPINNER} 188 * @attr ref android.R.styleable#DatePicker_datePickerMode 189 * @hide Visible for testing only. 190 */ 191 @DatePickerMode 192 @TestApi 193 public int getMode() { 194 return mMode; 195 } 196 197 /** 198 * Initialize the state. If the provided values designate an inconsistent 199 * date the values are normalized before updating the spinners. 200 * 201 * @param year The initial year. 202 * @param monthOfYear The initial month <strong>starting from zero</strong>. 203 * @param dayOfMonth The initial day of the month. 204 * @param onDateChangedListener How user is notified date is changed by 205 * user, can be null. 206 */ 207 public void init(int year, int monthOfYear, int dayOfMonth, 208 OnDateChangedListener onDateChangedListener) { 209 mDelegate.init(year, monthOfYear, dayOfMonth, onDateChangedListener); 210 } 211 212 /** 213 * Update the current date. 214 * 215 * @param year The year. 216 * @param month The month which is <strong>starting from zero</strong>. 217 * @param dayOfMonth The day of the month. 218 */ 219 public void updateDate(int year, int month, int dayOfMonth) { 220 mDelegate.updateDate(year, month, dayOfMonth); 221 } 222 223 /** 224 * @return The selected year. 225 */ 226 public int getYear() { 227 return mDelegate.getYear(); 228 } 229 230 /** 231 * @return The selected month. 232 */ 233 public int getMonth() { 234 return mDelegate.getMonth(); 235 } 236 237 /** 238 * @return The selected day of month. 239 */ 240 public int getDayOfMonth() { 241 return mDelegate.getDayOfMonth(); 242 } 243 244 /** 245 * Gets the minimal date supported by this {@link DatePicker} in 246 * milliseconds since January 1, 1970 00:00:00 in 247 * {@link TimeZone#getDefault()} time zone. 248 * <p> 249 * Note: The default minimal date is 01/01/1900. 250 * <p> 251 * 252 * @return The minimal supported date. 253 */ 254 public long getMinDate() { 255 return mDelegate.getMinDate().getTimeInMillis(); 256 } 257 258 /** 259 * Sets the minimal date supported by this {@link NumberPicker} in 260 * milliseconds since January 1, 1970 00:00:00 in 261 * {@link TimeZone#getDefault()} time zone. 262 * 263 * @param minDate The minimal supported date. 264 */ 265 public void setMinDate(long minDate) { 266 mDelegate.setMinDate(minDate); 267 } 268 269 /** 270 * Gets the maximal date supported by this {@link DatePicker} in 271 * milliseconds since January 1, 1970 00:00:00 in 272 * {@link TimeZone#getDefault()} time zone. 273 * <p> 274 * Note: The default maximal date is 12/31/2100. 275 * <p> 276 * 277 * @return The maximal supported date. 278 */ 279 public long getMaxDate() { 280 return mDelegate.getMaxDate().getTimeInMillis(); 281 } 282 283 /** 284 * Sets the maximal date supported by this {@link DatePicker} in 285 * milliseconds since January 1, 1970 00:00:00 in 286 * {@link TimeZone#getDefault()} time zone. 287 * 288 * @param maxDate The maximal supported date. 289 */ 290 public void setMaxDate(long maxDate) { 291 mDelegate.setMaxDate(maxDate); 292 } 293 294 /** 295 * Sets the callback that indicates the current date is valid. 296 * 297 * @param callback the callback, may be null 298 * @hide 299 */ 300 public void setValidationCallback(@Nullable ValidationCallback callback) { 301 mDelegate.setValidationCallback(callback); 302 } 303 304 @Override 305 public void setEnabled(boolean enabled) { 306 if (mDelegate.isEnabled() == enabled) { 307 return; 308 } 309 super.setEnabled(enabled); 310 mDelegate.setEnabled(enabled); 311 } 312 313 @Override 314 public boolean isEnabled() { 315 return mDelegate.isEnabled(); 316 } 317 318 /** @hide */ 319 @Override 320 public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { 321 return mDelegate.dispatchPopulateAccessibilityEvent(event); 322 } 323 324 /** @hide */ 325 @Override 326 public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) { 327 super.onPopulateAccessibilityEventInternal(event); 328 mDelegate.onPopulateAccessibilityEvent(event); 329 } 330 331 @Override 332 public CharSequence getAccessibilityClassName() { 333 return DatePicker.class.getName(); 334 } 335 336 @Override 337 protected void onConfigurationChanged(Configuration newConfig) { 338 super.onConfigurationChanged(newConfig); 339 mDelegate.onConfigurationChanged(newConfig); 340 } 341 342 /** 343 * Sets the first day of week. 344 * 345 * @param firstDayOfWeek The first day of the week conforming to the 346 * {@link CalendarView} APIs. 347 * @see Calendar#SUNDAY 348 * @see Calendar#MONDAY 349 * @see Calendar#TUESDAY 350 * @see Calendar#WEDNESDAY 351 * @see Calendar#THURSDAY 352 * @see Calendar#FRIDAY 353 * @see Calendar#SATURDAY 354 * 355 * @attr ref android.R.styleable#DatePicker_firstDayOfWeek 356 */ 357 public void setFirstDayOfWeek(int firstDayOfWeek) { 358 if (firstDayOfWeek < Calendar.SUNDAY || firstDayOfWeek > Calendar.SATURDAY) { 359 throw new IllegalArgumentException("firstDayOfWeek must be between 1 and 7"); 360 } 361 mDelegate.setFirstDayOfWeek(firstDayOfWeek); 362 } 363 364 /** 365 * Gets the first day of week. 366 * 367 * @return The first day of the week conforming to the {@link CalendarView} 368 * APIs. 369 * @see Calendar#SUNDAY 370 * @see Calendar#MONDAY 371 * @see Calendar#TUESDAY 372 * @see Calendar#WEDNESDAY 373 * @see Calendar#THURSDAY 374 * @see Calendar#FRIDAY 375 * @see Calendar#SATURDAY 376 * 377 * @attr ref android.R.styleable#DatePicker_firstDayOfWeek 378 */ 379 public int getFirstDayOfWeek() { 380 return mDelegate.getFirstDayOfWeek(); 381 } 382 383 /** 384 * Returns whether the {@link CalendarView} is shown. 385 * <p> 386 * <strong>Note:</strong> This method returns {@code false} when the 387 * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set 388 * to {@code calendar}. 389 * 390 * @return {@code true} if the calendar view is shown 391 * @see #getCalendarView() 392 * @deprecated Not supported by Material-style {@code calendar} mode 393 */ 394 @Deprecated 395 public boolean getCalendarViewShown() { 396 return mDelegate.getCalendarViewShown(); 397 } 398 399 /** 400 * Returns the {@link CalendarView} used by this picker. 401 * <p> 402 * <strong>Note:</strong> This method throws an 403 * {@link UnsupportedOperationException} when the 404 * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set 405 * to {@code calendar}. 406 * 407 * @return the calendar view 408 * @see #getCalendarViewShown() 409 * @deprecated Not supported by Material-style {@code calendar} mode 410 * @throws UnsupportedOperationException if called when the picker is 411 * displayed in {@code calendar} mode 412 */ 413 @Deprecated 414 public CalendarView getCalendarView() { 415 return mDelegate.getCalendarView(); 416 } 417 418 /** 419 * Sets whether the {@link CalendarView} is shown. 420 * <p> 421 * <strong>Note:</strong> Calling this method has no effect when the 422 * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set 423 * to {@code calendar}. 424 * 425 * @param shown {@code true} to show the calendar view, {@code false} to 426 * hide it 427 * @deprecated Not supported by Material-style {@code calendar} mode 428 */ 429 @Deprecated 430 public void setCalendarViewShown(boolean shown) { 431 mDelegate.setCalendarViewShown(shown); 432 } 433 434 /** 435 * Returns whether the spinners are shown. 436 * <p> 437 * <strong>Note:</strong> his method returns {@code false} when the 438 * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set 439 * to {@code calendar}. 440 * 441 * @return {@code true} if the spinners are shown 442 * @deprecated Not supported by Material-style {@code calendar} mode 443 */ 444 @Deprecated 445 public boolean getSpinnersShown() { 446 return mDelegate.getSpinnersShown(); 447 } 448 449 /** 450 * Sets whether the spinners are shown. 451 * <p> 452 * Calling this method has no effect when the 453 * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set 454 * to {@code calendar}. 455 * 456 * @param shown {@code true} to show the spinners, {@code false} to hide 457 * them 458 * @deprecated Not supported by Material-style {@code calendar} mode 459 */ 460 @Deprecated 461 public void setSpinnersShown(boolean shown) { 462 mDelegate.setSpinnersShown(shown); 463 } 464 465 @Override 466 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 467 dispatchThawSelfOnly(container); 468 } 469 470 @Override 471 protected Parcelable onSaveInstanceState() { 472 Parcelable superState = super.onSaveInstanceState(); 473 return mDelegate.onSaveInstanceState(superState); 474 } 475 476 @Override 477 protected void onRestoreInstanceState(Parcelable state) { 478 BaseSavedState ss = (BaseSavedState) state; 479 super.onRestoreInstanceState(ss.getSuperState()); 480 mDelegate.onRestoreInstanceState(ss); 481 } 482 483 /** 484 * A delegate interface that defined the public API of the DatePicker. Allows different 485 * DatePicker implementations. This would need to be implemented by the DatePicker delegates 486 * for the real behavior. 487 * 488 * @hide 489 */ 490 interface DatePickerDelegate { 491 void init(int year, int monthOfYear, int dayOfMonth, 492 OnDateChangedListener onDateChangedListener); 493 494 void updateDate(int year, int month, int dayOfMonth); 495 496 int getYear(); 497 int getMonth(); 498 int getDayOfMonth(); 499 500 void setFirstDayOfWeek(int firstDayOfWeek); 501 int getFirstDayOfWeek(); 502 503 void setMinDate(long minDate); 504 Calendar getMinDate(); 505 506 void setMaxDate(long maxDate); 507 Calendar getMaxDate(); 508 509 void setEnabled(boolean enabled); 510 boolean isEnabled(); 511 512 CalendarView getCalendarView(); 513 514 void setCalendarViewShown(boolean shown); 515 boolean getCalendarViewShown(); 516 517 void setSpinnersShown(boolean shown); 518 boolean getSpinnersShown(); 519 520 void setValidationCallback(ValidationCallback callback); 521 522 void onConfigurationChanged(Configuration newConfig); 523 524 Parcelable onSaveInstanceState(Parcelable superState); 525 void onRestoreInstanceState(Parcelable state); 526 527 boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); 528 void onPopulateAccessibilityEvent(AccessibilityEvent event); 529 } 530 531 /** 532 * An abstract class which can be used as a start for DatePicker implementations 533 */ 534 abstract static class AbstractDatePickerDelegate implements DatePickerDelegate { 535 // The delegator 536 protected DatePicker mDelegator; 537 538 // The context 539 protected Context mContext; 540 541 // The current locale 542 protected Locale mCurrentLocale; 543 544 // Callbacks 545 protected OnDateChangedListener mOnDateChangedListener; 546 protected ValidationCallback mValidationCallback; 547 548 public AbstractDatePickerDelegate(DatePicker delegator, Context context) { 549 mDelegator = delegator; 550 mContext = context; 551 552 setCurrentLocale(Locale.getDefault()); 553 } 554 555 protected void setCurrentLocale(Locale locale) { 556 if (!locale.equals(mCurrentLocale)) { 557 mCurrentLocale = locale; 558 onLocaleChanged(locale); 559 } 560 } 561 562 @Override 563 public void setValidationCallback(ValidationCallback callback) { 564 mValidationCallback = callback; 565 } 566 567 protected void onValidationChanged(boolean valid) { 568 if (mValidationCallback != null) { 569 mValidationCallback.onValidationChanged(valid); 570 } 571 } 572 573 protected void onLocaleChanged(Locale locale) { 574 // Stub. 575 } 576 577 /** 578 * Class for managing state storing/restoring. 579 */ 580 static class SavedState extends View.BaseSavedState { 581 private final int mSelectedYear; 582 private final int mSelectedMonth; 583 private final int mSelectedDay; 584 private final long mMinDate; 585 private final long mMaxDate; 586 private final int mCurrentView; 587 private final int mListPosition; 588 private final int mListPositionOffset; 589 590 public SavedState(Parcelable superState, int year, int month, int day, long minDate, 591 long maxDate) { 592 this(superState, year, month, day, minDate, maxDate, 0, 0, 0); 593 } 594 595 /** 596 * Constructor called from {@link DatePicker#onSaveInstanceState()} 597 */ 598 public SavedState(Parcelable superState, int year, int month, int day, long minDate, 599 long maxDate, int currentView, int listPosition, int listPositionOffset) { 600 super(superState); 601 mSelectedYear = year; 602 mSelectedMonth = month; 603 mSelectedDay = day; 604 mMinDate = minDate; 605 mMaxDate = maxDate; 606 mCurrentView = currentView; 607 mListPosition = listPosition; 608 mListPositionOffset = listPositionOffset; 609 } 610 611 /** 612 * Constructor called from {@link #CREATOR} 613 */ 614 private SavedState(Parcel in) { 615 super(in); 616 mSelectedYear = in.readInt(); 617 mSelectedMonth = in.readInt(); 618 mSelectedDay = in.readInt(); 619 mMinDate = in.readLong(); 620 mMaxDate = in.readLong(); 621 mCurrentView = in.readInt(); 622 mListPosition = in.readInt(); 623 mListPositionOffset = in.readInt(); 624 } 625 626 @Override 627 public void writeToParcel(Parcel dest, int flags) { 628 super.writeToParcel(dest, flags); 629 dest.writeInt(mSelectedYear); 630 dest.writeInt(mSelectedMonth); 631 dest.writeInt(mSelectedDay); 632 dest.writeLong(mMinDate); 633 dest.writeLong(mMaxDate); 634 dest.writeInt(mCurrentView); 635 dest.writeInt(mListPosition); 636 dest.writeInt(mListPositionOffset); 637 } 638 639 public int getSelectedDay() { 640 return mSelectedDay; 641 } 642 643 public int getSelectedMonth() { 644 return mSelectedMonth; 645 } 646 647 public int getSelectedYear() { 648 return mSelectedYear; 649 } 650 651 public long getMinDate() { 652 return mMinDate; 653 } 654 655 public long getMaxDate() { 656 return mMaxDate; 657 } 658 659 public int getCurrentView() { 660 return mCurrentView; 661 } 662 663 public int getListPosition() { 664 return mListPosition; 665 } 666 667 public int getListPositionOffset() { 668 return mListPositionOffset; 669 } 670 671 @SuppressWarnings("all") 672 // suppress unused and hiding 673 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { 674 675 public SavedState createFromParcel(Parcel in) { 676 return new SavedState(in); 677 } 678 679 public SavedState[] newArray(int size) { 680 return new SavedState[size]; 681 } 682 }; 683 } 684 } 685 686 /** 687 * A callback interface for updating input validity when the date picker 688 * when included into a dialog. 689 * 690 * @hide 691 */ 692 public interface ValidationCallback { 693 void onValidationChanged(boolean valid); 694 } 695 } 696