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