/* * 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 com.tr4android.support.extension.picker.date; 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.os.Parcel; import android.os.Parcelable; import android.text.format.DateUtils; import android.util.AttributeSet; 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.TextView; import android.widget.ViewAnimator; import com.tr4android.appcompat.extension.R; import com.tr4android.support.extension.picker.DateFormatUtils; import com.tr4android.support.extension.utils.ViewCompatUtils; import com.tr4android.support.extension.picker.PickerThemeUtils; import com.tr4android.support.extension.picker.date.DayPickerView.OnDaySelectedListener; import com.tr4android.support.extension.picker.date.YearPickerView.OnYearSelectedListener; import com.tr4android.support.extension.utils.ThemeUtils; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Locale; /** * A delegate for picking up a date (day / month / year). */ class AppCompatDatePickerDelegate extends AppCompatDatePicker.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 SimpleDateFormat mYearFormat; private SimpleDateFormat 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 AppCompatDatePicker.OnDateChangedListener mDateChangedListener; private int mCurrentView = UNINITIALIZED; private final Calendar mCurrentDate; private final Calendar mTempDate; private final Calendar mMinDate; private final Calendar mMaxDate; private int mFirstDayOfWeek = USE_LOCALE; public AppCompatDatePickerDelegate(AppCompatDatePicker 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.DatePickerDialog, defStyleAttr, defStyleRes); final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); // Set up and attach container. mContainer = (ViewGroup) inflater.inflate(R.layout.date_picker_material, mDelegator, false); mDelegator.addView(mContainer); // Set up header views. final ViewGroup header = (ViewGroup) mContainer.findViewById(R.id.date_picker_header); mHeaderYear = (TextView) header.findViewById(R.id.date_picker_header_year); mHeaderYear.setOnClickListener(mOnHeaderClickListener); mHeaderMonthDay = (TextView) header.findViewById(R.id.date_picker_header_date); mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener); // Set up header text color, if available. ColorStateList headerTextColor; if (a.hasValue(R.styleable.DatePickerDialog_headerTextColor)) { headerTextColor = a.getColorStateList(R.styleable.DatePickerDialog_headerTextColor); } else { headerTextColor = PickerThemeUtils.getHeaderTextColorStateList(mContext); } mHeaderYear.setTextColor(headerTextColor); mHeaderMonthDay.setTextColor(headerTextColor); // Set up header background, if available. ViewCompatUtils.setBackground(header, PickerThemeUtils.getHeaderBackground(mContext, a.getColor(R.styleable.DatePickerDialog_headerBackground, ThemeUtils.getThemeAttrColor(mContext, R.attr.colorAccent)))); a.recycle(); // Set up picker container. mAnimator = (ViewAnimator) mContainer.findViewById(R.id.animator); // Set up day picker view. mDayPickerView = (DayPickerView) 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 = (YearPickerView) mAnimator.findViewById(R.id.date_picker_year_picker); mYearPickerView.setRange(mMinDate, mMaxDate); mYearPickerView.setDate(mCurrentDate.getTimeInMillis()); 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); } /** * 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); } }; /** * Listener called when the user clicks on a header item. */ private final OnClickListener mOnHeaderClickListener = new OnClickListener() { @Override public void onClick(View v) { tryVibrate(); int i = v.getId(); if (i == R.id.date_picker_header_year) { setCurrentView(VIEW_YEAR); } else if (i == R.id.date_picker_header_date) { setCurrentView(VIEW_MONTH_DAY); } } }; @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. final String datePattern = DateFormatUtils.getBestDateTimePattern(locale, "EMMMd"); mMonthDayFormat = new SimpleDateFormat(datePattern, locale); mYearFormat = new SimpleDateFormat("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 = DateFormatUtils.format(mMonthDayFormat, mCurrentDate); mHeaderMonthDay.setText(monthDay); // TODO: This should use live regions. if (announce) { final long millis = mCurrentDate.getTimeInMillis(); final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; final String fullDateText = DateUtils.formatDateTime(mContext, millis, flags); ViewCompatUtils.announceForAccessibility(mAnimator, fullDateText); } } private void setCurrentView(final int viewIndex) { switch (viewIndex) { case VIEW_MONTH_DAY: mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); if (mCurrentView != viewIndex) { mHeaderMonthDay.setSelected(true); mHeaderYear.setSelected(false); mAnimator.setDisplayedChild(VIEW_MONTH_DAY); mCurrentView = viewIndex; } ViewCompatUtils.announceForAccessibility(mAnimator, mSelectDay); break; case VIEW_YEAR: mYearPickerView.setDate(mCurrentDate.getTimeInMillis()); if (mCurrentView != viewIndex) { mHeaderMonthDay.setSelected(false); mHeaderYear.setSelected(true); mAnimator.setDisplayedChild(VIEW_YEAR); mCurrentView = viewIndex; } ViewCompatUtils.announceForAccessibility(mAnimator, mSelectYear); break; } } @Override public void init(int year, int monthOfYear, int dayOfMonth, AppCompatDatePicker.OnDateChangedListener callBack) { mCurrentDate.set(Calendar.YEAR, year); mCurrentDate.set(Calendar.MONTH, monthOfYear); mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); mDateChangedListener = callBack; onDateChanged(false, false); } @Override public void updateDate(int year, int month, int dayOfMonth) { mCurrentDate.set(Calendar.YEAR, year); mCurrentDate.set(Calendar.MONTH, month); mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); onDateChanged(false, true); } private void onDateChanged(boolean fromUser, boolean callbackToClient) { final int year = mCurrentDate.get(Calendar.YEAR); if (callbackToClient && mDateChangedListener != null) { final int monthOfYear = mCurrentDate.get(Calendar.MONTH); final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH); mDateChangedListener.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)) { 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)) { 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 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) { 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; } @Override public void onPopulateAccessibilityEvent(AccessibilityEvent event) { event.getText().add(mCurrentDate.getTime().toString()); } public CharSequence getAccessibilityClassName() { return AppCompatDatePicker.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.VIRTUAL_KEY); } /** * Class for managing state storing/restoring. */ private static class SavedState extends View.BaseSavedState { private final int mSelectedYear; private final int mSelectedMonth; private final int mSelectedDay; private final long mMinDate; private final long mMaxDate; private final int mCurrentView; private final int mListPosition; private final int mListPositionOffset; /** * Constructor called from {@link AppCompatDatePicker#onSaveInstanceState()} */ private SavedState(Parcelable superState, int year, int month, int day, long minDate, long maxDate, int currentView, int listPosition, int listPositionOffset) { super(superState); mSelectedYear = year; mSelectedMonth = month; mSelectedDay = day; mMinDate = minDate; mMaxDate = maxDate; mCurrentView = currentView; mListPosition = listPosition; mListPositionOffset = listPositionOffset; } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); mSelectedYear = in.readInt(); mSelectedMonth = in.readInt(); mSelectedDay = in.readInt(); mMinDate = in.readLong(); mMaxDate = in.readLong(); mCurrentView = in.readInt(); mListPosition = in.readInt(); mListPositionOffset = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(mSelectedYear); dest.writeInt(mSelectedMonth); dest.writeInt(mSelectedDay); dest.writeLong(mMinDate); dest.writeLong(mMaxDate); dest.writeInt(mCurrentView); dest.writeInt(mListPosition); dest.writeInt(mListPositionOffset); } public int getSelectedDay() { return mSelectedDay; } public int getSelectedMonth() { return mSelectedMonth; } public int getSelectedYear() { return mSelectedYear; } public long getMinDate() { return mMinDate; } public long getMaxDate() { return mMaxDate; } public int getCurrentView() { return mCurrentView; } public int getListPosition() { return mListPosition; } public int getListPositionOffset() { return mListPositionOffset; } @SuppressWarnings("all") // suppress unused and hiding public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }