1 /* 2 * Copyright (C) 2014 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.content.Context; 20 import android.content.res.ColorStateList; 21 import android.content.res.Configuration; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.text.format.DateFormat; 27 import android.text.format.DateUtils; 28 import android.util.AttributeSet; 29 import android.view.HapticFeedbackConstants; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.accessibility.AccessibilityEvent; 33 import android.view.accessibility.AccessibilityNodeInfo; 34 import android.view.animation.AlphaAnimation; 35 import android.view.animation.Animation; 36 37 import com.android.internal.R; 38 import com.android.internal.widget.AccessibleDateAnimator; 39 40 import java.text.SimpleDateFormat; 41 import java.util.Calendar; 42 import java.util.HashSet; 43 import java.util.Locale; 44 45 /** 46 * A delegate for picking up a date (day / month / year). 47 */ 48 class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate implements 49 View.OnClickListener, DatePickerController { 50 private static final int USE_LOCALE = 0; 51 52 private static final int UNINITIALIZED = -1; 53 private static final int MONTH_AND_DAY_VIEW = 0; 54 private static final int YEAR_VIEW = 1; 55 56 private static final int DEFAULT_START_YEAR = 1900; 57 private static final int DEFAULT_END_YEAR = 2100; 58 59 private static final int ANIMATION_DURATION = 300; 60 61 private static final int MONTH_INDEX = 0; 62 private static final int DAY_INDEX = 1; 63 private static final int YEAR_INDEX = 2; 64 65 private SimpleDateFormat mYearFormat = new SimpleDateFormat("y", Locale.getDefault()); 66 private SimpleDateFormat mDayFormat = new SimpleDateFormat("d", Locale.getDefault()); 67 68 private TextView mDayOfWeekView; 69 70 /** Layout that contains the current month, day, and year. */ 71 private LinearLayout mMonthDayYearLayout; 72 73 /** Clickable layout that contains the current day and year. */ 74 private LinearLayout mMonthAndDayLayout; 75 76 private TextView mHeaderMonthTextView; 77 private TextView mHeaderDayOfMonthTextView; 78 private TextView mHeaderYearTextView; 79 private DayPickerView mDayPickerView; 80 private YearPickerView mYearPickerView; 81 82 private boolean mIsEnabled = true; 83 84 // Accessibility strings. 85 private String mDayPickerDescription; 86 private String mSelectDay; 87 private String mYearPickerDescription; 88 private String mSelectYear; 89 90 private AccessibleDateAnimator mAnimator; 91 92 private DatePicker.OnDateChangedListener mDateChangedListener; 93 94 private int mCurrentView = UNINITIALIZED; 95 96 private Calendar mCurrentDate; 97 private Calendar mTempDate; 98 private Calendar mMinDate; 99 private Calendar mMaxDate; 100 101 private int mFirstDayOfWeek = USE_LOCALE; 102 103 private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>(); 104 105 public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs, 106 int defStyleAttr, int defStyleRes) { 107 super(delegator, context); 108 109 final Locale locale = Locale.getDefault(); 110 mMinDate = getCalendarForLocale(mMinDate, locale); 111 mMaxDate = getCalendarForLocale(mMaxDate, locale); 112 mTempDate = getCalendarForLocale(mMaxDate, locale); 113 mCurrentDate = getCalendarForLocale(mCurrentDate, locale); 114 115 mMinDate.set(DEFAULT_START_YEAR, 1, 1); 116 mMaxDate.set(DEFAULT_END_YEAR, 12, 31); 117 118 final Resources res = mDelegator.getResources(); 119 final TypedArray a = mContext.obtainStyledAttributes(attrs, 120 R.styleable.DatePicker, defStyleAttr, defStyleRes); 121 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 122 Context.LAYOUT_INFLATER_SERVICE); 123 final int layoutResourceId = a.getResourceId( 124 R.styleable.DatePicker_internalLayout, R.layout.date_picker_holo); 125 final View mainView = inflater.inflate(layoutResourceId, null); 126 mDelegator.addView(mainView); 127 128 mDayOfWeekView = (TextView) mainView.findViewById(R.id.date_picker_header); 129 130 // Layout that contains the current date and day name header. 131 final LinearLayout dateLayout = (LinearLayout) mainView.findViewById( 132 R.id.day_picker_selector_layout); 133 mMonthDayYearLayout = (LinearLayout) mainView.findViewById( 134 R.id.date_picker_month_day_year_layout); 135 mMonthAndDayLayout = (LinearLayout) mainView.findViewById( 136 R.id.date_picker_month_and_day_layout); 137 mMonthAndDayLayout.setOnClickListener(this); 138 mHeaderMonthTextView = (TextView) mainView.findViewById(R.id.date_picker_month); 139 mHeaderDayOfMonthTextView = (TextView) mainView.findViewById(R.id.date_picker_day); 140 mHeaderYearTextView = (TextView) mainView.findViewById(R.id.date_picker_year); 141 mHeaderYearTextView.setOnClickListener(this); 142 143 // Obtain default highlight color from the theme. 144 final int defaultHighlightColor = mHeaderYearTextView.getHighlightColor(); 145 146 // Use Theme attributes if possible 147 final int dayOfWeekTextAppearanceResId = a.getResourceId( 148 R.styleable.DatePicker_dayOfWeekTextAppearance, -1); 149 if (dayOfWeekTextAppearanceResId != -1) { 150 mDayOfWeekView.setTextAppearance(context, dayOfWeekTextAppearanceResId); 151 } 152 153 mDayOfWeekView.setBackground(a.getDrawable(R.styleable.DatePicker_dayOfWeekBackground)); 154 155 dateLayout.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground)); 156 157 final int headerSelectedTextColor = a.getColor( 158 R.styleable.DatePicker_headerSelectedTextColor, defaultHighlightColor); 159 final int monthTextAppearanceResId = a.getResourceId( 160 R.styleable.DatePicker_headerMonthTextAppearance, -1); 161 if (monthTextAppearanceResId != -1) { 162 mHeaderMonthTextView.setTextAppearance(context, monthTextAppearanceResId); 163 } 164 mHeaderMonthTextView.setTextColor(ColorStateList.addFirstIfMissing( 165 mHeaderMonthTextView.getTextColors(), R.attr.state_selected, 166 headerSelectedTextColor)); 167 168 final int dayOfMonthTextAppearanceResId = a.getResourceId( 169 R.styleable.DatePicker_headerDayOfMonthTextAppearance, -1); 170 if (dayOfMonthTextAppearanceResId != -1) { 171 mHeaderDayOfMonthTextView.setTextAppearance(context, dayOfMonthTextAppearanceResId); 172 } 173 mHeaderDayOfMonthTextView.setTextColor(ColorStateList.addFirstIfMissing( 174 mHeaderDayOfMonthTextView.getTextColors(), R.attr.state_selected, 175 headerSelectedTextColor)); 176 177 final int yearTextAppearanceResId = a.getResourceId( 178 R.styleable.DatePicker_headerYearTextAppearance, -1); 179 if (yearTextAppearanceResId != -1) { 180 mHeaderYearTextView.setTextAppearance(context, yearTextAppearanceResId); 181 } 182 mHeaderYearTextView.setTextColor(ColorStateList.addFirstIfMissing( 183 mHeaderYearTextView.getTextColors(), R.attr.state_selected, 184 headerSelectedTextColor)); 185 186 mDayPickerView = new DayPickerView(mContext); 187 mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek); 188 mDayPickerView.setMinDate(mMinDate.getTimeInMillis()); 189 mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis()); 190 mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); 191 mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener); 192 193 mYearPickerView = new YearPickerView(mContext); 194 mYearPickerView.init(this); 195 mYearPickerView.setRange(mMinDate, mMaxDate); 196 197 final int yearSelectedCircleColor = a.getColor(R.styleable.DatePicker_yearListSelectorColor, 198 defaultHighlightColor); 199 mYearPickerView.setYearSelectedCircleColor(yearSelectedCircleColor); 200 201 final ColorStateList calendarTextColor = a.getColorStateList( 202 R.styleable.DatePicker_calendarTextColor); 203 final int calendarSelectedTextColor = a.getColor( 204 R.styleable.DatePicker_calendarSelectedTextColor, defaultHighlightColor); 205 mDayPickerView.setCalendarTextColor(ColorStateList.addFirstIfMissing( 206 calendarTextColor, R.attr.state_selected, calendarSelectedTextColor)); 207 208 mDayPickerDescription = res.getString(R.string.day_picker_description); 209 mSelectDay = res.getString(R.string.select_day); 210 mYearPickerDescription = res.getString(R.string.year_picker_description); 211 mSelectYear = res.getString(R.string.select_year); 212 213 mAnimator = (AccessibleDateAnimator) mainView.findViewById(R.id.animator); 214 mAnimator.addView(mDayPickerView); 215 mAnimator.addView(mYearPickerView); 216 mAnimator.setDateMillis(mCurrentDate.getTimeInMillis()); 217 218 final Animation animation = new AlphaAnimation(0.0f, 1.0f); 219 animation.setDuration(ANIMATION_DURATION); 220 mAnimator.setInAnimation(animation); 221 222 final Animation animation2 = new AlphaAnimation(1.0f, 0.0f); 223 animation2.setDuration(ANIMATION_DURATION); 224 mAnimator.setOutAnimation(animation2); 225 226 updateDisplay(false); 227 setCurrentView(MONTH_AND_DAY_VIEW); 228 } 229 230 /** 231 * Gets a calendar for locale bootstrapped with the value of a given calendar. 232 * 233 * @param oldCalendar The old calendar. 234 * @param locale The locale. 235 */ 236 private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { 237 if (oldCalendar == null) { 238 return Calendar.getInstance(locale); 239 } else { 240 final long currentTimeMillis = oldCalendar.getTimeInMillis(); 241 Calendar newCalendar = Calendar.getInstance(locale); 242 newCalendar.setTimeInMillis(currentTimeMillis); 243 return newCalendar; 244 } 245 } 246 247 /** 248 * Compute the array representing the order of Month / Day / Year views in their layout. 249 * Will be used for I18N purpose as the order of them depends on the Locale. 250 */ 251 private int[] getMonthDayYearIndexes(String pattern) { 252 int[] result = new int[3]; 253 254 final String filteredPattern = pattern.replaceAll("'.*?'", ""); 255 256 final int dayIndex = filteredPattern.indexOf('d'); 257 final int monthMIndex = filteredPattern.indexOf("M"); 258 final int monthIndex = (monthMIndex != -1) ? monthMIndex : filteredPattern.indexOf("L"); 259 final int yearIndex = filteredPattern.indexOf("y"); 260 261 if (yearIndex < monthIndex) { 262 result[YEAR_INDEX] = 0; 263 264 if (monthIndex < dayIndex) { 265 result[MONTH_INDEX] = 1; 266 result[DAY_INDEX] = 2; 267 } else { 268 result[MONTH_INDEX] = 2; 269 result[DAY_INDEX] = 1; 270 } 271 } else { 272 result[YEAR_INDEX] = 2; 273 274 if (monthIndex < dayIndex) { 275 result[MONTH_INDEX] = 0; 276 result[DAY_INDEX] = 1; 277 } else { 278 result[MONTH_INDEX] = 1; 279 result[DAY_INDEX] = 0; 280 } 281 } 282 return result; 283 } 284 285 private void updateDisplay(boolean announce) { 286 if (mDayOfWeekView != null) { 287 mDayOfWeekView.setText(mCurrentDate.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, 288 Locale.getDefault())); 289 } 290 291 // Compute indices of Month, Day and Year views 292 final String bestDateTimePattern = 293 DateFormat.getBestDateTimePattern(mCurrentLocale, "yMMMd"); 294 final int[] viewIndices = getMonthDayYearIndexes(bestDateTimePattern); 295 296 // Position the Year and MonthAndDay views within the header. 297 mMonthDayYearLayout.removeAllViews(); 298 if (viewIndices[YEAR_INDEX] == 0) { 299 mMonthDayYearLayout.addView(mHeaderYearTextView); 300 mMonthDayYearLayout.addView(mMonthAndDayLayout); 301 } else { 302 mMonthDayYearLayout.addView(mMonthAndDayLayout); 303 mMonthDayYearLayout.addView(mHeaderYearTextView); 304 } 305 306 // Position Day and Month views within the MonthAndDay view. 307 mMonthAndDayLayout.removeAllViews(); 308 if (viewIndices[MONTH_INDEX] > viewIndices[DAY_INDEX]) { 309 mMonthAndDayLayout.addView(mHeaderDayOfMonthTextView); 310 mMonthAndDayLayout.addView(mHeaderMonthTextView); 311 } else { 312 mMonthAndDayLayout.addView(mHeaderMonthTextView); 313 mMonthAndDayLayout.addView(mHeaderDayOfMonthTextView); 314 } 315 316 mHeaderMonthTextView.setText(mCurrentDate.getDisplayName(Calendar.MONTH, Calendar.SHORT, 317 Locale.getDefault()).toUpperCase(Locale.getDefault())); 318 mHeaderDayOfMonthTextView.setText(mDayFormat.format(mCurrentDate.getTime())); 319 mHeaderYearTextView.setText(mYearFormat.format(mCurrentDate.getTime())); 320 321 // Accessibility. 322 long millis = mCurrentDate.getTimeInMillis(); 323 mAnimator.setDateMillis(millis); 324 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR; 325 String monthAndDayText = DateUtils.formatDateTime(mContext, millis, flags); 326 mMonthAndDayLayout.setContentDescription(monthAndDayText); 327 328 if (announce) { 329 flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 330 String fullDateText = DateUtils.formatDateTime(mContext, millis, flags); 331 mAnimator.announceForAccessibility(fullDateText); 332 } 333 } 334 335 private void setCurrentView(final int viewIndex) { 336 long millis = mCurrentDate.getTimeInMillis(); 337 338 switch (viewIndex) { 339 case MONTH_AND_DAY_VIEW: 340 mDayPickerView.setDate(getSelectedDay().getTimeInMillis()); 341 if (mCurrentView != viewIndex) { 342 mMonthAndDayLayout.setSelected(true); 343 mHeaderYearTextView.setSelected(false); 344 mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); 345 mCurrentView = viewIndex; 346 } 347 348 final int flags = DateUtils.FORMAT_SHOW_DATE; 349 final String dayString = DateUtils.formatDateTime(mContext, millis, flags); 350 mAnimator.setContentDescription(mDayPickerDescription + ": " + dayString); 351 mAnimator.announceForAccessibility(mSelectDay); 352 break; 353 case YEAR_VIEW: 354 mYearPickerView.onDateChanged(); 355 if (mCurrentView != viewIndex) { 356 mMonthAndDayLayout.setSelected(false); 357 mHeaderYearTextView.setSelected(true); 358 mAnimator.setDisplayedChild(YEAR_VIEW); 359 mCurrentView = viewIndex; 360 } 361 362 final CharSequence yearString = mYearFormat.format(millis); 363 mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString); 364 mAnimator.announceForAccessibility(mSelectYear); 365 break; 366 } 367 } 368 369 @Override 370 public void init(int year, int monthOfYear, int dayOfMonth, 371 DatePicker.OnDateChangedListener callBack) { 372 mCurrentDate.set(Calendar.YEAR, year); 373 mCurrentDate.set(Calendar.MONTH, monthOfYear); 374 mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); 375 376 mDateChangedListener = callBack; 377 378 onDateChanged(false, false); 379 } 380 381 @Override 382 public void updateDate(int year, int month, int dayOfMonth) { 383 mCurrentDate.set(Calendar.YEAR, year); 384 mCurrentDate.set(Calendar.MONTH, month); 385 mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); 386 387 onDateChanged(false, true); 388 } 389 390 private void onDateChanged(boolean fromUser, boolean callbackToClient) { 391 if (callbackToClient && mDateChangedListener != null) { 392 final int year = mCurrentDate.get(Calendar.YEAR); 393 final int monthOfYear = mCurrentDate.get(Calendar.MONTH); 394 final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH); 395 mDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth); 396 } 397 398 for (OnDateChangedListener listener : mListeners) { 399 listener.onDateChanged(); 400 } 401 402 mDayPickerView.setDate(getSelectedDay().getTimeInMillis()); 403 404 updateDisplay(fromUser); 405 406 if (fromUser) { 407 tryVibrate(); 408 } 409 } 410 411 @Override 412 public int getYear() { 413 return mCurrentDate.get(Calendar.YEAR); 414 } 415 416 @Override 417 public int getMonth() { 418 return mCurrentDate.get(Calendar.MONTH); 419 } 420 421 @Override 422 public int getDayOfMonth() { 423 return mCurrentDate.get(Calendar.DAY_OF_MONTH); 424 } 425 426 @Override 427 public void setMinDate(long minDate) { 428 mTempDate.setTimeInMillis(minDate); 429 if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) 430 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { 431 return; 432 } 433 if (mCurrentDate.before(mTempDate)) { 434 mCurrentDate.setTimeInMillis(minDate); 435 onDateChanged(false, true); 436 } 437 mMinDate.setTimeInMillis(minDate); 438 mDayPickerView.setMinDate(minDate); 439 mYearPickerView.setRange(mMinDate, mMaxDate); 440 } 441 442 @Override 443 public Calendar getMinDate() { 444 return mMinDate; 445 } 446 447 @Override 448 public void setMaxDate(long maxDate) { 449 mTempDate.setTimeInMillis(maxDate); 450 if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) 451 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { 452 return; 453 } 454 if (mCurrentDate.after(mTempDate)) { 455 mCurrentDate.setTimeInMillis(maxDate); 456 onDateChanged(false, true); 457 } 458 mMaxDate.setTimeInMillis(maxDate); 459 mDayPickerView.setMaxDate(maxDate); 460 mYearPickerView.setRange(mMinDate, mMaxDate); 461 } 462 463 @Override 464 public Calendar getMaxDate() { 465 return mMaxDate; 466 } 467 468 @Override 469 public void setFirstDayOfWeek(int firstDayOfWeek) { 470 mFirstDayOfWeek = firstDayOfWeek; 471 472 mDayPickerView.setFirstDayOfWeek(firstDayOfWeek); 473 } 474 475 @Override 476 public int getFirstDayOfWeek() { 477 if (mFirstDayOfWeek != USE_LOCALE) { 478 return mFirstDayOfWeek; 479 } 480 return mCurrentDate.getFirstDayOfWeek(); 481 } 482 483 @Override 484 public void setEnabled(boolean enabled) { 485 mMonthAndDayLayout.setEnabled(enabled); 486 mHeaderYearTextView.setEnabled(enabled); 487 mAnimator.setEnabled(enabled); 488 mIsEnabled = enabled; 489 } 490 491 @Override 492 public boolean isEnabled() { 493 return mIsEnabled; 494 } 495 496 @Override 497 public CalendarView getCalendarView() { 498 throw new UnsupportedOperationException( 499 "CalendarView does not exists for the new DatePicker"); 500 } 501 502 @Override 503 public void setCalendarViewShown(boolean shown) { 504 // No-op for compatibility with the old DatePicker. 505 } 506 507 @Override 508 public boolean getCalendarViewShown() { 509 return false; 510 } 511 512 @Override 513 public void setSpinnersShown(boolean shown) { 514 // No-op for compatibility with the old DatePicker. 515 } 516 517 @Override 518 public boolean getSpinnersShown() { 519 return false; 520 } 521 522 @Override 523 public void onConfigurationChanged(Configuration newConfig) { 524 mYearFormat = new SimpleDateFormat("y", newConfig.locale); 525 mDayFormat = new SimpleDateFormat("d", newConfig.locale); 526 } 527 528 @Override 529 public Parcelable onSaveInstanceState(Parcelable superState) { 530 final int year = mCurrentDate.get(Calendar.YEAR); 531 final int month = mCurrentDate.get(Calendar.MONTH); 532 final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH); 533 534 int listPosition = -1; 535 int listPositionOffset = -1; 536 537 if (mCurrentView == MONTH_AND_DAY_VIEW) { 538 listPosition = mDayPickerView.getMostVisiblePosition(); 539 } else if (mCurrentView == YEAR_VIEW) { 540 listPosition = mYearPickerView.getFirstVisiblePosition(); 541 listPositionOffset = mYearPickerView.getFirstPositionOffset(); 542 } 543 544 return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(), 545 mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset); 546 } 547 548 @Override 549 public void onRestoreInstanceState(Parcelable state) { 550 SavedState ss = (SavedState) state; 551 552 mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay()); 553 mCurrentView = ss.getCurrentView(); 554 mMinDate.setTimeInMillis(ss.getMinDate()); 555 mMaxDate.setTimeInMillis(ss.getMaxDate()); 556 557 updateDisplay(false); 558 setCurrentView(mCurrentView); 559 560 final int listPosition = ss.getListPosition(); 561 if (listPosition != -1) { 562 if (mCurrentView == MONTH_AND_DAY_VIEW) { 563 mDayPickerView.postSetSelection(listPosition); 564 } else if (mCurrentView == YEAR_VIEW) { 565 mYearPickerView.postSetSelectionFromTop(listPosition, ss.getListPositionOffset()); 566 } 567 } 568 } 569 570 @Override 571 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 572 onPopulateAccessibilityEvent(event); 573 return true; 574 } 575 576 @Override 577 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 578 event.getText().add(mCurrentDate.getTime().toString()); 579 } 580 581 @Override 582 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 583 event.setClassName(DatePicker.class.getName()); 584 } 585 586 @Override 587 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 588 info.setClassName(DatePicker.class.getName()); 589 } 590 591 @Override 592 public void onYearSelected(int year) { 593 adjustDayInMonthIfNeeded(mCurrentDate.get(Calendar.MONTH), year); 594 mCurrentDate.set(Calendar.YEAR, year); 595 onDateChanged(true, true); 596 597 // Auto-advance to month and day view. 598 setCurrentView(MONTH_AND_DAY_VIEW); 599 } 600 601 // If the newly selected month / year does not contain the currently selected day number, 602 // change the selected day number to the last day of the selected month or year. 603 // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 604 // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 605 private void adjustDayInMonthIfNeeded(int month, int year) { 606 int day = mCurrentDate.get(Calendar.DAY_OF_MONTH); 607 int daysInMonth = getDaysInMonth(month, year); 608 if (day > daysInMonth) { 609 mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth); 610 } 611 } 612 613 public static int getDaysInMonth(int month, int year) { 614 switch (month) { 615 case Calendar.JANUARY: 616 case Calendar.MARCH: 617 case Calendar.MAY: 618 case Calendar.JULY: 619 case Calendar.AUGUST: 620 case Calendar.OCTOBER: 621 case Calendar.DECEMBER: 622 return 31; 623 case Calendar.APRIL: 624 case Calendar.JUNE: 625 case Calendar.SEPTEMBER: 626 case Calendar.NOVEMBER: 627 return 30; 628 case Calendar.FEBRUARY: 629 return (year % 4 == 0) ? 29 : 28; 630 default: 631 throw new IllegalArgumentException("Invalid Month"); 632 } 633 } 634 635 @Override 636 public void registerOnDateChangedListener(OnDateChangedListener listener) { 637 mListeners.add(listener); 638 } 639 640 @Override 641 public Calendar getSelectedDay() { 642 return mCurrentDate; 643 } 644 645 @Override 646 public void tryVibrate() { 647 mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE); 648 } 649 650 @Override 651 public void onClick(View v) { 652 tryVibrate(); 653 if (v.getId() == R.id.date_picker_year) { 654 setCurrentView(YEAR_VIEW); 655 } else if (v.getId() == R.id.date_picker_month_and_day_layout) { 656 setCurrentView(MONTH_AND_DAY_VIEW); 657 } 658 } 659 660 /** 661 * Listener called when the user selects a day in the day picker view. 662 */ 663 private final DayPickerView.OnDaySelectedListener 664 mOnDaySelectedListener = new DayPickerView.OnDaySelectedListener() { 665 @Override 666 public void onDaySelected(DayPickerView view, Calendar day) { 667 mCurrentDate.setTimeInMillis(day.getTimeInMillis()); 668 onDateChanged(true, true); 669 } 670 }; 671 672 /** 673 * Class for managing state storing/restoring. 674 */ 675 private static class SavedState extends View.BaseSavedState { 676 677 private final int mSelectedYear; 678 private final int mSelectedMonth; 679 private final int mSelectedDay; 680 private final long mMinDate; 681 private final long mMaxDate; 682 private final int mCurrentView; 683 private final int mListPosition; 684 private final int mListPositionOffset; 685 686 /** 687 * Constructor called from {@link DatePicker#onSaveInstanceState()} 688 */ 689 private SavedState(Parcelable superState, int year, int month, int day, 690 long minDate, long maxDate, int currentView, int listPosition, 691 int listPositionOffset) { 692 super(superState); 693 mSelectedYear = year; 694 mSelectedMonth = month; 695 mSelectedDay = day; 696 mMinDate = minDate; 697 mMaxDate = maxDate; 698 mCurrentView = currentView; 699 mListPosition = listPosition; 700 mListPositionOffset = listPositionOffset; 701 } 702 703 /** 704 * Constructor called from {@link #CREATOR} 705 */ 706 private SavedState(Parcel in) { 707 super(in); 708 mSelectedYear = in.readInt(); 709 mSelectedMonth = in.readInt(); 710 mSelectedDay = in.readInt(); 711 mMinDate = in.readLong(); 712 mMaxDate = in.readLong(); 713 mCurrentView = in.readInt(); 714 mListPosition = in.readInt(); 715 mListPositionOffset = in.readInt(); 716 } 717 718 @Override 719 public void writeToParcel(Parcel dest, int flags) { 720 super.writeToParcel(dest, flags); 721 dest.writeInt(mSelectedYear); 722 dest.writeInt(mSelectedMonth); 723 dest.writeInt(mSelectedDay); 724 dest.writeLong(mMinDate); 725 dest.writeLong(mMaxDate); 726 dest.writeInt(mCurrentView); 727 dest.writeInt(mListPosition); 728 dest.writeInt(mListPositionOffset); 729 } 730 731 public int getSelectedDay() { 732 return mSelectedDay; 733 } 734 735 public int getSelectedMonth() { 736 return mSelectedMonth; 737 } 738 739 public int getSelectedYear() { 740 return mSelectedYear; 741 } 742 743 public long getMinDate() { 744 return mMinDate; 745 } 746 747 public long getMaxDate() { 748 return mMaxDate; 749 } 750 751 public int getCurrentView() { 752 return mCurrentView; 753 } 754 755 public int getListPosition() { 756 return mListPosition; 757 } 758 759 public int getListPositionOffset() { 760 return mListPositionOffset; 761 } 762 763 @SuppressWarnings("all") 764 // suppress unused and hiding 765 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { 766 767 public SavedState createFromParcel(Parcel in) { 768 return new SavedState(in); 769 } 770 771 public SavedState[] newArray(int size) { 772 return new SavedState[size]; 773 } 774 }; 775 } 776 } 777