DatePickerCalendarDelegate.java revision a770530e121cd62b74161a70104441720f6eb1c2
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 com.android.internal.R; 20 21import android.annotation.Nullable; 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.icu.text.DisplayContext; 28import android.icu.text.SimpleDateFormat; 29import android.icu.util.Calendar; 30import android.os.Parcelable; 31import android.text.format.DateFormat; 32import android.text.format.DateUtils; 33import android.util.AttributeSet; 34import android.util.StateSet; 35import android.view.HapticFeedbackConstants; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.View.OnClickListener; 39import android.view.ViewGroup; 40import android.view.accessibility.AccessibilityEvent; 41import android.widget.DayPickerView.OnDaySelectedListener; 42import android.widget.YearPickerView.OnYearSelectedListener; 43 44import java.util.Locale; 45 46/** 47 * A delegate for picking up a date (day / month / year). 48 */ 49class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { 50 private static final int USE_LOCALE = 0; 51 52 private static final int UNINITIALIZED = -1; 53 private static final int VIEW_MONTH_DAY = 0; 54 private static final int VIEW_YEAR = 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[] ATTRS_TEXT_COLOR = new int[] { 62 com.android.internal.R.attr.textColor}; 63 private static final int[] ATTRS_DISABLED_ALPHA = new int[] { 64 com.android.internal.R.attr.disabledAlpha}; 65 66 private SimpleDateFormat mYearFormat; 67 private SimpleDateFormat mMonthDayFormat; 68 private SimpleDateFormat mAccessibilityEventFormat; 69 70 71 // Top-level container. 72 private ViewGroup mContainer; 73 74 // Header views. 75 private TextView mHeaderYear; 76 private TextView mHeaderMonthDay; 77 78 // Picker views. 79 private ViewAnimator mAnimator; 80 private DayPickerView mDayPickerView; 81 private YearPickerView mYearPickerView; 82 83 // Accessibility strings. 84 private String mSelectDay; 85 private String mSelectYear; 86 87 private DatePicker.OnDateChangedListener mDateChangedListener; 88 89 private int mCurrentView = UNINITIALIZED; 90 91 private final Calendar mCurrentDate; 92 private final Calendar mTempDate; 93 private final Calendar mMinDate; 94 private final Calendar mMaxDate; 95 96 private int mFirstDayOfWeek = USE_LOCALE; 97 98 public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs, 99 int defStyleAttr, int defStyleRes) { 100 super(delegator, context); 101 102 final Locale locale = mCurrentLocale; 103 mCurrentDate = Calendar.getInstance(locale); 104 mTempDate = Calendar.getInstance(locale); 105 mMinDate = Calendar.getInstance(locale); 106 mMaxDate = Calendar.getInstance(locale); 107 108 mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1); 109 mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31); 110 111 final Resources res = mDelegator.getResources(); 112 final TypedArray a = mContext.obtainStyledAttributes(attrs, 113 R.styleable.DatePicker, defStyleAttr, defStyleRes); 114 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 115 Context.LAYOUT_INFLATER_SERVICE); 116 final int layoutResourceId = a.getResourceId( 117 R.styleable.DatePicker_internalLayout, R.layout.date_picker_material); 118 119 // Set up and attach container. 120 mContainer = (ViewGroup) inflater.inflate(layoutResourceId, mDelegator, false); 121 mDelegator.addView(mContainer); 122 123 // Set up header views. 124 final ViewGroup header = (ViewGroup) mContainer.findViewById(R.id.date_picker_header); 125 mHeaderYear = (TextView) header.findViewById(R.id.date_picker_header_year); 126 mHeaderYear.setOnClickListener(mOnHeaderClickListener); 127 mHeaderMonthDay = (TextView) header.findViewById(R.id.date_picker_header_date); 128 mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener); 129 130 // For the sake of backwards compatibility, attempt to extract the text 131 // color from the header month text appearance. If it's set, we'll let 132 // that override the "real" header text color. 133 ColorStateList headerTextColor = null; 134 135 @SuppressWarnings("deprecation") 136 final int monthHeaderTextAppearance = a.getResourceId( 137 R.styleable.DatePicker_headerMonthTextAppearance, 0); 138 if (monthHeaderTextAppearance != 0) { 139 final TypedArray textAppearance = mContext.obtainStyledAttributes(null, 140 ATTRS_TEXT_COLOR, 0, monthHeaderTextAppearance); 141 final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0); 142 headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor); 143 textAppearance.recycle(); 144 } 145 146 if (headerTextColor == null) { 147 headerTextColor = a.getColorStateList(R.styleable.DatePicker_headerTextColor); 148 } 149 150 if (headerTextColor != null) { 151 mHeaderYear.setTextColor(headerTextColor); 152 mHeaderMonthDay.setTextColor(headerTextColor); 153 } 154 155 // Set up header background, if available. 156 if (a.hasValueOrEmpty(R.styleable.DatePicker_headerBackground)) { 157 header.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground)); 158 } 159 160 a.recycle(); 161 162 // Set up picker container. 163 mAnimator = (ViewAnimator) mContainer.findViewById(R.id.animator); 164 165 // Set up day picker view. 166 mDayPickerView = (DayPickerView) mAnimator.findViewById(R.id.date_picker_day_picker); 167 mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek); 168 mDayPickerView.setMinDate(mMinDate.getTimeInMillis()); 169 mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis()); 170 mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); 171 mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener); 172 173 // Set up year picker view. 174 mYearPickerView = (YearPickerView) mAnimator.findViewById(R.id.date_picker_year_picker); 175 mYearPickerView.setRange(mMinDate, mMaxDate); 176 mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR)); 177 mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener); 178 179 // Set up content descriptions. 180 mSelectDay = res.getString(R.string.select_day); 181 mSelectYear = res.getString(R.string.select_year); 182 183 // Initialize for current locale. This also initializes the date, so no 184 // need to call onDateChanged. 185 onLocaleChanged(mCurrentLocale); 186 187 setCurrentView(VIEW_MONTH_DAY); 188 } 189 190 /** 191 * The legacy text color might have been poorly defined. Ensures that it 192 * has an appropriate activated state, using the selected state if one 193 * exists or modifying the default text color otherwise. 194 * 195 * @param color a legacy text color, or {@code null} 196 * @return a color state list with an appropriate activated state, or 197 * {@code null} if a valid activated state could not be generated 198 */ 199 @Nullable 200 private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) { 201 if (color == null || color.hasState(R.attr.state_activated)) { 202 return color; 203 } 204 205 final int activatedColor; 206 final int defaultColor; 207 if (color.hasState(R.attr.state_selected)) { 208 activatedColor = color.getColorForState(StateSet.get( 209 StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0); 210 defaultColor = color.getColorForState(StateSet.get( 211 StateSet.VIEW_STATE_ENABLED), 0); 212 } else { 213 activatedColor = color.getDefaultColor(); 214 215 // Generate a non-activated color using the disabled alpha. 216 final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA); 217 final float disabledAlpha = ta.getFloat(0, 0.30f); 218 defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha); 219 } 220 221 if (activatedColor == 0 || defaultColor == 0) { 222 // We somehow failed to obtain the colors. 223 return null; 224 } 225 226 final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}}; 227 final int[] colors = new int[] { activatedColor, defaultColor }; 228 return new ColorStateList(stateSet, colors); 229 } 230 231 private int multiplyAlphaComponent(int color, float alphaMod) { 232 final int srcRgb = color & 0xFFFFFF; 233 final int srcAlpha = (color >> 24) & 0xFF; 234 final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f); 235 return srcRgb | (dstAlpha << 24); 236 } 237 238 /** 239 * Listener called when the user selects a day in the day picker view. 240 */ 241 private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() { 242 @Override 243 public void onDaySelected(DayPickerView view, Calendar day) { 244 mCurrentDate.setTimeInMillis(day.getTimeInMillis()); 245 onDateChanged(true, true); 246 } 247 }; 248 249 /** 250 * Listener called when the user selects a year in the year picker view. 251 */ 252 private final OnYearSelectedListener mOnYearSelectedListener = new OnYearSelectedListener() { 253 @Override 254 public void onYearChanged(YearPickerView view, int year) { 255 // If the newly selected month / year does not contain the 256 // currently selected day number, change the selected day number 257 // to the last day of the selected month or year. 258 // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 259 // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 260 final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH); 261 final int month = mCurrentDate.get(Calendar.MONTH); 262 final int daysInMonth = getDaysInMonth(month, year); 263 if (day > daysInMonth) { 264 mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth); 265 } 266 267 mCurrentDate.set(Calendar.YEAR, year); 268 onDateChanged(true, true); 269 270 // Automatically switch to day picker. 271 setCurrentView(VIEW_MONTH_DAY); 272 273 // Switch focus back to the year text. 274 mHeaderYear.requestFocus(); 275 } 276 }; 277 278 /** 279 * Listener called when the user clicks on a header item. 280 */ 281 private final OnClickListener mOnHeaderClickListener = new OnClickListener() { 282 @Override 283 public void onClick(View v) { 284 tryVibrate(); 285 286 switch (v.getId()) { 287 case R.id.date_picker_header_year: 288 setCurrentView(VIEW_YEAR); 289 break; 290 case R.id.date_picker_header_date: 291 setCurrentView(VIEW_MONTH_DAY); 292 break; 293 } 294 } 295 }; 296 297 @Override 298 protected void onLocaleChanged(Locale locale) { 299 final TextView headerYear = mHeaderYear; 300 if (headerYear == null) { 301 // Abort, we haven't initialized yet. This method will get called 302 // again later after everything has been set up. 303 return; 304 } 305 306 // Update the date formatter. 307 final String datePattern = DateFormat.getBestDateTimePattern(locale, "EMMMd"); 308 mMonthDayFormat = new SimpleDateFormat(datePattern, locale); 309 mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE); 310 mYearFormat = new SimpleDateFormat("y", locale); 311 312 // Clear out the lazily-initialized accessibility event formatter. 313 mAccessibilityEventFormat = null; 314 315 // Update the header text. 316 onCurrentDateChanged(false); 317 } 318 319 private void onCurrentDateChanged(boolean announce) { 320 if (mHeaderYear == null) { 321 // Abort, we haven't initialized yet. This method will get called 322 // again later after everything has been set up. 323 return; 324 } 325 326 final String year = mYearFormat.format(mCurrentDate.getTime()); 327 mHeaderYear.setText(year); 328 329 final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime()); 330 mHeaderMonthDay.setText(monthDay); 331 332 // TODO: This should use live regions. 333 if (announce) { 334 final long millis = mCurrentDate.getTimeInMillis(); 335 final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; 336 final String fullDateText = DateUtils.formatDateTime(mContext, millis, flags); 337 mAnimator.announceForAccessibility(fullDateText); 338 } 339 } 340 341 private void setCurrentView(final int viewIndex) { 342 switch (viewIndex) { 343 case VIEW_MONTH_DAY: 344 mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); 345 346 if (mCurrentView != viewIndex) { 347 mHeaderMonthDay.setActivated(true); 348 mHeaderYear.setActivated(false); 349 mAnimator.setDisplayedChild(VIEW_MONTH_DAY); 350 mCurrentView = viewIndex; 351 } 352 353 mAnimator.announceForAccessibility(mSelectDay); 354 break; 355 case VIEW_YEAR: 356 final int year = mCurrentDate.get(Calendar.YEAR); 357 mYearPickerView.setYear(year); 358 mYearPickerView.post(new Runnable() { 359 @Override 360 public void run() { 361 mYearPickerView.requestFocus(); 362 final View selected = mYearPickerView.getSelectedView(); 363 if (selected != null) { 364 selected.requestFocus(); 365 } 366 } 367 }); 368 369 if (mCurrentView != viewIndex) { 370 mHeaderMonthDay.setActivated(false); 371 mHeaderYear.setActivated(true); 372 mAnimator.setDisplayedChild(VIEW_YEAR); 373 mCurrentView = viewIndex; 374 } 375 376 mAnimator.announceForAccessibility(mSelectYear); 377 break; 378 } 379 } 380 381 @Override 382 public void init(int year, int monthOfYear, int dayOfMonth, 383 DatePicker.OnDateChangedListener callBack) { 384 mCurrentDate.set(Calendar.YEAR, year); 385 mCurrentDate.set(Calendar.MONTH, monthOfYear); 386 mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); 387 388 onDateChanged(false, false); 389 390 mDateChangedListener = callBack; 391 } 392 393 @Override 394 public void updateDate(int year, int month, int dayOfMonth) { 395 mCurrentDate.set(Calendar.YEAR, year); 396 mCurrentDate.set(Calendar.MONTH, month); 397 mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); 398 399 onDateChanged(false, true); 400 } 401 402 private void onDateChanged(boolean fromUser, boolean callbackToClient) { 403 final int year = mCurrentDate.get(Calendar.YEAR); 404 405 if (callbackToClient && mDateChangedListener != null) { 406 final int monthOfYear = mCurrentDate.get(Calendar.MONTH); 407 final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH); 408 mDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth); 409 } 410 411 mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); 412 mYearPickerView.setYear(year); 413 414 onCurrentDateChanged(fromUser); 415 416 if (fromUser) { 417 tryVibrate(); 418 } 419 } 420 421 @Override 422 public int getYear() { 423 return mCurrentDate.get(Calendar.YEAR); 424 } 425 426 @Override 427 public int getMonth() { 428 return mCurrentDate.get(Calendar.MONTH); 429 } 430 431 @Override 432 public int getDayOfMonth() { 433 return mCurrentDate.get(Calendar.DAY_OF_MONTH); 434 } 435 436 @Override 437 public void setMinDate(long minDate) { 438 mTempDate.setTimeInMillis(minDate); 439 if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) 440 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) { 441 // Same day, no-op. 442 return; 443 } 444 if (mCurrentDate.before(mTempDate)) { 445 mCurrentDate.setTimeInMillis(minDate); 446 onDateChanged(false, true); 447 } 448 mMinDate.setTimeInMillis(minDate); 449 mDayPickerView.setMinDate(minDate); 450 mYearPickerView.setRange(mMinDate, mMaxDate); 451 } 452 453 @Override 454 public Calendar getMinDate() { 455 return mMinDate; 456 } 457 458 @Override 459 public void setMaxDate(long maxDate) { 460 mTempDate.setTimeInMillis(maxDate); 461 if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) 462 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) { 463 // Same day, no-op. 464 return; 465 } 466 if (mCurrentDate.after(mTempDate)) { 467 mCurrentDate.setTimeInMillis(maxDate); 468 onDateChanged(false, true); 469 } 470 mMaxDate.setTimeInMillis(maxDate); 471 mDayPickerView.setMaxDate(maxDate); 472 mYearPickerView.setRange(mMinDate, mMaxDate); 473 } 474 475 @Override 476 public Calendar getMaxDate() { 477 return mMaxDate; 478 } 479 480 @Override 481 public void setFirstDayOfWeek(int firstDayOfWeek) { 482 mFirstDayOfWeek = firstDayOfWeek; 483 484 mDayPickerView.setFirstDayOfWeek(firstDayOfWeek); 485 } 486 487 @Override 488 public int getFirstDayOfWeek() { 489 if (mFirstDayOfWeek != USE_LOCALE) { 490 return mFirstDayOfWeek; 491 } 492 return mCurrentDate.getFirstDayOfWeek(); 493 } 494 495 @Override 496 public void setEnabled(boolean enabled) { 497 mContainer.setEnabled(enabled); 498 mDayPickerView.setEnabled(enabled); 499 mYearPickerView.setEnabled(enabled); 500 mHeaderYear.setEnabled(enabled); 501 mHeaderMonthDay.setEnabled(enabled); 502 } 503 504 @Override 505 public boolean isEnabled() { 506 return mContainer.isEnabled(); 507 } 508 509 @Override 510 public CalendarView getCalendarView() { 511 throw new UnsupportedOperationException("Not supported by calendar-mode DatePicker"); 512 } 513 514 @Override 515 public void setCalendarViewShown(boolean shown) { 516 // No-op for compatibility with the old DatePicker. 517 } 518 519 @Override 520 public boolean getCalendarViewShown() { 521 return false; 522 } 523 524 @Override 525 public void setSpinnersShown(boolean shown) { 526 // No-op for compatibility with the old DatePicker. 527 } 528 529 @Override 530 public boolean getSpinnersShown() { 531 return false; 532 } 533 534 @Override 535 public void onConfigurationChanged(Configuration newConfig) { 536 setCurrentLocale(newConfig.locale); 537 } 538 539 @Override 540 public Parcelable onSaveInstanceState(Parcelable superState) { 541 final int year = mCurrentDate.get(Calendar.YEAR); 542 final int month = mCurrentDate.get(Calendar.MONTH); 543 final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH); 544 545 int listPosition = -1; 546 int listPositionOffset = -1; 547 548 if (mCurrentView == VIEW_MONTH_DAY) { 549 listPosition = mDayPickerView.getMostVisiblePosition(); 550 } else if (mCurrentView == VIEW_YEAR) { 551 listPosition = mYearPickerView.getFirstVisiblePosition(); 552 listPositionOffset = mYearPickerView.getFirstPositionOffset(); 553 } 554 555 return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(), 556 mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset); 557 } 558 559 @Override 560 public void onRestoreInstanceState(Parcelable state) { 561 if (state instanceof SavedState) { 562 final SavedState ss = (SavedState) state; 563 564 // TODO: Move instance state into DayPickerView, YearPickerView. 565 mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay()); 566 mMinDate.setTimeInMillis(ss.getMinDate()); 567 mMaxDate.setTimeInMillis(ss.getMaxDate()); 568 569 onCurrentDateChanged(false); 570 571 final int currentView = ss.getCurrentView(); 572 setCurrentView(currentView); 573 574 final int listPosition = ss.getListPosition(); 575 if (listPosition != -1) { 576 if (currentView == VIEW_MONTH_DAY) { 577 mDayPickerView.setPosition(listPosition); 578 } else if (currentView == VIEW_YEAR) { 579 final int listPositionOffset = ss.getListPositionOffset(); 580 mYearPickerView.setSelectionFromTop(listPosition, listPositionOffset); 581 } 582 } 583 } 584 } 585 586 @Override 587 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 588 onPopulateAccessibilityEvent(event); 589 return true; 590 } 591 592 @Override 593 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 594 if (mAccessibilityEventFormat == null) { 595 final String pattern = DateFormat.getBestDateTimePattern(mCurrentLocale, "EMMMMdy"); 596 mAccessibilityEventFormat = new SimpleDateFormat(pattern); 597 } 598 final CharSequence text = mAccessibilityEventFormat.format(mCurrentDate.getTime()); 599 event.getText().add(text); 600 } 601 602 public CharSequence getAccessibilityClassName() { 603 return DatePicker.class.getName(); 604 } 605 606 public static int getDaysInMonth(int month, int year) { 607 switch (month) { 608 case Calendar.JANUARY: 609 case Calendar.MARCH: 610 case Calendar.MAY: 611 case Calendar.JULY: 612 case Calendar.AUGUST: 613 case Calendar.OCTOBER: 614 case Calendar.DECEMBER: 615 return 31; 616 case Calendar.APRIL: 617 case Calendar.JUNE: 618 case Calendar.SEPTEMBER: 619 case Calendar.NOVEMBER: 620 return 30; 621 case Calendar.FEBRUARY: 622 return (year % 4 == 0) ? 29 : 28; 623 default: 624 throw new IllegalArgumentException("Invalid Month"); 625 } 626 } 627 628 private void tryVibrate() { 629 mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE); 630 } 631} 632