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