/* * Copyright (C) 2013 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.codetroopers.betterpickers.calendardatepicker; import android.app.Activity; import android.content.DialogInterface; import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; import android.support.v4.content.ContextCompat; import android.text.format.DateUtils; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import com.codetroopers.betterpickers.HapticFeedbackController; import com.codetroopers.betterpickers.OnDialogDismissListener; import com.codetroopers.betterpickers.R; import com.codetroopers.betterpickers.Utils; import com.codetroopers.betterpickers.calendardatepicker.MonthAdapter.CalendarDay; import com.nineoldandroids.animation.ObjectAnimator; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; /** * Dialog allowing users to select a date. */ public class CalendarDatePickerDialogFragment extends DialogFragment implements OnClickListener, CalendarDatePickerController { private static final String TAG = "DatePickerDialog"; private static final int UNINITIALIZED = -1; private static final int MONTH_AND_DAY_VIEW = 0; private static final int YEAR_VIEW = 1; private static final String KEY_SELECTED_YEAR = "year"; private static final String KEY_SELECTED_MONTH = "month"; private static final String KEY_SELECTED_DAY = "day"; private static final String KEY_LIST_POSITION = "list_position"; private static final String KEY_WEEK_START = "week_start"; private static final String KEY_DATE_START = "date_start"; private static final String KEY_DATE_END = "date_end"; private static final String KEY_CURRENT_VIEW = "current_view"; private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset"; private static final String KEY_THEME = "theme"; private static final String KEY_DISABLED_DAYS = "disabled_days"; private static final CalendarDay DEFAULT_START_DATE = new CalendarDay(1900, Calendar.JANUARY, 1); private static final CalendarDay DEFAULT_END_DATE = new CalendarDay(2100, Calendar.DECEMBER, 31); private static final int ANIMATION_DURATION = 300; private static final int ANIMATION_DELAY = 500; private static final SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault()); private static final SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("dd", Locale.getDefault()); private final Calendar mCalendar = Calendar.getInstance(); private OnDateSetListener mCallBack; private OnDialogDismissListener mDimissCallback; private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>(); private AccessibleDateAnimator mAnimator; private LinearLayout mSelectedDateLayout; private TextView mDayOfWeekView; private LinearLayout mMonthAndDayView; private TextView mSelectedMonthTextView; private TextView mSelectedDayTextView; private TextView mYearView; private DayPickerView mDayPickerView; private YearPickerView mYearPickerView; private int mCurrentView = UNINITIALIZED; private int mWeekStart = mCalendar.getFirstDayOfWeek(); private CalendarDay mMinDate = DEFAULT_START_DATE; private CalendarDay mMaxDate = DEFAULT_END_DATE; private String mDoneText; private String mCancelText; private SparseArray<CalendarDay> mDisabledDays; private HapticFeedbackController mHapticFeedbackController; private boolean mDelayAnimation = true; // Accessibility strings. private String mDayPickerDescription; private String mSelectDay; private String mYearPickerDescription; private String mSelectYear; private int mStyleResId; private int mSelectedColor; private int mUnselectedColor; /** * The callback used to indicate the user is done filling in the date. */ public interface OnDateSetListener { /** * @param dialog The view associated with this listener. * @param year The year that was set. * @param monthOfYear The month that was set (0-11) for compatibility with {@link java.util.Calendar}. * @param dayOfMonth The day of the month that was set. */ void onDateSet(CalendarDatePickerDialogFragment dialog, int year, int monthOfYear, int dayOfMonth); } /** * The callback used to notify other date picker components of a change in selected date. */ public interface OnDateChangedListener { public void onDateChanged(); } public CalendarDatePickerDialogFragment() { // Empty constructor required for dialog fragment. mStyleResId = R.style.BetterPickersRadialTimePickerDialog_PrimaryColor; } public boolean isThemeDark() { return mStyleResId == R.style.BetterPickersRadialTimePickerDialog_Dark; } public CalendarDatePickerDialogFragment setThemeDark() { mStyleResId = R.style.BetterPickersRadialTimePickerDialog_Dark; return this; } public CalendarDatePickerDialogFragment setThemeLight() { mStyleResId = R.style.BetterPickersRadialTimePickerDialog_Light; return this; } public CalendarDatePickerDialogFragment setThemeCustom(int styleResId) { this.mStyleResId = styleResId; return this; } public CalendarDatePickerDialogFragment setPreselectedDate(int year, int monthOfYear, int dayOfMonth) { mCalendar.set(Calendar.YEAR, year); mCalendar.set(Calendar.MONTH, monthOfYear); mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); return this; } public CalendarDatePickerDialogFragment setDoneText(String text) { mDoneText = text; return this; } public CalendarDatePickerDialogFragment setCancelText(String text) { mCancelText = text; return this; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Activity activity = getActivity(); activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); if (savedInstanceState != null) { mCalendar.set(Calendar.YEAR, savedInstanceState.getInt(KEY_SELECTED_YEAR)); mCalendar.set(Calendar.MONTH, savedInstanceState.getInt(KEY_SELECTED_MONTH)); mCalendar.set(Calendar.DAY_OF_MONTH, savedInstanceState.getInt(KEY_SELECTED_DAY)); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(KEY_SELECTED_YEAR, mCalendar.get(Calendar.YEAR)); outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH)); outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH)); outState.putInt(KEY_WEEK_START, mWeekStart); outState.putLong(KEY_DATE_START, mMinDate.getDateInMillis()); outState.putLong(KEY_DATE_END, mMaxDate.getDateInMillis()); outState.putInt(KEY_CURRENT_VIEW, mCurrentView); outState.putInt(KEY_THEME, mStyleResId); int listPosition = -1; if (mCurrentView == MONTH_AND_DAY_VIEW) { listPosition = mDayPickerView.getMostVisiblePosition(); } else if (mCurrentView == YEAR_VIEW) { listPosition = mYearPickerView.getFirstVisiblePosition(); outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset()); } outState.putInt(KEY_LIST_POSITION, listPosition); outState.putSparseParcelableArray(KEY_DISABLED_DAYS, mDisabledDays); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(TAG, "onCreateView: "); if (getShowsDialog()) { getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); } View view = inflater.inflate(R.layout.calendar_date_picker_dialog, container, false); mSelectedDateLayout = (LinearLayout) view.findViewById(R.id.day_picker_selected_date_layout); mDayOfWeekView = (TextView) view.findViewById(R.id.date_picker_header); mMonthAndDayView = (LinearLayout) view.findViewById(R.id.date_picker_month_and_day); mMonthAndDayView.setOnClickListener(this); mSelectedMonthTextView = (TextView) view.findViewById(R.id.date_picker_month); mSelectedDayTextView = (TextView) view.findViewById(R.id.date_picker_day); mYearView = (TextView) view.findViewById(R.id.date_picker_year); mYearView.setOnClickListener(this); int listPosition = -1; int listPositionOffset = 0; int currentView = MONTH_AND_DAY_VIEW; if (savedInstanceState != null) { mWeekStart = savedInstanceState.getInt(KEY_WEEK_START); mMinDate = new CalendarDay(savedInstanceState.getLong(KEY_DATE_START)); mMaxDate = new CalendarDay(savedInstanceState.getLong(KEY_DATE_END)); currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW); listPosition = savedInstanceState.getInt(KEY_LIST_POSITION); listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET); mStyleResId = savedInstanceState.getInt(KEY_THEME); mDisabledDays = savedInstanceState.getSparseParcelableArray(KEY_DISABLED_DAYS); } final Activity activity = getActivity(); mDayPickerView = new SimpleDayPickerView(activity, this); mYearPickerView = new YearPickerView(activity, this); Resources res = getResources(); TypedArray themeColors = getActivity().obtainStyledAttributes(mStyleResId, R.styleable.BetterPickersDialogs); mDayPickerDescription = res.getString(R.string.day_picker_description); mSelectDay = res.getString(R.string.select_day); mYearPickerDescription = res.getString(R.string.year_picker_description); mSelectYear = res.getString(R.string.select_year); int headerBackgroundColor = themeColors.getColor(R.styleable.BetterPickersDialogs_bpHeaderBackgroundColor, ContextCompat.getColor(getActivity(), R.color.bpWhite)); int preHeaderBackgroundColor = themeColors.getColor(R.styleable.BetterPickersDialogs_bpPreHeaderBackgroundColor, ContextCompat.getColor(getActivity(), R.color.bpWhite)); int bodyBgColor = themeColors.getColor(R.styleable.BetterPickersDialogs_bpBodyBackgroundColor, ContextCompat.getColor(getActivity(), R.color.bpWhite)); int buttonBgColor = themeColors.getColor(R.styleable.BetterPickersDialogs_bpButtonsBackgroundColor, ContextCompat.getColor(getActivity(), R.color.bpWhite)); int buttonTextColor = themeColors.getColor(R.styleable.BetterPickersDialogs_bpButtonsTextColor, ContextCompat.getColor(getActivity(), R.color.bpBlue)); mSelectedColor = themeColors.getColor(R.styleable.BetterPickersDialogs_bpHeaderSelectedTextColor, ContextCompat.getColor(getActivity(), R.color.bpWhite)); mUnselectedColor = themeColors.getColor(R.styleable.BetterPickersDialogs_bpHeaderUnselectedTextColor, ContextCompat.getColor(getActivity(), R.color.radial_gray_light)); mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.animator); mAnimator.addView(mDayPickerView); mAnimator.addView(mYearPickerView); mAnimator.setDateMillis(mCalendar.getTimeInMillis()); // TODO: Replace with animation decided upon by the design team. Animation animation = new AlphaAnimation(0.0f, 1.0f); animation.setDuration(ANIMATION_DURATION); mAnimator.setInAnimation(animation); // TODO: Replace with animation decided upon by the design team. Animation animation2 = new AlphaAnimation(1.0f, 0.0f); animation2.setDuration(ANIMATION_DURATION); mAnimator.setOutAnimation(animation2); Button doneButton = (Button) view.findViewById(R.id.done_button); if (mDoneText != null) { doneButton.setText(mDoneText); } doneButton.setTextColor(buttonTextColor); doneButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { tryVibrate(); if (mCallBack != null) { mCallBack.onDateSet(CalendarDatePickerDialogFragment.this, mCalendar.get(Calendar.YEAR), mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH)); } dismiss(); } }); Button cancelButton = (Button) view.findViewById(R.id.cancel_button); if (mCancelText != null) { cancelButton.setText(mCancelText); } cancelButton.setTextColor(buttonTextColor); cancelButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { tryVibrate(); dismiss(); } }); view.findViewById(R.id.ok_cancel_buttons_layout).setBackgroundColor(buttonBgColor); updateDisplay(false); setCurrentView(currentView); if (listPosition != -1) { if (currentView == MONTH_AND_DAY_VIEW) { mDayPickerView.postSetSelection(listPosition); } else if (currentView == YEAR_VIEW) { mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset); } } mHapticFeedbackController = new HapticFeedbackController(activity); mDayPickerView.setTheme(themeColors); mYearPickerView.setTheme(themeColors); mSelectedDateLayout.setBackgroundColor(headerBackgroundColor); mYearView.setBackgroundColor(headerBackgroundColor); mMonthAndDayView.setBackgroundColor(headerBackgroundColor); if (mDayOfWeekView != null) { mDayOfWeekView.setBackgroundColor(preHeaderBackgroundColor); } view.setBackgroundColor(bodyBgColor); mYearPickerView.setBackgroundColor(bodyBgColor); mDayPickerView.setBackgroundColor(bodyBgColor); return view; } @Override public void onResume() { super.onResume(); mHapticFeedbackController.start(); } @Override public void onPause() { super.onPause(); mHapticFeedbackController.stop(); } private void setCurrentView(final int viewIndex) { long millis = mCalendar.getTimeInMillis(); switch (viewIndex) { case MONTH_AND_DAY_VIEW: ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f, 1.05f); if (mDelayAnimation) { pulseAnimator.setStartDelay(ANIMATION_DELAY); mDelayAnimation = false; } mDayPickerView.onDateChanged(); if (mCurrentView != viewIndex) { mMonthAndDayView.setSelected(true); mYearView.setSelected(false); mSelectedDayTextView.setTextColor(mSelectedColor); mSelectedMonthTextView.setTextColor(mSelectedColor); mYearView.setTextColor(mUnselectedColor); mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); mCurrentView = viewIndex; } pulseAnimator.start(); int flags = DateUtils.FORMAT_SHOW_DATE; String dayString = DateUtils.formatDateTime(getActivity(), millis, flags); mAnimator.setContentDescription(mDayPickerDescription + ": " + dayString); Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay); break; case YEAR_VIEW: pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f); if (mDelayAnimation) { pulseAnimator.setStartDelay(ANIMATION_DELAY); mDelayAnimation = false; } mYearPickerView.onDateChanged(); if (mCurrentView != viewIndex) { mMonthAndDayView.setSelected(false); mYearView.setSelected(true); mSelectedDayTextView.setTextColor(mUnselectedColor); mSelectedMonthTextView.setTextColor(mUnselectedColor); mYearView.setTextColor(mSelectedColor); mAnimator.setDisplayedChild(YEAR_VIEW); mCurrentView = viewIndex; } pulseAnimator.start(); CharSequence yearString = YEAR_FORMAT.format(millis); mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString); Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear); break; } } private void updateDisplay(boolean announce) { if (mDayOfWeekView != null) { mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.getDefault()).toUpperCase(Locale.getDefault())); } mSelectedMonthTextView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault()).toUpperCase(Locale.getDefault())); mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime())); mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime())); // Accessibility. long millis = mCalendar.getTimeInMillis(); mAnimator.setDateMillis(millis); int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR; String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags); mMonthAndDayView.setContentDescription(monthAndDayText); if (announce) { flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags); Utils.tryAccessibilityAnnounce(mAnimator, fullDateText); } } public CalendarDatePickerDialogFragment setFirstDayOfWeek(int startOfWeek) { if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) { throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and Calendar.SATURDAY"); } mWeekStart = startOfWeek; if (mDayPickerView != null) { mDayPickerView.onChange(); } return this; } /** * Sets the range of the dialog to be within the specific dates. Years and months outside of the * range are not shown, the days that are outside of the range are visible but cannot be selected. * * @param startDate The start date of the range (inclusive) * @param endDate The end date of the range (inclusive) * @throws IllegalArgumentException in case the end date is smaller than the start date */ public CalendarDatePickerDialogFragment setDateRange(@Nullable CalendarDay startDate, @Nullable CalendarDay endDate) { if (startDate == null) { mMinDate = DEFAULT_START_DATE; } else { mMinDate = startDate; } if (endDate == null) { mMaxDate = DEFAULT_END_DATE; } else { mMaxDate = endDate; } if (mMaxDate.compareTo(mMinDate) < 0) { throw new IllegalArgumentException("End date must be larger than start date"); } if (mDayPickerView != null) { mDayPickerView.onChange(); } return this; } /** * Sets a map of disabled days to declare as unselectable by the user. These days can be styled * in a different way than the currently selected day * * @param disabledDays sparse array of key date int (yyyyMMdd) to a calendar day object * @throws IllegalArgumentException in case the end date is smaller than the start date */ public CalendarDatePickerDialogFragment setDisabledDays(@NonNull SparseArray<CalendarDay> disabledDays) { mDisabledDays = disabledDays; if (mDayPickerView != null) { mDayPickerView.onChange(); } return this; } public CalendarDatePickerDialogFragment setOnDateSetListener(OnDateSetListener listener) { mCallBack = listener; return this; } public CalendarDatePickerDialogFragment setOnDismissListener(OnDialogDismissListener ondialogdismisslistener) { mDimissCallback = ondialogdismisslistener; return this; } // 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 private void adjustDayInMonthIfNeeded(int month, int year) { int day = mCalendar.get(Calendar.DAY_OF_MONTH); int daysInMonth = Utils.getDaysInMonth(month, year); if (day > daysInMonth) { mCalendar.set(Calendar.DAY_OF_MONTH, daysInMonth); } } @Override public void onDismiss(DialogInterface dialoginterface) { super.onDismiss(dialoginterface); if (mDimissCallback != null) { mDimissCallback.onDialogDismiss(dialoginterface); } } @Override public void onClick(View v) { tryVibrate(); if (v.getId() == R.id.date_picker_year) { setCurrentView(YEAR_VIEW); } else if (v.getId() == R.id.date_picker_month_and_day) { setCurrentView(MONTH_AND_DAY_VIEW); } } @Override public void onYearSelected(int year) { adjustDayInMonthIfNeeded(mCalendar.get(Calendar.MONTH), year); mCalendar.set(Calendar.YEAR, year); updatePickers(); setCurrentView(MONTH_AND_DAY_VIEW); updateDisplay(true); } @Override public void onDayOfMonthSelected(int year, int month, int day) { mCalendar.set(Calendar.YEAR, year); mCalendar.set(Calendar.MONTH, month); mCalendar.set(Calendar.DAY_OF_MONTH, day); updatePickers(); updateDisplay(true); } private void updatePickers() { Iterator<OnDateChangedListener> iterator = mListeners.iterator(); while (iterator.hasNext()) { iterator.next().onDateChanged(); } } @Override public CalendarDay getSelectedDay() { return new CalendarDay(mCalendar); } @Override public CalendarDay getMinDate() { return mMinDate; } @Override public CalendarDay getMaxDate() { return mMaxDate; } @Override public SparseArray<CalendarDay> getDisabledDays() { return mDisabledDays; } @Override public int getFirstDayOfWeek() { return mWeekStart; } @Override public void registerOnDateChangedListener(OnDateChangedListener listener) { mListeners.add(listener); } @Override public void unregisterOnDateChangedListener(OnDateChangedListener listener) { mListeners.remove(listener); } @Override public void tryVibrate() { mHapticFeedbackController.tryVibrate(); } }