| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.widget; |
| |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.content.res.ColorStateList; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.icu.text.DateFormat; |
| import android.icu.text.DisplayContext; |
| import android.icu.util.Calendar; |
| import android.os.Parcelable; |
| import android.util.AttributeSet; |
| import android.util.StateSet; |
| import android.view.HapticFeedbackConstants; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.widget.DayPickerView.OnDaySelectedListener; |
| import android.widget.YearPickerView.OnYearSelectedListener; |
| |
| import com.android.internal.R; |
| |
| import java.util.Locale; |
| |
| /** |
| * A delegate for picking up a date (day / month / year). |
| */ |
| class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { |
| private static final int USE_LOCALE = 0; |
| |
| private static final int UNINITIALIZED = -1; |
| private static final int VIEW_MONTH_DAY = 0; |
| private static final int VIEW_YEAR = 1; |
| |
| private static final int DEFAULT_START_YEAR = 1900; |
| private static final int DEFAULT_END_YEAR = 2100; |
| |
| private static final int ANIMATION_DURATION = 300; |
| |
| private static final int[] ATTRS_TEXT_COLOR = new int[] { |
| com.android.internal.R.attr.textColor}; |
| private static final int[] ATTRS_DISABLED_ALPHA = new int[] { |
| com.android.internal.R.attr.disabledAlpha}; |
| |
| private DateFormat mYearFormat; |
| private DateFormat mMonthDayFormat; |
| |
| // Top-level container. |
| private ViewGroup mContainer; |
| |
| // Header views. |
| private TextView mHeaderYear; |
| private TextView mHeaderMonthDay; |
| |
| // Picker views. |
| private ViewAnimator mAnimator; |
| private DayPickerView mDayPickerView; |
| private YearPickerView mYearPickerView; |
| |
| // Accessibility strings. |
| private String mSelectDay; |
| private String mSelectYear; |
| |
| private int mCurrentView = UNINITIALIZED; |
| |
| private final Calendar mTempDate; |
| private final Calendar mMinDate; |
| private final Calendar mMaxDate; |
| |
| private int mFirstDayOfWeek = USE_LOCALE; |
| |
| public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs, |
| int defStyleAttr, int defStyleRes) { |
| super(delegator, context); |
| |
| final Locale locale = mCurrentLocale; |
| mCurrentDate = Calendar.getInstance(locale); |
| mTempDate = Calendar.getInstance(locale); |
| mMinDate = Calendar.getInstance(locale); |
| mMaxDate = Calendar.getInstance(locale); |
| |
| mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1); |
| mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31); |
| |
| final Resources res = mDelegator.getResources(); |
| final TypedArray a = mContext.obtainStyledAttributes(attrs, |
| R.styleable.DatePicker, defStyleAttr, defStyleRes); |
| final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( |
| Context.LAYOUT_INFLATER_SERVICE); |
| final int layoutResourceId = a.getResourceId( |
| R.styleable.DatePicker_internalLayout, R.layout.date_picker_material); |
| |
| // Set up and attach container. |
| mContainer = (ViewGroup) inflater.inflate(layoutResourceId, mDelegator, false); |
| mContainer.setSaveFromParentEnabled(false); |
| mDelegator.addView(mContainer); |
| |
| // Set up header views. |
| final ViewGroup header = mContainer.findViewById(R.id.date_picker_header); |
| mHeaderYear = header.findViewById(R.id.date_picker_header_year); |
| mHeaderYear.setOnClickListener(mOnHeaderClickListener); |
| mHeaderMonthDay = header.findViewById(R.id.date_picker_header_date); |
| mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener); |
| |
| // For the sake of backwards compatibility, attempt to extract the text |
| // color from the header month text appearance. If it's set, we'll let |
| // that override the "real" header text color. |
| ColorStateList headerTextColor = null; |
| |
| @SuppressWarnings("deprecation") |
| final int monthHeaderTextAppearance = a.getResourceId( |
| R.styleable.DatePicker_headerMonthTextAppearance, 0); |
| if (monthHeaderTextAppearance != 0) { |
| final TypedArray textAppearance = mContext.obtainStyledAttributes(null, |
| ATTRS_TEXT_COLOR, 0, monthHeaderTextAppearance); |
| final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0); |
| headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor); |
| textAppearance.recycle(); |
| } |
| |
| if (headerTextColor == null) { |
| headerTextColor = a.getColorStateList(R.styleable.DatePicker_headerTextColor); |
| } |
| |
| if (headerTextColor != null) { |
| mHeaderYear.setTextColor(headerTextColor); |
| mHeaderMonthDay.setTextColor(headerTextColor); |
| } |
| |
| // Set up header background, if available. |
| if (a.hasValueOrEmpty(R.styleable.DatePicker_headerBackground)) { |
| header.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground)); |
| } |
| |
| a.recycle(); |
| |
| // Set up picker container. |
| mAnimator = mContainer.findViewById(R.id.animator); |
| |
| // Set up day picker view. |
| mDayPickerView = mAnimator.findViewById(R.id.date_picker_day_picker); |
| mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek); |
| mDayPickerView.setMinDate(mMinDate.getTimeInMillis()); |
| mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis()); |
| mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); |
| mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener); |
| |
| // Set up year picker view. |
| mYearPickerView = mAnimator.findViewById(R.id.date_picker_year_picker); |
| mYearPickerView.setRange(mMinDate, mMaxDate); |
| mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR)); |
| mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener); |
| |
| // Set up content descriptions. |
| mSelectDay = res.getString(R.string.select_day); |
| mSelectYear = res.getString(R.string.select_year); |
| |
| // Initialize for current locale. This also initializes the date, so no |
| // need to call onDateChanged. |
| onLocaleChanged(mCurrentLocale); |
| |
| setCurrentView(VIEW_MONTH_DAY); |
| } |
| |
| /** |
| * The legacy text color might have been poorly defined. Ensures that it |
| * has an appropriate activated state, using the selected state if one |
| * exists or modifying the default text color otherwise. |
| * |
| * @param color a legacy text color, or {@code null} |
| * @return a color state list with an appropriate activated state, or |
| * {@code null} if a valid activated state could not be generated |
| */ |
| @Nullable |
| private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) { |
| if (color == null || color.hasState(R.attr.state_activated)) { |
| return color; |
| } |
| |
| final int activatedColor; |
| final int defaultColor; |
| if (color.hasState(R.attr.state_selected)) { |
| activatedColor = color.getColorForState(StateSet.get( |
| StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0); |
| defaultColor = color.getColorForState(StateSet.get( |
| StateSet.VIEW_STATE_ENABLED), 0); |
| } else { |
| activatedColor = color.getDefaultColor(); |
| |
| // Generate a non-activated color using the disabled alpha. |
| final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA); |
| final float disabledAlpha = ta.getFloat(0, 0.30f); |
| defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha); |
| } |
| |
| if (activatedColor == 0 || defaultColor == 0) { |
| // We somehow failed to obtain the colors. |
| return null; |
| } |
| |
| final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}}; |
| final int[] colors = new int[] { activatedColor, defaultColor }; |
| return new ColorStateList(stateSet, colors); |
| } |
| |
| private int multiplyAlphaComponent(int color, float alphaMod) { |
| final int srcRgb = color & 0xFFFFFF; |
| final int srcAlpha = (color >> 24) & 0xFF; |
| final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f); |
| return srcRgb | (dstAlpha << 24); |
| } |
| |
| /** |
| * Listener called when the user selects a day in the day picker view. |
| */ |
| private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() { |
| @Override |
| public void onDaySelected(DayPickerView view, Calendar day) { |
| mCurrentDate.setTimeInMillis(day.getTimeInMillis()); |
| onDateChanged(true, true); |
| } |
| }; |
| |
| /** |
| * Listener called when the user selects a year in the year picker view. |
| */ |
| private final OnYearSelectedListener mOnYearSelectedListener = new OnYearSelectedListener() { |
| @Override |
| public void onYearChanged(YearPickerView view, int year) { |
| // If the newly selected month / year does not contain the |
| // currently selected day number, change the selected day number |
| // to the last day of the selected month or year. |
| // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 |
| // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 |
| final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH); |
| final int month = mCurrentDate.get(Calendar.MONTH); |
| final int daysInMonth = getDaysInMonth(month, year); |
| if (day > daysInMonth) { |
| mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth); |
| } |
| |
| mCurrentDate.set(Calendar.YEAR, year); |
| onDateChanged(true, true); |
| |
| // Automatically switch to day picker. |
| setCurrentView(VIEW_MONTH_DAY); |
| |
| // Switch focus back to the year text. |
| mHeaderYear.requestFocus(); |
| } |
| }; |
| |
| /** |
| * Listener called when the user clicks on a header item. |
| */ |
| private final OnClickListener mOnHeaderClickListener = v -> { |
| tryVibrate(); |
| |
| switch (v.getId()) { |
| case R.id.date_picker_header_year: |
| setCurrentView(VIEW_YEAR); |
| break; |
| case R.id.date_picker_header_date: |
| setCurrentView(VIEW_MONTH_DAY); |
| break; |
| } |
| }; |
| |
| @Override |
| protected void onLocaleChanged(Locale locale) { |
| final TextView headerYear = mHeaderYear; |
| if (headerYear == null) { |
| // Abort, we haven't initialized yet. This method will get called |
| // again later after everything has been set up. |
| return; |
| } |
| |
| // Update the date formatter. |
| mMonthDayFormat = DateFormat.getInstanceForSkeleton("EMMMd", locale); |
| mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE); |
| mYearFormat = DateFormat.getInstanceForSkeleton("y", locale); |
| |
| // Update the header text. |
| onCurrentDateChanged(false); |
| } |
| |
| private void onCurrentDateChanged(boolean announce) { |
| if (mHeaderYear == null) { |
| // Abort, we haven't initialized yet. This method will get called |
| // again later after everything has been set up. |
| return; |
| } |
| |
| final String year = mYearFormat.format(mCurrentDate.getTime()); |
| mHeaderYear.setText(year); |
| |
| final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime()); |
| mHeaderMonthDay.setText(monthDay); |
| |
| // TODO: This should use live regions. |
| if (announce) { |
| mAnimator.announceForAccessibility(getFormattedCurrentDate()); |
| } |
| } |
| |
| private void setCurrentView(final int viewIndex) { |
| switch (viewIndex) { |
| case VIEW_MONTH_DAY: |
| mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); |
| |
| if (mCurrentView != viewIndex) { |
| mHeaderMonthDay.setActivated(true); |
| mHeaderYear.setActivated(false); |
| mAnimator.setDisplayedChild(VIEW_MONTH_DAY); |
| mCurrentView = viewIndex; |
| } |
| |
| mAnimator.announceForAccessibility(mSelectDay); |
| break; |
| case VIEW_YEAR: |
| final int year = mCurrentDate.get(Calendar.YEAR); |
| mYearPickerView.setYear(year); |
| mYearPickerView.post(() -> { |
| mYearPickerView.requestFocus(); |
| final View selected = mYearPickerView.getSelectedView(); |
| if (selected != null) { |
| selected.requestFocus(); |
| } |
| }); |
| |
| if (mCurrentView != viewIndex) { |
| mHeaderMonthDay.setActivated(false); |
| mHeaderYear.setActivated(true); |
| mAnimator.setDisplayedChild(VIEW_YEAR); |
| mCurrentView = viewIndex; |
| } |
| |
| mAnimator.announceForAccessibility(mSelectYear); |
| break; |
| } |
| } |
| |
| @Override |
| public void init(int year, int month, int dayOfMonth, |
| DatePicker.OnDateChangedListener callBack) { |
| setDate(year, month, dayOfMonth); |
| onDateChanged(false, false); |
| |
| mOnDateChangedListener = callBack; |
| } |
| |
| @Override |
| public void updateDate(int year, int month, int dayOfMonth) { |
| setDate(year, month, dayOfMonth); |
| onDateChanged(false, true); |
| } |
| |
| private void setDate(int year, int month, int dayOfMonth) { |
| mCurrentDate.set(Calendar.YEAR, year); |
| mCurrentDate.set(Calendar.MONTH, month); |
| mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); |
| resetAutofilledValue(); |
| } |
| |
| private void onDateChanged(boolean fromUser, boolean callbackToClient) { |
| final int year = mCurrentDate.get(Calendar.YEAR); |
| |
| if (callbackToClient |
| && (mOnDateChangedListener != null || mAutoFillChangeListener != null)) { |
| final int monthOfYear = mCurrentDate.get(Calendar.MONTH); |
| final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH); |
| if (mOnDateChangedListener != null) { |
| mOnDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth); |
| } |
| if (mAutoFillChangeListener != null) { |
| mAutoFillChangeListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth); |
| } |
| } |
| |
| mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); |
| mYearPickerView.setYear(year); |
| |
| onCurrentDateChanged(fromUser); |
| |
| if (fromUser) { |
| tryVibrate(); |
| } |
| } |
| |
| @Override |
| public int getYear() { |
| return mCurrentDate.get(Calendar.YEAR); |
| } |
| |
| @Override |
| public int getMonth() { |
| return mCurrentDate.get(Calendar.MONTH); |
| } |
| |
| @Override |
| public int getDayOfMonth() { |
| return mCurrentDate.get(Calendar.DAY_OF_MONTH); |
| } |
| |
| @Override |
| public void setMinDate(long minDate) { |
| mTempDate.setTimeInMillis(minDate); |
| if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) |
| && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) { |
| // Same day, no-op. |
| return; |
| } |
| if (mCurrentDate.before(mTempDate)) { |
| mCurrentDate.setTimeInMillis(minDate); |
| onDateChanged(false, true); |
| } |
| mMinDate.setTimeInMillis(minDate); |
| mDayPickerView.setMinDate(minDate); |
| mYearPickerView.setRange(mMinDate, mMaxDate); |
| } |
| |
| @Override |
| public Calendar getMinDate() { |
| return mMinDate; |
| } |
| |
| @Override |
| public void setMaxDate(long maxDate) { |
| mTempDate.setTimeInMillis(maxDate); |
| if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) |
| && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) { |
| // Same day, no-op. |
| return; |
| } |
| if (mCurrentDate.after(mTempDate)) { |
| mCurrentDate.setTimeInMillis(maxDate); |
| onDateChanged(false, true); |
| } |
| mMaxDate.setTimeInMillis(maxDate); |
| mDayPickerView.setMaxDate(maxDate); |
| mYearPickerView.setRange(mMinDate, mMaxDate); |
| } |
| |
| @Override |
| public Calendar getMaxDate() { |
| return mMaxDate; |
| } |
| |
| @Override |
| public void setFirstDayOfWeek(int firstDayOfWeek) { |
| mFirstDayOfWeek = firstDayOfWeek; |
| |
| mDayPickerView.setFirstDayOfWeek(firstDayOfWeek); |
| } |
| |
| @Override |
| public int getFirstDayOfWeek() { |
| if (mFirstDayOfWeek != USE_LOCALE) { |
| return mFirstDayOfWeek; |
| } |
| return mCurrentDate.getFirstDayOfWeek(); |
| } |
| |
| @Override |
| public void setEnabled(boolean enabled) { |
| mContainer.setEnabled(enabled); |
| mDayPickerView.setEnabled(enabled); |
| mYearPickerView.setEnabled(enabled); |
| mHeaderYear.setEnabled(enabled); |
| mHeaderMonthDay.setEnabled(enabled); |
| } |
| |
| @Override |
| public boolean isEnabled() { |
| return mContainer.isEnabled(); |
| } |
| |
| @Override |
| public CalendarView getCalendarView() { |
| throw new UnsupportedOperationException("Not supported by calendar-mode DatePicker"); |
| } |
| |
| @Override |
| public void setCalendarViewShown(boolean shown) { |
| // No-op for compatibility with the old DatePicker. |
| } |
| |
| @Override |
| public boolean getCalendarViewShown() { |
| return false; |
| } |
| |
| @Override |
| public void setSpinnersShown(boolean shown) { |
| // No-op for compatibility with the old DatePicker. |
| } |
| |
| @Override |
| public boolean getSpinnersShown() { |
| return false; |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| setCurrentLocale(newConfig.locale); |
| } |
| |
| @Override |
| public Parcelable onSaveInstanceState(Parcelable superState) { |
| final int year = mCurrentDate.get(Calendar.YEAR); |
| final int month = mCurrentDate.get(Calendar.MONTH); |
| final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH); |
| |
| int listPosition = -1; |
| int listPositionOffset = -1; |
| |
| if (mCurrentView == VIEW_MONTH_DAY) { |
| listPosition = mDayPickerView.getMostVisiblePosition(); |
| } else if (mCurrentView == VIEW_YEAR) { |
| listPosition = mYearPickerView.getFirstVisiblePosition(); |
| listPositionOffset = mYearPickerView.getFirstPositionOffset(); |
| } |
| |
| return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(), |
| mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset); |
| } |
| |
| @Override |
| public void onRestoreInstanceState(Parcelable state) { |
| if (state instanceof SavedState) { |
| final SavedState ss = (SavedState) state; |
| |
| // TODO: Move instance state into DayPickerView, YearPickerView. |
| mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay()); |
| mMinDate.setTimeInMillis(ss.getMinDate()); |
| mMaxDate.setTimeInMillis(ss.getMaxDate()); |
| |
| onCurrentDateChanged(false); |
| |
| final int currentView = ss.getCurrentView(); |
| setCurrentView(currentView); |
| |
| final int listPosition = ss.getListPosition(); |
| if (listPosition != -1) { |
| if (currentView == VIEW_MONTH_DAY) { |
| mDayPickerView.setPosition(listPosition); |
| } else if (currentView == VIEW_YEAR) { |
| final int listPositionOffset = ss.getListPositionOffset(); |
| mYearPickerView.setSelectionFromTop(listPosition, listPositionOffset); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { |
| onPopulateAccessibilityEvent(event); |
| return true; |
| } |
| |
| public CharSequence getAccessibilityClassName() { |
| return DatePicker.class.getName(); |
| } |
| |
| public static int getDaysInMonth(int month, int year) { |
| switch (month) { |
| case Calendar.JANUARY: |
| case Calendar.MARCH: |
| case Calendar.MAY: |
| case Calendar.JULY: |
| case Calendar.AUGUST: |
| case Calendar.OCTOBER: |
| case Calendar.DECEMBER: |
| return 31; |
| case Calendar.APRIL: |
| case Calendar.JUNE: |
| case Calendar.SEPTEMBER: |
| case Calendar.NOVEMBER: |
| return 30; |
| case Calendar.FEBRUARY: |
| return (year % 4 == 0) ? 29 : 28; |
| default: |
| throw new IllegalArgumentException("Invalid Month"); |
| } |
| } |
| |
| private void tryVibrate() { |
| mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE); |
| } |
| } |