Java tutorial
/* * 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.philliphsu.bottomsheetpickers.date; import android.app.Activity; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.v4.content.ContextCompat; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.widget.LinearLayout; import android.widget.TextView; import com.philliphsu.bottomsheetpickers.HapticFeedbackController; import com.philliphsu.bottomsheetpickers.R; import com.philliphsu.bottomsheetpickers.Utils; import com.philliphsu.bottomsheetpickers.date.MonthAdapter.CalendarDay; 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 BottomSheetDatePickerDialog extends DatePickerDialog implements OnClickListener, DatePickerController { private static final String TAG = "BottomSheetDatePickerDialog"; 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_YEAR_START = "year_start"; private static final String KEY_YEAR_END = "year_end"; private static final String KEY_CURRENT_VIEW = "current_view"; private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset"; 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 ANIMATION_DELAY = 500; private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault()); private static SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("dd", Locale.getDefault()); private final Calendar mCalendar = Calendar.getInstance(); private OnDateSetListener mCallBack; private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>(); private AccessibleDateAnimator mAnimator; private TextView mDayOfWeekView; private LinearLayout mMonthDayYearView; private TextView mFirstTextView; private TextView mSecondTextView; private DayPickerView mDayPickerView; private YearPickerView mYearPickerView; private FloatingActionButton mDoneButton; private int mCurrentView = UNINITIALIZED; private int mWeekStart = mCalendar.getFirstDayOfWeek(); private int mMinYear = DEFAULT_START_YEAR; private int mMaxYear = DEFAULT_END_YEAR; private Calendar mMinDate; private Calendar mMaxDate; private HapticFeedbackController mHapticFeedbackController; private boolean mDelayAnimation = true; // Accessibility strings. private String mDayPickerDescription; private String mSelectDay; private String mYearPickerDescription; private String mSelectYear; // Relative positions of (MD) and Y in the locale's date formatting style. // TODO: Verify that these don't need to be saved in state when rotating. // Because in the new instance, onCreateView() will get called again and // so will determinteLocale_MD_Y_Indices(). private int mLocaleMonthDayIndex; private int mLocaleYearIndex; public BottomSheetDatePickerDialog() { // Empty constructor required for dialog fragment. } /** * @param callBack How the parent is notified that the date is set. * @param year The initial year of the dialog. * @param monthOfYear The initial month of the dialog. * @param dayOfMonth The initial day of the dialog. */ public static BottomSheetDatePickerDialog newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { BottomSheetDatePickerDialog ret = new BottomSheetDatePickerDialog(); ret.initialize(callBack, year, monthOfYear, dayOfMonth); return ret; } public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { mCallBack = callBack; mCalendar.set(Calendar.YEAR, year); mCalendar.set(Calendar.MONTH, monthOfYear); mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Activity activity = getActivity(); activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); setCancelable(false); 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.putInt(KEY_YEAR_START, mMinYear); outState.putInt(KEY_YEAR_END, mMaxYear); outState.putInt(KEY_CURRENT_VIEW, mCurrentView); 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); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = super.onCreateView(inflater, container, savedInstanceState); mDayOfWeekView = (TextView) view.findViewById(R.id.date_picker_header); mMonthDayYearView = (LinearLayout) view.findViewById(R.id.date_picker_month_day_year); mFirstTextView = (TextView) view.findViewById(R.id.date_picker_first_textview); mFirstTextView.setOnClickListener(this); mSecondTextView = (TextView) view.findViewById(R.id.date_picker_second_textview); mSecondTextView.setOnClickListener(this); int listPosition = -1; int listPositionOffset = 0; int currentView = MONTH_AND_DAY_VIEW; if (savedInstanceState != null) { mWeekStart = savedInstanceState.getInt(KEY_WEEK_START); mMinYear = savedInstanceState.getInt(KEY_YEAR_START); mMaxYear = savedInstanceState.getInt(KEY_YEAR_END); currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW); listPosition = savedInstanceState.getInt(KEY_LIST_POSITION); listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET); } final Activity activity = getActivity(); mDayPickerView = new SimpleDayPickerView(activity, this, mThemeDark); mYearPickerView = new YearPickerView(activity, this); mYearPickerView.setTheme(activity, mThemeDark); Resources res = getResources(); 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); 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); mDoneButton = (FloatingActionButton) view.findViewById(R.id.done); mDoneButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { tryVibrate(); if (mCallBack != null) { mCallBack.onDateSet(BottomSheetDatePickerDialog.this, mCalendar.get(Calendar.YEAR), mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH)); } dismiss(); } }); // Theme-specific configurations. if (mThemeDark) { // This is so the margin gets colored as well. view.setBackgroundColor(mDarkGray); mAnimator.setBackgroundColor(mDarkGray); } else { // Setup FAB icon color. Drawable fabIcon = mDoneButton.getDrawable(); Utils.setTint(fabIcon, mAccentColor); mDoneButton.setImageDrawable(fabIcon); } // Configurations for both themes. View selectedDateLayout = view.findViewById(R.id.day_picker_selected_date_layout); selectedDateLayout.setBackgroundColor(mThemeDark ? mLightGray : mAccentColor); int white = ContextCompat.getColor(getActivity(), android.R.color.white); mDoneButton.setBackgroundTintList(ColorStateList.valueOf(mThemeDark ? mAccentColor : white)); determineLocale_MD_Y_Indices(); 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); 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: mDayPickerView.onDateChanged(); if (mCurrentView != viewIndex) { updateHeaderSelectedView(MONTH_AND_DAY_VIEW); mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); mCurrentView = viewIndex; } 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: mYearPickerView.onDateChanged(); if (mCurrentView != viewIndex) { updateHeaderSelectedView(YEAR_VIEW); mAnimator.setDisplayedChild(YEAR_VIEW); mCurrentView = viewIndex; } CharSequence yearString = YEAR_FORMAT.format(millis); mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString); Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear); break; } } private void updateHeaderSelectedView(final int viewIndex) { switch (viewIndex) { case MONTH_AND_DAY_VIEW: mFirstTextView.setSelected(mLocaleMonthDayIndex == 0); mSecondTextView.setSelected(mLocaleMonthDayIndex != 0); break; case YEAR_VIEW: mFirstTextView.setSelected(mLocaleYearIndex == 0); mSecondTextView.setSelected(mLocaleYearIndex != 0); break; } } /** * Determine the relative positions of (MD) and Y according to the formatting style * of the current locale. */ private void determineLocale_MD_Y_Indices() { String formattedDate = formatMonthDayYear(mCalendar); // Get the (MD) and Y parts of the formatted date in the current locale, // so that we can compare their relative positions. // // You may be wondering why we need this method at all. // "Just split() the formattedDate string around the year delimiter // to get the two parts in an array already positioned correctly! // Then setText() on mFirstTextView and mSecondTextView with the contents of that array!" // That is harder than it sounds. // Different locales use different year delimiters, and some don't use one at all. // For example, a fully formatted date in the French locale is "30 juin 2009". String monthAndDay = formatMonthAndDay(mCalendar); String year = extractYearFromFormattedDate(formattedDate, monthAndDay); // All locales format the M and D together; which comes // first is not a necessary consideration for the comparison. if (formattedDate.indexOf(monthAndDay) < formattedDate.indexOf(year/*not null*/)) { mLocaleMonthDayIndex = 0; mLocaleYearIndex = 1; } else { mLocaleYearIndex = 0; mLocaleMonthDayIndex = 1; } } private static String formatMonthDayYear(Calendar calendar) { int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_YEAR; return formatDateTime(calendar, flags); } private static String formatMonthAndDay(Calendar calendar) { int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_NO_YEAR; return formatDateTime(calendar, flags); } private String extractYearFromFormattedDate(String formattedDate, String monthAndDay) { String[] parts = formattedDate.split(monthAndDay); for (String part : parts) { // If the locale's date format is (MD)Y, then split(MD) = {"", Y}. // If it is Y(MD), then split(MD) = {Y}. "Trailing empty strings are // [...] not included in the resulting array." if (!part.isEmpty()) { return part; } } // We will NEVER reach here, as long as the parameters are valid strings. // We don't want this because it is not localized. return YEAR_FORMAT.format(mCalendar.getTime()); } private void updateDisplay(boolean announce) { if (mDayOfWeekView != null) { mDayOfWeekView .setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, Locale.getDefault())); } String monthAndDay = formatMonthAndDay(mCalendar); String year = extractYearFromFormattedDate(formatMonthDayYear(mCalendar), monthAndDay); mFirstTextView.setText(mLocaleMonthDayIndex == 0 ? monthAndDay : year); mSecondTextView.setText(mLocaleMonthDayIndex == 0 ? year : monthAndDay); // 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); mMonthDayYearView.setContentDescription(monthAndDayText); if (announce) { flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags); Utils.tryAccessibilityAnnounce(mAnimator, fullDateText); } } private static String formatDateTime(Calendar calendar, int flags) { return DateUtils.formatDateTime(null, calendar.getTimeInMillis(), flags); } public void 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(); } } public void setYearRange(int startYear, int endYear) { if (endYear <= startYear) { throw new IllegalArgumentException("Year end must be larger than year start"); } mMinYear = startYear; mMaxYear = endYear; if (mDayPickerView != null) { mDayPickerView.onChange(); } } /** * Sets the minimal date supported by this DatePicker. Dates before (but not including) the * specified date will be disallowed from being selected. * @param calendar a Calendar object set to the year, month, day desired as the mindate. */ public void setMinDate(Calendar calendar) { mMinDate = calendar; if (mDayPickerView != null) { mDayPickerView.onChange(); } } /** * @return The minimal date supported by this DatePicker. Null if it has not been set. */ @Override public Calendar getMinDate() { return mMinDate; } /** * Sets the minimal date supported by this DatePicker. Dates after (but not including) the * specified date will be disallowed from being selected. * @param calendar a Calendar object set to the year, month, day desired as the maxdate. */ public void setMaxDate(Calendar calendar) { mMaxDate = calendar; if (mDayPickerView != null) { mDayPickerView.onChange(); } } /** * @return The maximal date supported by this DatePicker. Null if it has not been set. */ @Override public Calendar getMaxDate() { return mMaxDate; } public void setOnDateSetListener(OnDateSetListener listener) { mCallBack = listener; } // 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 onClick(View v) { tryVibrate(); if (v.getId() == R.id.date_picker_second_textview) { setCurrentView(mLocaleMonthDayIndex == 0 ? YEAR_VIEW : MONTH_AND_DAY_VIEW); } else if (v.getId() == R.id.date_picker_first_textview) { setCurrentView(mLocaleMonthDayIndex == 0 ? MONTH_AND_DAY_VIEW : YEAR_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 int getMinYear() { return mMinYear; } @Override public int getMaxYear() { return mMaxYear; } @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(); } @Override protected int contentLayout() { return R.layout.date_picker_dialog; } }