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