From bd9152f6ee156ee473f05f6f05f238605996fca4 Mon Sep 17 00:00:00 2001 From: Fabrice Di Meglio Date: Tue, 1 Oct 2013 11:21:31 -0700 Subject: [PATCH] Update DatePicker widget and its related dialog - the old DatePicker widget is still there for obvious layout compatibility reasons - add a new delegate implementation for having a new UI - use the new delegate only for the DatePickerDialog (which does not need to be the same) - added support for Theming and light/dark Themes - added support for RTL - added support for Accessibility - verified support for Keyboard - verified that CTS tests for DatePicker are passing (for both the legacy and the new widgets) Also added a new HapticFeedbackConstants.CALENDAR_DATE and its related code for enabling day selection vibration Change-Id: I256bd7c21edd8f3b910413ca15ce26d3a5ef7d9c --- api/current.txt | 25 + core/java/android/app/DatePickerDialog.java | 74 +- .../android/view/HapticFeedbackConstants.java | 6 + core/java/android/widget/DatePicker.java | 417 +++++++- .../android/widget/DatePickerController.java | 56 + .../android/widget/DatePickerDelegate.java | 979 ++++++++++++++++++ core/java/android/widget/DayPickerView.java | 506 +++++++++ .../android/widget/OnDateChangedListener.java | 27 + .../android/widget/SimpleMonthAdapter.java | 178 ++++ core/java/android/widget/SimpleMonthView.java | 720 +++++++++++++ .../widget/TextViewWithCircularIndicator.java | 114 ++ .../android/widget/TimePickerDelegate.java | 4 +- core/java/android/widget/YearPickerView.java | 200 ++++ .../widget/AccessibleDateAnimator.java | 56 + .../internal/widget/ExploreByTouchHelper.java | 719 +++++++++++++ .../color/date_picker_calendar_holo_dark.xml | 25 + .../color/date_picker_calendar_holo_light.xml | 25 + .../date_picker_calendar_material_dark.xml | 25 + .../date_picker_calendar_material_light.xml | 25 + .../color/date_picker_selector_holo_dark.xml | 25 + .../color/date_picker_selector_holo_light.xml | 25 + .../date_picker_selector_material_dark.xml | 25 + .../date_picker_selector_material_light.xml | 25 + .../date_picker_year_selector_holo_dark.xml | 23 + .../date_picker_year_selector_holo_light.xml | 23 + ...ate_picker_year_selector_material_dark.xml | 23 + ...te_picker_year_selector_material_light.xml | 23 + core/res/res/layout-land/date_picker_holo.xml | 46 + .../res/layout-sw600dp/date_picker_holo.xml | 38 + .../res/layout/date_picker_done_button.xml | 33 + .../res/layout/date_picker_header_view.xml | 26 + core/res/res/layout/date_picker_holo.xml | 101 +- ...date_picker.xml => date_picker_legacy.xml} | 0 .../res/layout/date_picker_legacy_holo.xml | 92 ++ .../res/layout/date_picker_selected_date.xml | 65 ++ .../res/layout/date_picker_view_animator.xml | 21 + core/res/res/layout/year_label_text_view.xml | 23 + core/res/res/values/attrs.xml | 49 + core/res/res/values/colors_holo.xml | 28 + core/res/res/values/colors_material.xml | 28 + core/res/res/values/config.xml | 6 + core/res/res/values/dimens.xml | 18 + core/res/res/values/public.xml | 10 + core/res/res/values/styles.xml | 25 +- .../res/res/values/styles_device_defaults.xml | 25 + core/res/res/values/styles_holo.xml | 86 +- core/res/res/values/styles_material.xml | 82 +- core/res/res/values/symbols.xml | 60 +- core/res/res/values/themes.xml | 37 +- .../res/res/values/themes_device_defaults.xml | 32 + core/res/res/values/themes_holo.xml | 70 ++ core/res/res/values/themes_material.xml | 75 ++ .../policy/impl/PhoneWindowManager.java | 8 + 53 files changed, 5327 insertions(+), 130 deletions(-) create mode 100644 core/java/android/widget/DatePickerController.java create mode 100644 core/java/android/widget/DatePickerDelegate.java create mode 100644 core/java/android/widget/DayPickerView.java create mode 100644 core/java/android/widget/OnDateChangedListener.java create mode 100644 core/java/android/widget/SimpleMonthAdapter.java create mode 100644 core/java/android/widget/SimpleMonthView.java create mode 100644 core/java/android/widget/TextViewWithCircularIndicator.java create mode 100644 core/java/android/widget/YearPickerView.java create mode 100644 core/java/com/android/internal/widget/AccessibleDateAnimator.java create mode 100644 core/java/com/android/internal/widget/ExploreByTouchHelper.java create mode 100644 core/res/res/color/date_picker_calendar_holo_dark.xml create mode 100644 core/res/res/color/date_picker_calendar_holo_light.xml create mode 100644 core/res/res/color/date_picker_calendar_material_dark.xml create mode 100644 core/res/res/color/date_picker_calendar_material_light.xml create mode 100644 core/res/res/color/date_picker_selector_holo_dark.xml create mode 100644 core/res/res/color/date_picker_selector_holo_light.xml create mode 100644 core/res/res/color/date_picker_selector_material_dark.xml create mode 100644 core/res/res/color/date_picker_selector_material_light.xml create mode 100644 core/res/res/color/date_picker_year_selector_holo_dark.xml create mode 100644 core/res/res/color/date_picker_year_selector_holo_light.xml create mode 100644 core/res/res/color/date_picker_year_selector_material_dark.xml create mode 100644 core/res/res/color/date_picker_year_selector_material_light.xml create mode 100644 core/res/res/layout-land/date_picker_holo.xml create mode 100644 core/res/res/layout-sw600dp/date_picker_holo.xml create mode 100644 core/res/res/layout/date_picker_done_button.xml create mode 100644 core/res/res/layout/date_picker_header_view.xml rename core/res/res/layout/{date_picker.xml => date_picker_legacy.xml} (100%) create mode 100644 core/res/res/layout/date_picker_legacy_holo.xml create mode 100644 core/res/res/layout/date_picker_selected_date.xml create mode 100644 core/res/res/layout/date_picker_view_animator.xml create mode 100644 core/res/res/layout/year_label_text_view.xml diff --git a/api/current.txt b/api/current.txt index 111869d16ea11..c565897cd9bf4 100644 --- a/api/current.txt +++ b/api/current.txt @@ -359,6 +359,7 @@ package android { field public static final int buttonTint = 16843889; // 0x1010471 field public static final int buttonTintMode = 16843890; // 0x1010472 field public static final int cacheColorHint = 16843009; // 0x1010101 + field public static final int calendarTextColor = 16843931; // 0x101049b field public static final int calendarViewShown = 16843596; // 0x101034c field public static final int calendarViewStyle = 16843613; // 0x101035d field public static final int canRequestEnhancedWebAccessibility = 16843736; // 0x10103d8 @@ -447,6 +448,14 @@ package android { field public static final int dashWidth = 16843174; // 0x10101a6 field public static final int data = 16842798; // 0x101002e field public static final int datePickerStyle = 16843612; // 0x101035c + field public static final int dateSelectorBackgroundColor = 16843925; // 0x1010495 + field public static final int dateSelectorDayOfMonthTextAppearance = 16843927; // 0x1010497 + field public static final int dateSelectorDayOfWeekBackgroundColor = 16843923; // 0x1010493 + field public static final int dateSelectorDayOfWeekTextAppearance = 16843924; // 0x1010494 + field public static final int dateSelectorMonthTextAppearance = 16843926; // 0x1010496 + field public static final int dateSelectorYearListItemTextAppearance = 16843929; // 0x1010499 + field public static final int dateSelectorYearListSelectedCircleColor = 16843930; // 0x101049a + field public static final int dateSelectorYearTextAppearance = 16843928; // 0x1010498 field public static final int dateTextAppearance = 16843593; // 0x1010349 field public static final int debuggable = 16842767; // 0x101000f field public static final int defaultValue = 16843245; // 0x10101ed @@ -37152,8 +37161,16 @@ package android.widget { ctor public DatePicker(android.content.Context, android.util.AttributeSet); ctor public DatePicker(android.content.Context, android.util.AttributeSet, int); ctor public DatePicker(android.content.Context, android.util.AttributeSet, int, int); + method public android.content.res.ColorStateList getCalendarTextColor(); method public android.widget.CalendarView getCalendarView(); method public boolean getCalendarViewShown(); + method public int getDateSelectorBackgroundColor(); + method public int getDateSelectorDayOfMonthTextAppearance(); + method public int getDateSelectorDayOfWeekBackgroundColor(); + method public int getDateSelectorDayOfWeekTextAppearance(); + method public int getDateSelectorMonthTextAppearance(); + method public int getDateSelectorYearListItemTextAppearance(); + method public int getDateSelectorYearTextAppearance(); method public int getDayOfMonth(); method public long getMaxDate(); method public long getMinDate(); @@ -37161,7 +37178,15 @@ package android.widget { method public boolean getSpinnersShown(); method public int getYear(); method public void init(int, int, int, android.widget.DatePicker.OnDateChangedListener); + method public void setCalendarTextColor(android.content.res.ColorStateList); method public void setCalendarViewShown(boolean); + method public void setDateSelectorBackgroundColor(int); + method public void setDateSelectorDayOfMonthTextAppearance(int); + method public void setDateSelectorDayOfWeekBackgroundColor(int); + method public void setDateSelectorDayOfWeekTextAppearance(int); + method public void setDateSelectorMonthTextAppearance(int); + method public void setDateSelectorYearListItemTextAppearance(int); + method public void setDateSelectorYearTextAppearance(int); method public void setMaxDate(long); method public void setMinDate(long); method public void setSpinnersShown(boolean); diff --git a/core/java/android/app/DatePickerDialog.java b/core/java/android/app/DatePickerDialog.java index 26c2c30c27996..d7fb7073f56bc 100644 --- a/core/java/android/app/DatePickerDialog.java +++ b/core/java/android/app/DatePickerDialog.java @@ -21,6 +21,7 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; import android.text.format.DateUtils; +import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.widget.DatePicker; @@ -44,10 +45,11 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, private static final String DAY = "day"; private final DatePicker mDatePicker; - private final OnDateSetListener mCallBack; + private final OnDateSetListener mDateSetListener; private final Calendar mCalendar; private boolean mTitleNeedsUpdate = true; + private boolean mIsCanceled = false; /** * The callback used to indicate the user is done filling in the date. @@ -79,29 +81,36 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, this(context, 0, callBack, year, monthOfYear, dayOfMonth); } + static int resolveDialogTheme(Context context, int resid) { + if (resid == 0) { + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.datePickerDialogTheme, outValue, true); + return outValue.resourceId; + } else { + return resid; + } + } + /** * @param context The context the dialog is to run in. * @param theme the theme to apply to this dialog - * @param callBack How the parent is notified that the date is set. + * @param listener 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 DatePickerDialog(Context context, int theme, - OnDateSetListener callBack, + OnDateSetListener listener, int year, int monthOfYear, int dayOfMonth) { - super(context, theme); - - mCallBack = callBack; + super(context, resolveDialogTheme(context, theme)); + mDateSetListener = listener; mCalendar = Calendar.getInstance(); Context themeContext = getContext(); - setButton(BUTTON_POSITIVE, themeContext.getText(R.string.date_time_done), this); - setIcon(0); LayoutInflater inflater = (LayoutInflater) themeContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -109,20 +118,53 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, setView(view); setButtonPanelLayoutHint(LAYOUT_HINT_SIDE); mDatePicker = (DatePicker) view.findViewById(R.id.datePicker); + + // Initialize state + mDatePicker.setLegacyMode(false, null); + mDatePicker.setShowDoneButton(true); + mDatePicker.setDismissCallback(new DatePicker.DatePickerDismissCallback() { + @Override + public void dismiss(DatePicker view, boolean isCancel, int year, int month, int dayOfMonth) { + mIsCanceled = isCancel; + if (!isCancel) { + mDateSetListener.onDateSet(view, year, month, dayOfMonth); + } + DatePickerDialog.this.dismiss(); + } + }); mDatePicker.init(year, monthOfYear, dayOfMonth, this); - updateTitle(year, monthOfYear, dayOfMonth); } public void onClick(DialogInterface dialog, int which) { tryNotifyDateSet(); } + @Override + public void cancel() { + mIsCanceled = true; + super.cancel(); + } + + @Override + protected void onStop() { + tryNotifyDateSet(); + super.onStop(); + } + public void onDateChanged(DatePicker view, int year, int month, int day) { mDatePicker.init(year, month, day, this); updateTitle(year, month, day); } + private void tryNotifyDateSet() { + if (mDateSetListener != null && !mIsCanceled) { + mDatePicker.clearFocus(); + mDateSetListener.onDateSet(mDatePicker, mDatePicker.getYear(), + mDatePicker.getMonth(), mDatePicker.getDayOfMonth()); + } + } + /** * Gets the {@link DatePicker} contained in this dialog. * @@ -143,20 +185,6 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, mDatePicker.updateDate(year, monthOfYear, dayOfMonth); } - private void tryNotifyDateSet() { - if (mCallBack != null) { - mDatePicker.clearFocus(); - mCallBack.onDateSet(mDatePicker, mDatePicker.getYear(), - mDatePicker.getMonth(), mDatePicker.getDayOfMonth()); - } - } - - @Override - protected void onStop() { - tryNotifyDateSet(); - super.onStop(); - } - private void updateTitle(int year, int month, int day) { if (!mDatePicker.getCalendarViewShown()) { mCalendar.set(Calendar.YEAR, year); diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java index 26f47f926ed3c..cc090ad548d1e 100644 --- a/core/java/android/view/HapticFeedbackConstants.java +++ b/core/java/android/view/HapticFeedbackConstants.java @@ -45,6 +45,12 @@ public class HapticFeedbackConstants { */ public static final int CLOCK_TICK = 4; + /** + * The user has pressed either a day or month or year date of a Calendar. + * @hide + */ + public static final int CALENDAR_DATE = 5; + /** * This is a private constant. Feel free to renumber as desired. * @hide diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 2c1a77c9f9e65..8f49fb8517c1d 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -17,7 +17,9 @@ package android.widget; import android.annotation.Widget; +import android.app.UiModeManager; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.TypedArray; import android.os.Parcel; @@ -49,6 +51,8 @@ import java.util.TimeZone; import libcore.icu.ICU; +import static android.os.Build.VERSION_CODES.L; + /** * This class is a widget for selecting a date. The date can be selected by a * year, month, and day spinners or a {@link CalendarView}. The set of spinners @@ -70,6 +74,15 @@ import libcore.icu.ICU; * @attr ref android.R.styleable#DatePicker_minDate * @attr ref android.R.styleable#DatePicker_spinnersShown * @attr ref android.R.styleable#DatePicker_calendarViewShown + * @attr ref android.R.styleable#DatePicker_dateSelectorDayOfWeekBackgroundColor + * @attr ref android.R.styleable#DatePicker_dateSelectorDayOfWeekTextAppearance + * @attr ref android.R.styleable#DatePicker_dateSelectorBackgroundColor + * @attr ref android.R.styleable#DatePicker_dateSelectorMonthTextAppearance + * @attr ref android.R.styleable#DatePicker_dateSelectorDayOfMonthTextAppearance + * @attr ref android.R.styleable#DatePicker_dateSelectorYearTextAppearance + * @attr ref android.R.styleable#DatePicker_dateSelectorYearListItemTextAppearance + * @attr ref android.R.styleable#DatePicker_dateSelectorYearListSelectedCircleColor + * @attr ref android.R.styleable#DatePicker_calendarTextColor */ @Widget public class DatePicker extends FrameLayout { @@ -78,6 +91,10 @@ public class DatePicker extends FrameLayout { private DatePickerDelegate mDelegate; + private int mDefStyleAttr; + private int mDefStyleRes; + private Context mContext; + /** * The callback used to indicate the user changes\d the date. */ @@ -110,7 +127,65 @@ public class DatePicker extends FrameLayout { public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mDelegate = new LegacyDatePickerDelegate(this, context, attrs, defStyleAttr, defStyleRes); + mContext = context; + mDefStyleAttr = defStyleAttr; + mDefStyleRes = defStyleRes; + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, + mDefStyleAttr, mDefStyleRes); + + // Create the correct UI delegate. Default is the legacy one. + final boolean isLegacyMode = a.getBoolean(R.styleable.DatePicker_legacyMode, + isLegacyMode()); + + a.recycle(); + + setLegacyMode(isLegacyMode, attrs); + } + + private boolean isLegacyMode() { + UiModeManager uiModeManager = + (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { + return true; + } + final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; + return targetSdkVersion < L; + } + + private DatePickerDelegate createLegacyUIDelegate(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + return new LegacyDatePickerDelegate(this, context, attrs, defStyleAttr, defStyleRes); + } + + private DatePickerDelegate createNewUIDelegate(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + return new android.widget.DatePickerDelegate(this, context, attrs, defStyleAttr, + defStyleRes); + } + + /** + * @hide + */ + public void setLegacyMode(boolean isLegacyMode, AttributeSet attrs) { + removeAllViewsInLayout(); + mDelegate = isLegacyMode ? + createLegacyUIDelegate(mContext, attrs, mDefStyleAttr, mDefStyleRes) : + createNewUIDelegate(mContext, attrs, mDefStyleAttr, mDefStyleRes); + } + + /** + * @hide + */ + public void setShowDoneButton(boolean showDoneButton) { + mDelegate.setShowDoneButton(showDoneButton); + } + + /** + * @hide + */ + public void setDismissCallback(DatePickerDismissCallback callback) { + mDelegate.setDismissCallback(callback); } /** @@ -129,7 +204,7 @@ public class DatePicker extends FrameLayout { } /** - * Updates the current date. + * Update the current date. * * @param year The year. * @param month The month which is starting from zero. @@ -171,7 +246,7 @@ public class DatePicker extends FrameLayout { * @return The minimal supported date. */ public long getMinDate() { - return mDelegate.getMinDate(); + return mDelegate.getMinDate().getTimeInMillis(); } /** @@ -196,7 +271,7 @@ public class DatePicker extends FrameLayout { * @return The maximal supported date. */ public long getMaxDate() { - return mDelegate.getMaxDate(); + return mDelegate.getMaxDate().getTimeInMillis(); } /** @@ -300,6 +375,182 @@ public class DatePicker extends FrameLayout { mDelegate.setSpinnersShown(shown); } + /** + * Sets the background color for the date selector's day of week. + * + * @param color The background color. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorDayOfWeekBackgroundColor + */ + public void setDateSelectorDayOfWeekBackgroundColor(int color) { + mDelegate.setDateSelectorDayOfWeekBackgroundColor(color); + } + + /** + * Gets the background color for the date selector's day of week. + * + * @return The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorDayOfWeekBackgroundColor + */ + public int getDateSelectorDayOfWeekBackgroundColor() { + return mDelegate.getDateSelectorDayOfWeekBackgroundColor(); + } + + /** + * Sets the text appearance for the date selector's day of week. + * + * @param resId The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorDayOfWeekTextAppearance + */ + public void setDateSelectorDayOfWeekTextAppearance(int resId) { + mDelegate.setDateSelectorDayOfWeekTextAppearance(resId); + } + + /** + * Gets the text appearance for the date selector's day of week. + * + * @return The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorDayOfWeekTextAppearance + */ + public int getDateSelectorDayOfWeekTextAppearance() { + return mDelegate.getDateSelectorDayOfWeekTextAppearance(); + } + + /** + * Sets the background color for the date selector's. + * + * @param color The background color. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorBackgroundColor + */ + public void setDateSelectorBackgroundColor(int color) { + mDelegate.setDateSelectorBackgroundColor(color); + } + + /** + * Gets the background color for the date selector's. + * + * @return The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorBackgroundColor + */ + public int getDateSelectorBackgroundColor() { + return mDelegate.getDateSelectorBackgroundColor(); + } + + /** + * Sets the text appearance for the date selector's month. + * + * @param resId The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorMonthTextAppearance + */ + public void setDateSelectorMonthTextAppearance(int resId) { + mDelegate.setDateSelectorMonthTextAppearance(resId); + } + + /** + * Gets the text appearance for the date selector's month. + * + * @return The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorMonthTextAppearance + */ + public int getDateSelectorMonthTextAppearance() { + return mDelegate.getDateSelectorMonthTextAppearance(); + } + + /** + * Sets the text appearance for the date selector's day of month. + * + * @param resId The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorDayOfMonthTextAppearance + */ + public void setDateSelectorDayOfMonthTextAppearance(int resId) { + mDelegate.setDateSelectorDayOfMonthTextAppearance(resId); + } + + /** + * Gets the text appearance for the date selector's day of month. + * + * @return The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorDayOfMonthTextAppearance + */ + public int getDateSelectorDayOfMonthTextAppearance() { + return mDelegate.getDateSelectorDayOfMonthTextAppearance(); + } + + /** + * Sets the text appearance for the date selector's year. + * + * @param resId The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorYearTextAppearance + */ + public void setDateSelectorYearTextAppearance(int resId) { + mDelegate.setDateSelectorYearTextAppearance(resId); + } + + /** + * Gets the text appearance for the date selector's year. + * + * @return The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorYearTextAppearance + */ + public int getDateSelectorYearTextAppearance() { + return mDelegate.getDateSelectorYearTextAppearance(); + } + + /** + * Sets the text appearance for the year list item. + * + * @param resId The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorYearListItemTextAppearance + */ + public void setDateSelectorYearListItemTextAppearance(int resId) { + mDelegate.setDateSelectorYearListItemTextAppearance(resId); + } + + /** + * Gets the text appearance for the year list item. + * + * @return The text appearance resource id. + * + * @attr ref android.R.styleable#DatePicker_dateSelectorYearListItemTextAppearance + */ + public int getDateSelectorYearListItemTextAppearance() { + return mDelegate.getDateSelectorYearListItemTextAppearance(); + } + + /** + * Sets the text color state list for the calendar. + * + * @param colors The text color state list. + * + * @attr ref android.R.styleable#DatePicker_calendarTextColor + */ + public void setCalendarTextColor(ColorStateList colors) { + mDelegate.setCalendarTextColor(colors); + } + + /** + * Gets the text color state list for the calendar. + * + * @return The text color state list for the calendar. + * + * @attr ref android.R.styleable#DatePicker_calendarTextColor + */ + public ColorStateList getCalendarTextColor() { + return mDelegate.getCalendarTextColors(); + } + // Override so we are in complete control of save / restore for this widget. @Override protected void dispatchRestoreInstanceState(SparseArray container) { @@ -323,6 +574,8 @@ public class DatePicker extends FrameLayout { * A delegate interface that defined the public API of the DatePicker. Allows different * DatePicker implementations. This would need to be implemented by the DatePicker delegates * for the real behavior. + * + * @hide */ interface DatePickerDelegate { void init(int year, int monthOfYear, int dayOfMonth, @@ -335,15 +588,42 @@ public class DatePicker extends FrameLayout { int getDayOfMonth(); void setMinDate(long minDate); - long getMinDate(); + Calendar getMinDate(); void setMaxDate(long maxDate); - long getMaxDate(); + Calendar getMaxDate(); void setEnabled(boolean enabled); boolean isEnabled(); - CalendarView getCalendarView (); + void setDateSelectorDayOfWeekBackgroundColor(int color); + int getDateSelectorDayOfWeekBackgroundColor(); + + void setDateSelectorDayOfWeekTextAppearance(int resId); + int getDateSelectorDayOfWeekTextAppearance(); + + void setDateSelectorBackgroundColor(int color); + int getDateSelectorBackgroundColor(); + + void setDateSelectorMonthTextAppearance(int resId); + int getDateSelectorMonthTextAppearance(); + + void setDateSelectorDayOfMonthTextAppearance(int resId); + int getDateSelectorDayOfMonthTextAppearance(); + + void setDateSelectorYearTextAppearance(int resId); + int getDateSelectorYearTextAppearance(); + + void setDateSelectorYearListItemTextAppearance(int resId); + int getDateSelectorYearListItemTextAppearance(); + + void setDateSelectorYearListSelectedCircleColor(int color); + int getDateSelectorYearListSelectedCircleColor(); + + void setCalendarTextColor(ColorStateList colors); + ColorStateList getCalendarTextColors(); + + CalendarView getCalendarView(); void setCalendarViewShown(boolean shown); boolean getCalendarViewShown(); @@ -351,6 +631,9 @@ public class DatePicker extends FrameLayout { void setSpinnersShown(boolean shown); boolean getSpinnersShown(); + void setShowDoneButton(boolean showDoneButton); + void setDismissCallback(DatePickerDismissCallback callback); + void onConfigurationChanged(Configuration newConfig); void dispatchRestoreInstanceState(SparseArray container); @@ -366,7 +649,7 @@ public class DatePicker extends FrameLayout { /** * An abstract class which can be used as a start for DatePicker implementations */ - abstract static class AbstractTimePickerDelegate implements DatePickerDelegate { + abstract static class AbstractDatePickerDelegate implements DatePickerDelegate { // The delegator protected DatePicker mDelegator; @@ -379,7 +662,7 @@ public class DatePicker extends FrameLayout { // Callbacks protected OnDateChangedListener mOnDateChangedListener; - public AbstractTimePickerDelegate(DatePicker delegator, Context context) { + public AbstractDatePickerDelegate(DatePicker delegator, Context context) { mDelegator = delegator; mContext = context; @@ -395,10 +678,19 @@ public class DatePicker extends FrameLayout { } } + /** + * A callback interface for dismissing the DatePicker when included into a Dialog + * + * @hide + */ + public static interface DatePickerDismissCallback { + void dismiss(DatePicker view, boolean isCancel, int year, int month, int dayOfMonth); + } + /** * A delegate implementing the basic DatePicker */ - private static class LegacyDatePickerDelegate extends AbstractTimePickerDelegate { + private static class LegacyDatePickerDelegate extends AbstractDatePickerDelegate { private static final String DATE_FORMAT = "MM/dd/yyyy"; @@ -466,7 +758,7 @@ public class DatePicker extends FrameLayout { String minDate = attributesArray.getString(R.styleable.DatePicker_minDate); String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate); int layoutResourceId = attributesArray.getResourceId( - R.styleable.DatePicker_internalLayout, R.layout.date_picker); + R.styleable.DatePicker_legacyLayout, R.layout.date_picker_legacy); attributesArray.recycle(); LayoutInflater inflater = (LayoutInflater) context @@ -643,8 +935,10 @@ public class DatePicker extends FrameLayout { } @Override - public long getMinDate() { - return mCalendarView.getMinDate(); + public Calendar getMinDate() { + final Calendar minDate = Calendar.getInstance(); + minDate.setTimeInMillis(mCalendarView.getMinDate()); + return minDate; } @Override @@ -664,8 +958,10 @@ public class DatePicker extends FrameLayout { } @Override - public long getMaxDate() { - return mCalendarView.getMaxDate(); + public Calendar getMaxDate() { + final Calendar maxDate = Calendar.getInstance(); + maxDate.setTimeInMillis(mCalendarView.getMaxDate()); + return maxDate; } @Override @@ -682,6 +978,87 @@ public class DatePicker extends FrameLayout { return mIsEnabled; } + @Override + public void setDateSelectorDayOfWeekBackgroundColor(int color) { + } + + @Override + public int getDateSelectorDayOfWeekBackgroundColor() { + return 0; + } + + @Override + public void setDateSelectorDayOfWeekTextAppearance(int resId) { + } + + @Override + public int getDateSelectorDayOfWeekTextAppearance() { + return 0; + } + + @Override + public void setDateSelectorBackgroundColor(int color) { + } + + @Override + public int getDateSelectorBackgroundColor() { + return 0; + } + + @Override + public void setDateSelectorMonthTextAppearance(int resId) { + } + + @Override + public int getDateSelectorMonthTextAppearance() { + return 0; + } + + @Override + public void setDateSelectorDayOfMonthTextAppearance(int resId) { + } + + @Override + public int getDateSelectorDayOfMonthTextAppearance() { + return 0; + } + + @Override + public void setDateSelectorYearTextAppearance(int resId) { + } + + @Override + public int getDateSelectorYearTextAppearance() { + return 0; + } + + @Override + public void setDateSelectorYearListItemTextAppearance(int resId) { + } + + @Override + public int getDateSelectorYearListItemTextAppearance() { + return 0; + } + + @Override + public void setDateSelectorYearListSelectedCircleColor(int color) { + } + + @Override + public int getDateSelectorYearListSelectedCircleColor() { + return 0; + } + + @Override + public void setCalendarTextColor(ColorStateList colors) { + } + + @Override + public ColorStateList getCalendarTextColors() { + return ColorStateList.valueOf(0); + } + @Override public CalendarView getCalendarView() { return mCalendarView; @@ -707,6 +1084,16 @@ public class DatePicker extends FrameLayout { return mSpinners.isShown(); } + @Override + public void setShowDoneButton(boolean showDoneButton) { + // Nothing to do + } + + @Override + public void setDismissCallback(DatePickerDismissCallback callback) { + // Nothing to do + } + @Override public void onConfigurationChanged(Configuration newConfig) { setCurrentLocale(newConfig.locale); diff --git a/core/java/android/widget/DatePickerController.java b/core/java/android/widget/DatePickerController.java new file mode 100644 index 0000000000000..6a074da697269 --- /dev/null +++ b/core/java/android/widget/DatePickerController.java @@ -0,0 +1,56 @@ +/* + * 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 java.util.Calendar; + +/** + * Controller class to communicate among the various components of the date picker dialog. + * + * @hide + */ +interface DatePickerController { + + void onYearSelected(int year); + + void onDayOfMonthSelected(int year, int month, int day); + + void registerOnDateChangedListener(OnDateChangedListener listener); + + void unregisterOnDateChangedListener(OnDateChangedListener listener); + + Calendar getSelectedDay(); + + int getFirstDayOfWeek(); + + int getMinYear(); + int getMaxYear(); + + int getMinMonth(); + int getMaxMonth(); + + int getMinDay(); + int getMaxDay(); + + void setMinDate(long minDate); + Calendar getMinDate(); + + void setMaxDate(long maxDate); + Calendar getMaxDate(); + + void tryVibrate(); +} diff --git a/core/java/android/widget/DatePickerDelegate.java b/core/java/android/widget/DatePickerDelegate.java new file mode 100644 index 0000000000000..31044d497c4eb --- /dev/null +++ b/core/java/android/widget/DatePickerDelegate.java @@ -0,0 +1,979 @@ +/* + * 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.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +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.DateFormat; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; + +import com.android.internal.R; +import com.android.internal.widget.AccessibleDateAnimator; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; + +/** + * A delegate for picking up a date (day / month / year). + */ +class DatePickerDelegate extends DatePicker.AbstractDatePickerDelegate implements + View.OnClickListener, DatePickerController { + + 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 int DEFAULT_START_YEAR = 1900; + private static final int DEFAULT_END_YEAR = 2100; + + private static final int PULSE_ANIMATOR_DURATION = 544; + + private static final int ANIMATION_DURATION = 300; + private static final int ANIMATION_DELAY = 650; + + private static final int MONTH_INDEX = 0; + private static final int DAY_INDEX = 1; + private static final int YEAR_INDEX = 2; + + private SimpleDateFormat mYearFormat = new SimpleDateFormat("y", Locale.getDefault()); + private SimpleDateFormat mDayFormat = new SimpleDateFormat("d", Locale.getDefault()); + + private TextView mDayOfWeekView; + private LinearLayout mDateLayout; + private LinearLayout mMonthAndDayLayout; + private TextView mSelectedMonthTextView; + private TextView mSelectedDayTextView; + private TextView mSelectedYearView; + private DayPickerView mDayPickerView; + private YearPickerView mYearPickerView; + + private ViewGroup mLayoutButtons; + + private boolean mIsEnabled = true; + + // Accessibility strings. + private String mDayPickerDescription; + private String mSelectDay; + private String mYearPickerDescription; + private String mSelectYear; + + private AccessibleDateAnimator mAnimator; + + private DatePicker.OnDateChangedListener mDateChangedListener; + + private boolean mDelayAnimation = true; + + private int mCurrentView = UNINITIALIZED; + + private Calendar mCurrentDate; + private Calendar mTempDate; + private Calendar mMinDate; + private Calendar mMaxDate; + + // For showing the done button when in a Dialog + private Button mDoneButton; + private boolean mShowDoneButton; + private DatePicker.DatePickerDismissCallback mDismissCallback; + + private HashSet mListeners = new HashSet(); + + private int mDayOfWeekTextAppearanceResId; + private int mMonthTextAppearanceResId; + private int mDayOfMonthTextAppearanceResId; + private int mYearTextAppearanceResId; + + private int mYearListItemTextAppearanceResId; + + private int mDayOfWeekBackgroundColor; + private int mMonthAndDayBackgroundColor; + + private ColorStateList mCalendarTextColors; + + public DatePickerDelegate(DatePicker delegator, Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + + super(delegator, context); + + final Locale locale = Locale.getDefault(); + mMinDate = getCalendarForLocale(mMinDate, locale); + mMaxDate = getCalendarForLocale(mMaxDate, locale); + mTempDate = getCalendarForLocale(mMaxDate, locale); + + mCurrentDate = getCalendarForLocale(mCurrentDate, locale); + + mMinDate.set(DEFAULT_START_YEAR, 1, 1); + mMaxDate.set(DEFAULT_END_YEAR, 12, 31); + + // process style attributes + 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_holo); + + View mainView = inflater.inflate(layoutResourceId, null); + mDelegator.addView(mainView); + + mDayOfWeekView = (TextView) mainView.findViewById(R.id.date_picker_header); + mDateLayout = (LinearLayout) mainView.findViewById(R.id.day_picker_selector_layout); + mMonthAndDayLayout = (LinearLayout) mainView.findViewById( + R.id.date_picker_month_and_day_layout); + mMonthAndDayLayout.setOnClickListener(this); + mSelectedMonthTextView = (TextView) mainView.findViewById(R.id.date_picker_month); + mSelectedDayTextView = (TextView) mainView.findViewById(R.id.date_picker_day); + mSelectedYearView = (TextView) mainView.findViewById(R.id.date_picker_year); + mSelectedYearView.setOnClickListener(this); + + // Use Theme attributes if possible + mDayOfWeekTextAppearanceResId = a.getResourceId( + R.styleable.DatePicker_dateSelectorDayOfWeekTextAppearance, -1); + if (mDayOfWeekTextAppearanceResId != -1) { + mDayOfWeekView.setTextAppearance(context, mDayOfWeekTextAppearanceResId); + } + + mMonthTextAppearanceResId = a.getResourceId( + R.styleable.DatePicker_dateSelectorMonthTextAppearance, -1); + if (mMonthTextAppearanceResId != -1) { + mSelectedMonthTextView.setTextAppearance(context, mMonthTextAppearanceResId); + } + + mDayOfMonthTextAppearanceResId = a.getResourceId( + R.styleable.DatePicker_dateSelectorDayOfMonthTextAppearance, -1); + if (mDayOfMonthTextAppearanceResId != -1) { + mSelectedDayTextView.setTextAppearance(context, mDayOfMonthTextAppearanceResId); + } + + mYearTextAppearanceResId = a.getResourceId( + R.styleable.DatePicker_dateSelectorYearTextAppearance, -1); + if (mYearTextAppearanceResId != -1) { + mSelectedYearView.setTextAppearance(context, mYearTextAppearanceResId); + } + + Resources res = mDelegator.getResources(); + + mDayOfWeekBackgroundColor = a.getColor( + R.styleable.DatePicker_dateSelectorDayOfWeekBackgroundColor, + res.getColor( + R.color.datepicker_default_header_dayofweek_background_color_holo_light)); + mDayOfWeekView.setBackgroundColor(mDayOfWeekBackgroundColor); + + mMonthAndDayBackgroundColor = a.getColor(R.styleable.DatePicker_dateSelectorBackgroundColor, + res.getColor(R.color.datepicker_default_header_selector_background_holo_light)); + mMonthAndDayLayout.setBackgroundColor(mMonthAndDayBackgroundColor); + + mDayPickerView = new DayPickerView(mContext, this); + mYearPickerView = new YearPickerView(mContext); + mYearPickerView.init(this); + + ColorStateList colors = a.getColorStateList(R.styleable.DatePicker_calendarTextColor); + setCalendarTextColor(colors); + + 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) mainView.findViewById(R.id.animator); + mAnimator.addView(mDayPickerView); + mAnimator.addView(mYearPickerView); + mAnimator.setDateMillis(mCurrentDate.getTimeInMillis()); + Animation animation = new AlphaAnimation(0.0f, 1.0f); + animation.setDuration(ANIMATION_DURATION); + mAnimator.setInAnimation(animation); + Animation animation2 = new AlphaAnimation(1.0f, 0.0f); + animation2.setDuration(ANIMATION_DURATION); + mAnimator.setOutAnimation(animation2); + + mLayoutButtons = (ViewGroup) mainView.findViewById(R.id.layout_buttons); + mDoneButton = (Button) mainView.findViewById(R.id.done); + mDoneButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + tryVibrate(); + if (mDismissCallback != null) { + mDismissCallback.dismiss(mDelegator, false, mCurrentDate.get(Calendar.YEAR), + mCurrentDate.get(Calendar.MONTH), + mCurrentDate.get(Calendar.DAY_OF_MONTH)); + } + } + }); + + updateDisplay(false); + setCurrentView(MONTH_AND_DAY_VIEW); + } + + /** + * Gets a calendar for locale bootstrapped with the value of a given calendar. + * + * @param oldCalendar The old calendar. + * @param locale The locale. + */ + private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { + if (oldCalendar == null) { + return Calendar.getInstance(locale); + } else { + final long currentTimeMillis = oldCalendar.getTimeInMillis(); + Calendar newCalendar = Calendar.getInstance(locale); + newCalendar.setTimeInMillis(currentTimeMillis); + return newCalendar; + } + } + + /** + * Compute the array representing the order of Month / Day / Year views in their layout. + * Will be used for I18N purpose as the order of them depends on the Locale. + */ + private int[] getMonthDayYearIndexes(String pattern) { + int[] result = new int[3]; + + final String filteredPattern = pattern.replaceAll("'.*?'", ""); + + final int dayIndex = filteredPattern.indexOf('d'); + final int monthMIndex = filteredPattern.indexOf("M"); + final int monthIndex = (monthMIndex != -1) ? monthMIndex : filteredPattern.indexOf("L"); + final int yearIndex = filteredPattern.indexOf("y"); + + if (yearIndex < monthIndex) { + result[YEAR_INDEX] = 0; + + if (monthIndex < dayIndex) { + result[MONTH_INDEX] = 1; + result[DAY_INDEX] = 2; + } else { + result[MONTH_INDEX] = 2; + result[DAY_INDEX] = 1; + } + } else { + result[YEAR_INDEX] = 2; + + if (monthIndex < dayIndex) { + result[MONTH_INDEX] = 0; + result[DAY_INDEX] = 1; + } else { + result[MONTH_INDEX] = 1; + result[DAY_INDEX] = 0; + } + } + return result; + } + + private void updateDisplay(boolean announce) { + if (mDayOfWeekView != null) { + mDayOfWeekView.setText(mCurrentDate.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, + Locale.getDefault())); + } + final String bestDateTimePattern = + DateFormat.getBestDateTimePattern(mCurrentLocale, "yMMMd"); + + // Compute indices of Month, Day and Year views + int[] viewIndices = getMonthDayYearIndexes(bestDateTimePattern); + + // Restart from a clean state + mMonthAndDayLayout.removeAllViews(); + mDateLayout.removeView(mSelectedYearView); + + // Position the Year View at the correct location + if (viewIndices[YEAR_INDEX] == 0) { + mDateLayout.addView(mSelectedYearView, 0); + } else { + mDateLayout.addView(mSelectedYearView, 1); + } + + // Position Day and Month Views + if (viewIndices[MONTH_INDEX] > viewIndices[DAY_INDEX]) { + // Day View is first + mMonthAndDayLayout.addView(mSelectedDayTextView); + mMonthAndDayLayout.addView(mSelectedMonthTextView); + } else { + // Month View is first + mMonthAndDayLayout.addView(mSelectedMonthTextView); + mMonthAndDayLayout.addView(mSelectedDayTextView); + } + + mSelectedMonthTextView.setText(mCurrentDate.getDisplayName(Calendar.MONTH, Calendar.SHORT, + Locale.getDefault()).toUpperCase(Locale.getDefault())); + mSelectedDayTextView.setText(mDayFormat.format(mCurrentDate.getTime())); + mSelectedYearView.setText(mYearFormat.format(mCurrentDate.getTime())); + + // Accessibility. + long millis = mCurrentDate.getTimeInMillis(); + mAnimator.setDateMillis(millis); + int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR; + String monthAndDayText = DateUtils.formatDateTime(mContext, millis, flags); + mMonthAndDayLayout.setContentDescription(monthAndDayText); + + if (announce) { + flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; + String fullDateText = DateUtils.formatDateTime(mContext, millis, flags); + mAnimator.announceForAccessibility(fullDateText); + } + updatePickers(); + } + + private void setCurrentView(final int viewIndex) { + long millis = mCurrentDate.getTimeInMillis(); + + switch (viewIndex) { + case MONTH_AND_DAY_VIEW: + ObjectAnimator pulseAnimator = getPulseAnimator(mMonthAndDayLayout, 0.9f, + 1.05f); + if (mDelayAnimation) { + pulseAnimator.setStartDelay(ANIMATION_DELAY); + mDelayAnimation = false; + } + mDayPickerView.onDateChanged(); + if (mCurrentView != viewIndex) { + mMonthAndDayLayout.setSelected(true); + mSelectedYearView.setSelected(false); + mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); + mCurrentView = viewIndex; + } + pulseAnimator.start(); + + int flags = DateUtils.FORMAT_SHOW_DATE; + String dayString = DateUtils.formatDateTime(mContext, millis, flags); + mAnimator.setContentDescription(mDayPickerDescription + ": " + dayString); + mAnimator.announceForAccessibility(mSelectDay); + break; + case YEAR_VIEW: + pulseAnimator = getPulseAnimator(mSelectedYearView, 0.85f, 1.1f); + if (mDelayAnimation) { + pulseAnimator.setStartDelay(ANIMATION_DELAY); + mDelayAnimation = false; + } + mYearPickerView.onDateChanged(); + if (mCurrentView != viewIndex) { + mMonthAndDayLayout.setSelected(false); + mSelectedYearView.setSelected(true); + mAnimator.setDisplayedChild(YEAR_VIEW); + mCurrentView = viewIndex; + } + pulseAnimator.start(); + + CharSequence yearString = mYearFormat.format(millis); + mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString); + mAnimator.announceForAccessibility(mSelectYear); + break; + } + } + + @Override + public void init(int year, int monthOfYear, int dayOfMonth, + DatePicker.OnDateChangedListener callBack) { + mDateChangedListener = callBack; + mCurrentDate.set(Calendar.YEAR, year); + mCurrentDate.set(Calendar.MONTH, monthOfYear); + mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDisplay(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); + mDateChangedListener.onDateChanged(mDelegator, year, month, dayOfMonth); + updateDisplay(false); + } + + @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); + updatePickers(); + updateDisplay(false); + } + mMinDate.setTimeInMillis(minDate); + mDayPickerView.goTo(getSelectedDay(), false, true, true); + } + + @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); + updatePickers(); + updateDisplay(false); + } + mMaxDate.setTimeInMillis(maxDate); + mDayPickerView.goTo(getSelectedDay(), false, true, true); + } + + @Override + public Calendar getMaxDate() { + return mMaxDate; + } + + @Override + public int getFirstDayOfWeek() { + return mCurrentDate.getFirstDayOfWeek(); + } + + @Override + public int getMinYear() { + return mMinDate.get(Calendar.YEAR); + } + + @Override + public int getMaxYear() { + return mMaxDate.get(Calendar.YEAR); + } + + @Override + public int getMinMonth() { + return mMinDate.get(Calendar.MONTH); + } + + @Override + public int getMaxMonth() { + return mMaxDate.get(Calendar.MONTH); + } + + @Override + public int getMinDay() { + return mMinDate.get(Calendar.DAY_OF_MONTH); + } + + @Override + public int getMaxDay() { + return mMaxDate.get(Calendar.DAY_OF_MONTH); + } + + @Override + public void setEnabled(boolean enabled) { + mMonthAndDayLayout.setEnabled(enabled); + mSelectedYearView.setEnabled(enabled); + mAnimator.setEnabled(enabled); + mIsEnabled = enabled; + } + + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + @Override + public void setDateSelectorDayOfWeekBackgroundColor(int color) { + if (mDayOfWeekBackgroundColor != color) { + mDayOfWeekBackgroundColor = color; + mDayOfWeekView.setBackgroundColor(color); + } + } + + @Override + public int getDateSelectorDayOfWeekBackgroundColor() { + return mDayOfWeekBackgroundColor; + } + + @Override + public void setDateSelectorDayOfWeekTextAppearance(int resId) { + if (mDayOfWeekTextAppearanceResId != resId && resId > 0) { + mDayOfWeekTextAppearanceResId = resId; + mDayOfWeekView.setTextAppearance(mContext, resId); + } + } + + @Override + public int getDateSelectorDayOfWeekTextAppearance() { + return mDayOfWeekTextAppearanceResId; + } + + @Override + public void setDateSelectorBackgroundColor(int color) { + if (mMonthAndDayBackgroundColor != color) { + mMonthAndDayBackgroundColor = color; + mMonthAndDayLayout.setBackgroundColor(color); + } + } + + @Override + public int getDateSelectorBackgroundColor() { + return mMonthAndDayBackgroundColor; + } + + @Override + public void setDateSelectorMonthTextAppearance(int resId) { + if (mMonthTextAppearanceResId != resId && resId > 0) { + mMonthTextAppearanceResId = resId; + mSelectedMonthTextView.setTextAppearance(mContext, resId); + } + } + + @Override + public int getDateSelectorMonthTextAppearance() { + return mMonthTextAppearanceResId; + } + + @Override + public void setDateSelectorDayOfMonthTextAppearance(int resId) { + if (mDayOfMonthTextAppearanceResId != resId && resId > 0) { + mDayOfMonthTextAppearanceResId = resId; + mSelectedDayTextView.setTextAppearance(mContext, resId); + } + } + + @Override + public int getDateSelectorDayOfMonthTextAppearance() { + return mDayOfMonthTextAppearanceResId; + } + + @Override + public void setDateSelectorYearTextAppearance(int resId) { + if (mYearTextAppearanceResId != resId && resId > 0) { + mYearTextAppearanceResId = resId; + mSelectedYearView.setTextAppearance(mContext, resId); + } + } + + @Override + public int getDateSelectorYearTextAppearance() { + return mYearTextAppearanceResId; + } + + @Override + public void setDateSelectorYearListItemTextAppearance(int resId) { + if (mYearListItemTextAppearanceResId != resId) { + mYearListItemTextAppearanceResId = resId; + mYearPickerView.setItemTextAppearance(resId); + } + } + + @Override + public int getDateSelectorYearListItemTextAppearance() { + return mYearListItemTextAppearanceResId; + } + + @Override + public void setDateSelectorYearListSelectedCircleColor(int color) { + mYearPickerView.setYearSelectedCircleColor(color); + } + + @Override + public int getDateSelectorYearListSelectedCircleColor() { + return mYearPickerView.getYearSelectedCircleColor(); + } + + @Override + public void setCalendarTextColor(ColorStateList colors) { + if (colors == null) { + return; + } + if (mCalendarTextColors == null || !mCalendarTextColors.equals(colors)) { + mCalendarTextColors = colors; + mDayPickerView.setCalendarTextColor(colors); + } + } + + @Override + public ColorStateList getCalendarTextColors() { + return mCalendarTextColors; + } + + @Override + public CalendarView getCalendarView() { + throw new UnsupportedOperationException( + "CalendarView does not exists for the new 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 setShowDoneButton(boolean showDoneButton) { + mShowDoneButton = showDoneButton; + updateDoneButtonVisibility(); + } + + private void updateDoneButtonVisibility() { + mLayoutButtons.setVisibility(mShowDoneButton ? View.VISIBLE : View.GONE); + } + + @Override + public void setDismissCallback(DatePicker.DatePickerDismissCallback callback) { + mDismissCallback = callback; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + mYearFormat = new SimpleDateFormat("y", newConfig.locale); + mDayFormat = new SimpleDateFormat("d", newConfig.locale); + } + + @Override + public void dispatchRestoreInstanceState(SparseArray container) { + // Nothing to do + } + + @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 == MONTH_AND_DAY_VIEW) { + listPosition = mDayPickerView.getMostVisiblePosition(); + } else if (mCurrentView == YEAR_VIEW) { + 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) { + SavedState ss = (SavedState) state; + + mCurrentDate.set(ss.getSelectedDay(), ss.getSelectedMonth(), ss.getSelectedYear()); + mCurrentView = ss.getCurrentView(); + mMinDate.setTimeInMillis(ss.getMinDate()); + mMaxDate.setTimeInMillis(ss.getMaxDate()); + + updateDisplay(false); + setCurrentView(mCurrentView); + + final int listPosition = ss.getListPosition(); + if (listPosition != -1) { + if (mCurrentView == MONTH_AND_DAY_VIEW) { + mDayPickerView.postSetSelection(listPosition); + } else if (mCurrentView == YEAR_VIEW) { + mYearPickerView.postSetSelectionFromTop(listPosition, ss.getListPositionOffset()); + } + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return true; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + event.getText().add(mCurrentDate.getTime().toString()); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(DatePicker.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + info.setClassName(DatePicker.class.getName()); + } + + @Override + public void onYearSelected(int year) { + adjustDayInMonthIfNeeded(mCurrentDate.get(Calendar.MONTH), year); + mCurrentDate.set(Calendar.YEAR, year); + updatePickers(); + setCurrentView(MONTH_AND_DAY_VIEW); + updateDisplay(true); + updateDoneButtonEnableState(); + } + + // 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 = mCurrentDate.get(Calendar.DAY_OF_MONTH); + int daysInMonth = getDaysInMonth(month, year); + if (day > daysInMonth) { + mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth); + } + } + + 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"); + } + } + + @Override + public void onDayOfMonthSelected(int year, int month, int day) { + mCurrentDate.set(Calendar.YEAR, year); + mCurrentDate.set(Calendar.MONTH, month); + mCurrentDate.set(Calendar.DAY_OF_MONTH, day); + updatePickers(); + updateDisplay(true); + updateDoneButtonEnableState(); + } + + private void updateDoneButtonEnableState() { + if (mShowDoneButton) { + final boolean enabled = mCurrentDate.equals(mMinDate) || + mCurrentDate.equals(mMaxDate) || + (mCurrentDate.after(mMinDate) && mCurrentDate.before(mMaxDate)); + mDoneButton.setEnabled(enabled); + } + } + + private void updatePickers() { + Iterator iterator = mListeners.iterator(); + while (iterator.hasNext()) { + iterator.next().onDateChanged(); + } + } + + @Override + public void registerOnDateChangedListener(OnDateChangedListener listener) { + mListeners.add(listener); + } + + @Override + public void unregisterOnDateChangedListener(OnDateChangedListener listener) { + mListeners.remove(listener); + } + + @Override + public Calendar getSelectedDay() { + return mCurrentDate; + } + + @Override + public void tryVibrate() { + mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE); + } + + @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_layout) { + setCurrentView(MONTH_AND_DAY_VIEW); + } + } + + /** + * 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 DatePicker#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 CREATOR = new Creator() { + + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * Render an animator to pulsate a view in place. + * @param labelToAnimate the view to pulsate. + * @return The animator object. Use .start() to begin. + */ + public static ObjectAnimator getPulseAnimator(View labelToAnimate, float decreaseRatio, + float increaseRatio) { + Keyframe k0 = Keyframe.ofFloat(0f, 1f); + Keyframe k1 = Keyframe.ofFloat(0.275f, decreaseRatio); + Keyframe k2 = Keyframe.ofFloat(0.69f, increaseRatio); + Keyframe k3 = Keyframe.ofFloat(1f, 1f); + + PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe(View.SCALE_X, k0, k1, k2, k3); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe(View.SCALE_Y, k0, k1, k2, k3); + ObjectAnimator pulseAnimator = + ObjectAnimator.ofPropertyValuesHolder(labelToAnimate, scaleX, scaleY); + pulseAnimator.setDuration(PULSE_ANIMATOR_DURATION); + + return pulseAnimator; + } +} diff --git a/core/java/android/widget/DayPickerView.java b/core/java/android/widget/DayPickerView.java new file mode 100644 index 0000000000000..c44bd46cb7991 --- /dev/null +++ b/core/java/android/widget/DayPickerView.java @@ -0,0 +1,506 @@ +/* + * 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.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + +/** + * This displays a list of months in a calendar format with selectable days. + */ +class DayPickerView extends ListView implements AbsListView.OnScrollListener, + OnDateChangedListener { + + private static final String TAG = "DayPickerView"; + + // How long the GoTo fling animation should last + private static final int GOTO_SCROLL_DURATION = 250; + + // How long to wait after receiving an onScrollStateChanged notification before acting on it + private static final int SCROLL_CHANGE_DELAY = 40; + + private static int LIST_TOP_OFFSET = -1; // so that the top line will be under the separator + + private SimpleDateFormat mYearFormat = new SimpleDateFormat("yyyy", Locale.getDefault()); + + // These affect the scroll speed and feel + private float mFriction = 1.0f; + + // highlighted time + private Calendar mSelectedDay = Calendar.getInstance(); + private SimpleMonthAdapter mAdapter; + + private Calendar mTempDay = Calendar.getInstance(); + + // which month should be displayed/highlighted [0-11] + private int mCurrentMonthDisplayed; + // used for tracking what state listview is in + private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; + // used for tracking what state listview is in + private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; + + private DatePickerController mController; + private boolean mPerformingScroll; + + private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(this); + + public DayPickerView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public DayPickerView(Context context, DatePickerController controller) { + super(context); + init(); + setController(controller); + } + + public void setController(DatePickerController controller) { + if (mController != null) { + mController.unregisterOnDateChangedListener(this); + } + mController = controller; + mController.registerOnDateChangedListener(this); + setUpAdapter(); + setAdapter(mAdapter); + onDateChanged(); + } + + public void init() { + setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + setDrawSelectorOnTop(false); + + setUpListView(); + } + + public void onChange() { + setUpAdapter(); + setAdapter(mAdapter); + } + + /** + * Creates a new adapter if necessary and sets up its parameters. Override + * this method to provide a custom adapter. + */ + protected void setUpAdapter() { + if (mAdapter == null) { + mAdapter = new SimpleMonthAdapter(getContext(), mController); + } else { + mAdapter.setSelectedDay(mSelectedDay); + mAdapter.notifyDataSetChanged(); + } + // refresh the view with the new parameters + mAdapter.notifyDataSetChanged(); + } + + /* + * Sets all the required fields for the list view. Override this method to + * set a different list view behavior. + */ + protected void setUpListView() { + // Transparent background on scroll + setCacheColorHint(0); + // No dividers + setDivider(null); + // Items are clickable + setItemsCanFocus(true); + // The thumb gets in the way, so disable it + setFastScrollEnabled(false); + setVerticalScrollBarEnabled(false); + setOnScrollListener(this); + setFadingEdgeLength(0); + // Make the scrolling behavior nicer + setFriction(ViewConfiguration.getScrollFriction() * mFriction); + } + + private int getDiffMonths(Calendar start, Calendar end){ + final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR); + final int diffMonths = end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears; + return diffMonths; + } + + private int getPositionFromDay(Calendar day) { + final int diffMonthMax = getDiffMonths(mController.getMinDate(), mController.getMaxDate()); + int diffMonth = getDiffMonths(mController.getMinDate(), day); + + if (diffMonth < 0 ) { + diffMonth = 0; + } else if (diffMonth > diffMonthMax) { + diffMonth = diffMonthMax; + } + + return diffMonth; + } + + /** + * This moves to the specified time in the view. If the time is not already + * in range it will move the list so that the first of the month containing + * the time is at the top of the view. If the new time is already in view + * the list will not be scrolled unless forceScroll is true. This time may + * optionally be highlighted as selected as well. + * + * @param day The day to move to + * @param animate Whether to scroll to the given time or just redraw at the + * new location + * @param setSelected Whether to set the given time as selected + * @param forceScroll Whether to recenter even if the time is already + * visible + * @return Whether or not the view animated to the new location + */ + public boolean goTo(Calendar day, boolean animate, boolean setSelected, + boolean forceScroll) { + + // Set the selected day + if (setSelected) { + mSelectedDay.setTimeInMillis(day.getTimeInMillis()); + } + + mTempDay.setTimeInMillis(day.getTimeInMillis()); + final int position = getPositionFromDay(day); + + View child; + int i = 0; + int top = 0; + // Find a child that's completely in the view + do { + child = getChildAt(i++); + if (child == null) { + break; + } + top = child.getTop(); + } while (top < 0); + + // Compute the first and last position visible + int selectedPosition; + if (child != null) { + selectedPosition = getPositionForView(child); + } else { + selectedPosition = 0; + } + + if (setSelected) { + mAdapter.setSelectedDay(mSelectedDay); + } + + // Check if the selected day is now outside of our visible range + // and if so scroll to the month that contains it + if (position != selectedPosition || forceScroll) { + setMonthDisplayed(mTempDay); + mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; + if (animate) { + smoothScrollToPositionFromTop( + position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION); + return true; + } else { + postSetSelection(position); + } + } else if (setSelected) { + setMonthDisplayed(mSelectedDay); + } + return false; + } + + public void postSetSelection(final int position) { + clearFocus(); + post(new Runnable() { + + @Override + public void run() { + setSelection(position); + } + }); + onScrollStateChanged(this, OnScrollListener.SCROLL_STATE_IDLE); + } + + /** + * Updates the title and selected month if the view has moved to a new + * month. + */ + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + SimpleMonthView child = (SimpleMonthView) view.getChildAt(0); + if (child == null) { + return; + } + + mPreviousScrollState = mCurrentScrollState; + } + + /** + * Sets the month displayed at the top of this view based on time. Override + * to add custom events when the title is changed. + */ + protected void setMonthDisplayed(Calendar date) { + if (mCurrentMonthDisplayed != date.get(Calendar.MONTH)) { + mCurrentMonthDisplayed = date.get(Calendar.MONTH); + invalidateViews(); + } + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // use a post to prevent re-entering onScrollStateChanged before it + // exits + mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); + } + + void setCalendarTextColor(ColorStateList colors) { + mAdapter.setCalendarTextColor(colors); + } + + protected class ScrollStateRunnable implements Runnable { + private int mNewState; + private View mParent; + + ScrollStateRunnable(View view) { + mParent = view; + } + + /** + * Sets up the runnable with a short delay in case the scroll state + * immediately changes again. + * + * @param view The list view that changed state + * @param scrollState The new state it changed to + */ + public void doScrollStateChange(AbsListView view, int scrollState) { + mParent.removeCallbacks(this); + mNewState = scrollState; + mParent.postDelayed(this, SCROLL_CHANGE_DELAY); + } + + @Override + public void run() { + mCurrentScrollState = mNewState; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, + "new scroll state: " + mNewState + " old state: " + mPreviousScrollState); + } + // Fix the position after a scroll or a fling ends + if (mNewState == OnScrollListener.SCROLL_STATE_IDLE + && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE + && mPreviousScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + mPreviousScrollState = mNewState; + int i = 0; + View child = getChildAt(i); + while (child != null && child.getBottom() <= 0) { + child = getChildAt(++i); + } + if (child == null) { + // The view is no longer visible, just return + return; + } + int firstPosition = getFirstVisiblePosition(); + int lastPosition = getLastVisiblePosition(); + boolean scroll = firstPosition != 0 && lastPosition != getCount() - 1; + final int top = child.getTop(); + final int bottom = child.getBottom(); + final int midpoint = getHeight() / 2; + if (scroll && top < LIST_TOP_OFFSET) { + if (bottom > midpoint) { + smoothScrollBy(top, GOTO_SCROLL_DURATION); + } else { + smoothScrollBy(bottom, GOTO_SCROLL_DURATION); + } + } + } else { + mPreviousScrollState = mNewState; + } + } + } + + /** + * Gets the position of the view that is most prominently displayed within the list view. + */ + public int getMostVisiblePosition() { + final int firstPosition = getFirstVisiblePosition(); + final int height = getHeight(); + + int maxDisplayedHeight = 0; + int mostVisibleIndex = 0; + int i=0; + int bottom = 0; + while (bottom < height) { + View child = getChildAt(i); + if (child == null) { + break; + } + bottom = child.getBottom(); + int displayedHeight = Math.min(bottom, height) - Math.max(0, child.getTop()); + if (displayedHeight > maxDisplayedHeight) { + mostVisibleIndex = i; + maxDisplayedHeight = displayedHeight; + } + i++; + } + return firstPosition + mostVisibleIndex; + } + + @Override + public void onDateChanged() { + goTo(mController.getSelectedDay(), false, true, true); + } + + /** + * Attempts to return the date that has accessibility focus. + * + * @return The date that has accessibility focus, or {@code null} if no date + * has focus. + */ + private Calendar findAccessibilityFocus() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child instanceof SimpleMonthView) { + final Calendar focus = ((SimpleMonthView) child).getAccessibilityFocus(); + if (focus != null) { + return focus; + } + } + } + + return null; + } + + /** + * Attempts to restore accessibility focus to a given date. No-op if + * {@code day} is {@code null}. + * + * @param day The date that should receive accessibility focus + * @return {@code true} if focus was restored + */ + private boolean restoreAccessibilityFocus(Calendar day) { + if (day == null) { + return false; + } + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child instanceof SimpleMonthView) { + if (((SimpleMonthView) child).restoreAccessibilityFocus(day)) { + return true; + } + } + } + + return false; + } + + @Override + protected void layoutChildren() { + final Calendar focusedDay = findAccessibilityFocus(); + super.layoutChildren(); + if (mPerformingScroll) { + mPerformingScroll = false; + } else { + restoreAccessibilityFocus(focusedDay); + } + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + mYearFormat = new SimpleDateFormat("yyyy", Locale.getDefault()); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setItemCount(-1); + } + + private String getMonthAndYearString(Calendar day) { + StringBuffer sbuf = new StringBuffer(); + sbuf.append(day.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault())); + sbuf.append(" "); + sbuf.append(mYearFormat.format(day.getTime())); + return sbuf.toString(); + } + + /** + * Necessary for accessibility, to ensure we support "scrolling" forward and backward + * in the month list. + */ + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + + /** + * When scroll forward/backward events are received, announce the newly scrolled-to month. + */ + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && + action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + return super.performAccessibilityAction(action, arguments); + } + + // Figure out what month is showing. + int firstVisiblePosition = getFirstVisiblePosition(); + int month = firstVisiblePosition % 12; + int year = firstVisiblePosition / 12 + mController.getMinYear(); + Calendar day = Calendar.getInstance(); + day.set(year, month, 1); + + // Scroll either forward or backward one month. + if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + day.add(Calendar.MONTH, 1); + if (day.get(Calendar.MONTH) == 12) { + day.set(Calendar.MONTH, 0); + day.add(Calendar.YEAR, 1); + } + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + View firstVisibleView = getChildAt(0); + // If the view is fully visible, jump one month back. Otherwise, we'll just jump + // to the first day of first visible month. + if (firstVisibleView != null && firstVisibleView.getTop() >= -1) { + // There's an off-by-one somewhere, so the top of the first visible item will + // actually be -1 when it's at the exact top. + day.add(Calendar.MONTH, -1); + if (day.get(Calendar.MONTH) == -1) { + day.set(Calendar.MONTH, 11); + day.add(Calendar.YEAR, -1); + } + } + } + + // Go to that month. + announceForAccessibility(getMonthAndYearString(day)); + goTo(day, true, false, true); + mPerformingScroll = true; + return true; + } +} diff --git a/core/java/android/widget/OnDateChangedListener.java b/core/java/android/widget/OnDateChangedListener.java new file mode 100644 index 0000000000000..29be888afe54b --- /dev/null +++ b/core/java/android/widget/OnDateChangedListener.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * The callback used to notify other date picker components of a change in the selected date. + * + */ +interface OnDateChangedListener { + + public void onDateChanged(); +} + diff --git a/core/java/android/widget/SimpleMonthAdapter.java b/core/java/android/widget/SimpleMonthAdapter.java new file mode 100644 index 0000000000000..53d0839eb5fa8 --- /dev/null +++ b/core/java/android/widget/SimpleMonthAdapter.java @@ -0,0 +1,178 @@ +/* + * 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.content.Context; +import android.content.res.ColorStateList; +import android.view.View; +import android.view.ViewGroup; + +import java.util.Calendar; +import java.util.HashMap; + +/** + * An adapter for a list of {@link android.widget.SimpleMonthView} items. + */ +class SimpleMonthAdapter extends BaseAdapter implements SimpleMonthView.OnDayClickListener { + private static final String TAG = "SimpleMonthAdapter"; + + private final Context mContext; + private final DatePickerController mController; + private Calendar mSelectedDay; + + private ColorStateList mCalendarTextColors; + + public SimpleMonthAdapter(Context context, DatePickerController controller) { + mContext = context; + mController = controller; + init(); + setSelectedDay(mController.getSelectedDay()); + } + + /** + * Updates the selected day and related parameters. + * + * @param day The day to highlight + */ + public void setSelectedDay(Calendar day) { + if (mSelectedDay != day) { + mSelectedDay = day; + notifyDataSetChanged(); + } + } + + void setCalendarTextColor(ColorStateList colors) { + mCalendarTextColors = colors; + } + + /** + * Set up the gesture detector and selected time + */ + protected void init() { + mSelectedDay = Calendar.getInstance(); + } + + @Override + public int getCount() { + final int diffYear = mController.getMaxYear() - mController.getMinYear(); + final int diffMonth = 1 + mController.getMaxMonth() - mController.getMinMonth() + + 12 * diffYear; + return diffMonth; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @SuppressWarnings("unchecked") + @Override + public View getView(int position, View convertView, ViewGroup parent) { + SimpleMonthView v; + HashMap drawingParams = null; + if (convertView != null) { + v = (SimpleMonthView) convertView; + // We store the drawing parameters in the view so it can be recycled + drawingParams = (HashMap) v.getTag(); + } else { + v = new SimpleMonthView(mContext); + // Set up the new view + AbsListView.LayoutParams params = new AbsListView.LayoutParams( + AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.MATCH_PARENT); + v.setLayoutParams(params); + v.setClickable(true); + v.setOnDayClickListener(this); + if (mCalendarTextColors != null) { + v.setTextColor(mCalendarTextColors); + } + } + if (drawingParams == null) { + drawingParams = new HashMap(); + } else { + drawingParams.clear(); + } + final int currentMonth = position + mController.getMinMonth(); + final int month = currentMonth % 12; + final int year = currentMonth / 12 + mController.getMinYear(); + + int selectedDay = -1; + if (isSelectedDayInMonth(year, month)) { + selectedDay = mSelectedDay.get(Calendar.DAY_OF_MONTH); + } + + // Invokes requestLayout() to ensure that the recycled view is set with the appropriate + // height/number of weeks before being displayed. + v.reuse(); + + final int enabledDayRangeStart; + if (mController.getMinMonth() == month && mController.getMinYear() == year) { + enabledDayRangeStart = mController.getMinDay(); + } else { + enabledDayRangeStart = 1; + } + + final int enabledDayRangeEnd; + if (mController.getMaxMonth() == month && mController.getMaxYear() == year) { + enabledDayRangeEnd = mController.getMaxDay(); + } else { + enabledDayRangeEnd = 31; + } + + drawingParams.put(SimpleMonthView.VIEW_PARAMS_SELECTED_DAY, selectedDay); + drawingParams.put(SimpleMonthView.VIEW_PARAMS_YEAR, year); + drawingParams.put(SimpleMonthView.VIEW_PARAMS_MONTH, month); + drawingParams.put(SimpleMonthView.VIEW_PARAMS_WEEK_START, mController.getFirstDayOfWeek()); + drawingParams.put(SimpleMonthView.VIEW_PARAMS_ENABLEDDAYRANGE_START, enabledDayRangeStart); + drawingParams.put(SimpleMonthView.VIEW_PARAMS_ENABLEDDAYRANGE_END, enabledDayRangeEnd); + v.setMonthParams(drawingParams); + v.invalidate(); + return v; + } + + private boolean isSelectedDayInMonth(int year, int month) { + return mSelectedDay.get(Calendar.YEAR) == year && mSelectedDay.get(Calendar.MONTH) == month; + } + + @Override + public void onDayClick(SimpleMonthView view, Calendar day) { + if (day != null) { + onDayTapped(day); + } + } + + /** + * Maintains the same hour/min/sec but moves the day to the tapped day. + * + * @param day The day that was tapped + */ + protected void onDayTapped(Calendar day) { + mController.tryVibrate(); + mController.onDayOfMonthSelected(day.get(Calendar.YEAR), day.get(Calendar.MONTH), + day.get(Calendar.DAY_OF_MONTH)); + setSelectedDay(day); + } +} diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java new file mode 100644 index 0000000000000..7589711f815db --- /dev/null +++ b/core/java/android/widget/SimpleMonthView.java @@ -0,0 +1,720 @@ +/* + * 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.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Bundle; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.text.format.Time; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.internal.R; +import com.android.internal.widget.ExploreByTouchHelper; + +import java.security.InvalidParameterException; +import java.util.Calendar; +import java.util.Formatter; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +/** + * A calendar-like view displaying a specified month and the appropriate selectable day numbers + * within the specified month. + */ +class SimpleMonthView extends View { + private static final String TAG = "SimpleMonthView"; + + /** + * These params can be passed into the view to control how it appears. + * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default + * values are unlikely to fit most layouts correctly. + */ + /** + * This sets the height of this week in pixels + */ + static final String VIEW_PARAMS_HEIGHT = "height"; + /** + * This specifies the position (or weeks since the epoch) of this week, + * calculated using + */ + static final String VIEW_PARAMS_MONTH = "month"; + /** + * This specifies the position (or weeks since the epoch) of this week, + * calculated using + */ + static final String VIEW_PARAMS_YEAR = "year"; + /** + * This sets one of the days in this view as selected {@link Time#SUNDAY} + * through {@link Time#SATURDAY}. + */ + static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; + /** + * Which day the week should start on. {@link Time#SUNDAY} through + * {@link Time#SATURDAY}. + */ + static final String VIEW_PARAMS_WEEK_START = "week_start"; + /** + * First enabled day. + */ + static final String VIEW_PARAMS_ENABLEDDAYRANGE_START = "enabled_day_range_start"; + /** + * Last enabled day. + */ + static final String VIEW_PARAMS_ENABLEDDAYRANGE_END = "enabled_day_range_end"; + + private static int DEFAULT_HEIGHT = 32; + private static int MIN_HEIGHT = 10; + + private static final int DEFAULT_SELECTED_DAY = -1; + private static final int DEFAULT_WEEK_START = Calendar.SUNDAY; + private static final int DEFAULT_NUM_DAYS = 7; + private static final int DEFAULT_NUM_ROWS = 6; + private static final int MAX_NUM_ROWS = 6; + + private static final int SELECTED_CIRCLE_ALPHA = 60; + + private static int DAY_SEPARATOR_WIDTH = 1; + + private int mMiniDayNumberTextSize; + private int mMonthLabelTextSize; + private int mMonthDayLabelTextSize; + private int mMonthHeaderSize; + private int mDaySelectedCircleSize; + + // used for scaling to the device density + private static float mScale = 0; + + // affects the padding on the sides of this view + private int mPadding = 0; + + private String mDayOfWeekTypeface; + private String mMonthTitleTypeface; + + private Paint mDayNumberPaint; + private Paint mDayNumberDisabledPaint; + private Paint mDayNumberSelectedPaint; + + private Paint mMonthTitlePaint; + private Paint mMonthDayLabelPaint; + + private final Formatter mFormatter; + private final StringBuilder mStringBuilder; + + private int mMonth; + private int mYear; + + // Quick reference to the width of this view, matches parent + private int mWidth; + + // The height this view should draw at in pixels, set by height param + private int mRowHeight = DEFAULT_HEIGHT; + + // If this view contains the today + private boolean mHasToday = false; + + // Which day is selected [0-6] or -1 if no day is selected + private int mSelectedDay = -1; + + // Which day is today [0-6] or -1 if no day is today + private int mToday = DEFAULT_SELECTED_DAY; + + // Which day of the week to start on [0-6] + private int mWeekStart = DEFAULT_WEEK_START; + + // How many days to display + private int mNumDays = DEFAULT_NUM_DAYS; + + // The number of days + a spot for week number if it is displayed + private int mNumCells = mNumDays; + + private int mDayOfWeekStart = 0; + + // First enabled day + private int mEnabledDayStart = 1; + + // Last enabled day + private int mEnabledDayEnd = 31; + + private final Calendar mCalendar = Calendar.getInstance(); + private final Calendar mDayLabelCalendar = Calendar.getInstance(); + + private final MonthViewTouchHelper mTouchHelper; + + private int mNumRows = DEFAULT_NUM_ROWS; + + // Optional listener for handling day click actions + private OnDayClickListener mOnDayClickListener; + + // Whether to prevent setting the accessibility delegate + private boolean mLockAccessibilityDelegate; + + private int mNormalTextColor; + private int mDisabledTextColor; + private int mSelectedDayColor; + + public SimpleMonthView(Context context) { + this(context, null); + } + + public SimpleMonthView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.datePickerStyle); + } + + public SimpleMonthView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs); + + final Resources res = context.getResources(); + + mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface); + mMonthTitleTypeface = res.getString(R.string.sans_serif); + + mStringBuilder = new StringBuilder(50); + mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); + + mMiniDayNumberTextSize = res.getDimensionPixelSize(R.dimen.datepicker_day_number_size); + mMonthLabelTextSize = res.getDimensionPixelSize(R.dimen.datepicker_month_label_size); + mMonthDayLabelTextSize = res.getDimensionPixelSize( + R.dimen.datepicker_month_day_label_text_size); + mMonthHeaderSize = res.getDimensionPixelOffset( + R.dimen.datepicker_month_list_item_header_height); + mDaySelectedCircleSize = res.getDimensionPixelSize( + R.dimen.datepicker_day_number_select_circle_radius); + + mRowHeight = (res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height) + - mMonthHeaderSize) / MAX_NUM_ROWS; + + // Set up accessibility components. + mTouchHelper = new MonthViewTouchHelper(this); + setAccessibilityDelegate(mTouchHelper); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + mLockAccessibilityDelegate = true; + + // Sets up any standard paints that will be used + initView(); + } + + void setTextColor(ColorStateList colors) { + final Resources res = getContext().getResources(); + + mNormalTextColor = colors.getColorForState(ENABLED_STATE_SET, + res.getColor(R.color.datepicker_default_normal_text_color_holo_light)); + mMonthTitlePaint.setColor(mNormalTextColor); + mMonthDayLabelPaint.setColor(mNormalTextColor); + + mDisabledTextColor = colors.getColorForState(EMPTY_STATE_SET, + res.getColor(R.color.datepicker_default_disabled_text_color_holo_light)); + mDayNumberDisabledPaint.setColor(mDisabledTextColor); + + mSelectedDayColor = colors.getColorForState(ENABLED_SELECTED_STATE_SET, + res.getColor(R.color.holo_blue_light)); + mDayNumberSelectedPaint.setColor(mSelectedDayColor); + mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); + } + + @Override + public void setAccessibilityDelegate(AccessibilityDelegate delegate) { + // Workaround for a JB MR1 issue where accessibility delegates on + // top-level ListView items are overwritten. + if (!mLockAccessibilityDelegate) { + super.setAccessibilityDelegate(delegate); + } + } + + public void setOnDayClickListener(OnDayClickListener listener) { + mOnDayClickListener = listener; + } + + @Override + public boolean dispatchHoverEvent(MotionEvent event) { + // First right-of-refusal goes the touch exploration helper. + if (mTouchHelper.dispatchHoverEvent(event)) { + return true; + } + return super.dispatchHoverEvent(event); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + final int day = getDayFromLocation(event.getX(), event.getY()); + if (day >= 0) { + onDayClick(day); + } + break; + } + return true; + } + + /** + * Sets up the text and style properties for painting. + */ + private void initView() { + mMonthTitlePaint = new Paint(); + mMonthTitlePaint.setAntiAlias(true); + mMonthTitlePaint.setColor(mNormalTextColor); + mMonthTitlePaint.setTextSize(mMonthLabelTextSize); + mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD)); + mMonthTitlePaint.setTextAlign(Align.CENTER); + mMonthTitlePaint.setStyle(Style.FILL); + mMonthTitlePaint.setFakeBoldText(true); + + mMonthDayLabelPaint = new Paint(); + mMonthDayLabelPaint.setAntiAlias(true); + mMonthDayLabelPaint.setColor(mNormalTextColor); + mMonthDayLabelPaint.setTextSize(mMonthDayLabelTextSize); + mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL)); + mMonthDayLabelPaint.setTextAlign(Align.CENTER); + mMonthDayLabelPaint.setStyle(Style.FILL); + mMonthDayLabelPaint.setFakeBoldText(true); + + mDayNumberSelectedPaint = new Paint(); + mDayNumberSelectedPaint.setAntiAlias(true); + mDayNumberSelectedPaint.setColor(mSelectedDayColor); + mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); + mDayNumberSelectedPaint.setTextAlign(Align.CENTER); + mDayNumberSelectedPaint.setStyle(Style.FILL); + mDayNumberSelectedPaint.setFakeBoldText(true); + + mDayNumberPaint = new Paint(); + mDayNumberPaint.setAntiAlias(true); + mDayNumberPaint.setTextSize(mMiniDayNumberTextSize); + mDayNumberPaint.setTextAlign(Align.CENTER); + mDayNumberPaint.setStyle(Style.FILL); + mDayNumberPaint.setFakeBoldText(false); + + mDayNumberDisabledPaint = new Paint(); + mDayNumberDisabledPaint.setAntiAlias(true); + mDayNumberDisabledPaint.setColor(mDisabledTextColor); + mDayNumberDisabledPaint.setTextSize(mMiniDayNumberTextSize); + mDayNumberDisabledPaint.setTextAlign(Align.CENTER); + mDayNumberDisabledPaint.setStyle(Style.FILL); + mDayNumberDisabledPaint.setFakeBoldText(false); + } + + @Override + protected void onDraw(Canvas canvas) { + drawMonthTitle(canvas); + drawWeekDayLabels(canvas); + drawDays(canvas); + } + + /** + * Sets all the parameters for displaying this week. The only required + * parameter is the week number. Other parameters have a default value and + * will only update if a new value is included, except for focus month, + * which will always default to no focus month if no value is passed in. See + * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters. + * + * @param params A map of the new parameters, see + * {@link #VIEW_PARAMS_HEIGHT} + */ + void setMonthParams(HashMap params) { + if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { + throw new InvalidParameterException( + "You must specify the month and year for this view"); + } + setTag(params); + // We keep the current value for any params not present + if (params.containsKey(VIEW_PARAMS_HEIGHT)) { + mRowHeight = params.get(VIEW_PARAMS_HEIGHT); + if (mRowHeight < MIN_HEIGHT) { + mRowHeight = MIN_HEIGHT; + } + } + if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { + mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY); + } + + // Allocate space for caching the day numbers and focus values + mMonth = params.get(VIEW_PARAMS_MONTH); + mYear = params.get(VIEW_PARAMS_YEAR); + + // Figure out what day today is + final Time today = new Time(Time.getCurrentTimezone()); + today.setToNow(); + mHasToday = false; + mToday = -1; + + mCalendar.set(Calendar.MONTH, mMonth); + mCalendar.set(Calendar.YEAR, mYear); + mCalendar.set(Calendar.DAY_OF_MONTH, 1); + mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); + + if (params.containsKey(VIEW_PARAMS_WEEK_START)) { + mWeekStart = params.get(VIEW_PARAMS_WEEK_START); + } else { + mWeekStart = mCalendar.getFirstDayOfWeek(); + } + + if (params.containsKey(VIEW_PARAMS_ENABLEDDAYRANGE_START)) { + mEnabledDayStart = params.get(VIEW_PARAMS_ENABLEDDAYRANGE_START); + } + if (params.containsKey(VIEW_PARAMS_ENABLEDDAYRANGE_END)) { + mEnabledDayEnd = params.get(VIEW_PARAMS_ENABLEDDAYRANGE_END); + } + + mNumCells = getDaysInMonth(mMonth, mYear); + for (int i = 0; i < mNumCells; i++) { + final int day = i + 1; + if (sameDay(day, today)) { + mHasToday = true; + mToday = day; + } + } + mNumRows = calculateNumRows(); + + // Invalidate cached accessibility information. + mTouchHelper.invalidateRoot(); + } + + private 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"); + } + } + + public void reuse() { + mNumRows = DEFAULT_NUM_ROWS; + requestLayout(); + } + + private int calculateNumRows() { + int offset = findDayOffset(); + int dividend = (offset + mNumCells) / mNumDays; + int remainder = (offset + mNumCells) % mNumDays; + return (dividend + (remainder > 0 ? 1 : 0)); + } + + private boolean sameDay(int day, Time today) { + return mYear == today.year && + mMonth == today.month && + day == today.monthDay; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows + + mMonthHeaderSize); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mWidth = w; + + // Invalidate cached accessibility information. + mTouchHelper.invalidateRoot(); + } + + private String getMonthAndYearString() { + int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR + | DateUtils.FORMAT_NO_MONTH_DAY; + mStringBuilder.setLength(0); + long millis = mCalendar.getTimeInMillis(); + return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, + Time.getCurrentTimezone()).toString(); + } + + private void drawMonthTitle(Canvas canvas) { + int x = (mWidth + 2 * mPadding) / 2; + int y = (mMonthHeaderSize - mMonthDayLabelTextSize) / 2 + (mMonthLabelTextSize / 3); + canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); + } + + private void drawWeekDayLabels(Canvas canvas) { + int y = mMonthHeaderSize - (mMonthDayLabelTextSize / 2); + int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); + + for (int i = 0; i < mNumDays; i++) { + int calendarDay = (i + mWeekStart) % mNumDays; + int x = (2 * i + 1) * dayWidthHalf + mPadding; + mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); + canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, + Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y, + mMonthDayLabelPaint); + } + } + + /** + * Draws the month days. + */ + private void drawDays(Canvas canvas) { + int y = (((mRowHeight + mMiniDayNumberTextSize) / 2) - DAY_SEPARATOR_WIDTH) + + mMonthHeaderSize; + int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); + int j = findDayOffset(); + for (int day = 1; day <= mNumCells; day++) { + int x = (2 * j + 1) * dayWidthHalf + mPadding; + if (mSelectedDay == day) { + canvas.drawCircle(x, y - (mMiniDayNumberTextSize / 3), mDaySelectedCircleSize, + mDayNumberSelectedPaint); + } + + if (mHasToday && mToday == day) { + mDayNumberPaint.setColor(mSelectedDayColor); + } else { + mDayNumberPaint.setColor(mNormalTextColor); + } + final Paint paint = (day < mEnabledDayStart || day > mEnabledDayEnd) ? + mDayNumberDisabledPaint : mDayNumberPaint; + canvas.drawText(String.format("%d", day), x, y, paint); + j++; + if (j == mNumDays) { + j = 0; + y += mRowHeight; + } + } + } + + private int findDayOffset() { + return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) + - mWeekStart; + } + + /** + * Calculates the day that the given x position is in, accounting for week + * number. Returns the day or -1 if the position wasn't in a day. + * + * @param x The x position of the touch event + * @return The day number, or -1 if the position wasn't in a day + */ + private int getDayFromLocation(float x, float y) { + int dayStart = mPadding; + if (x < dayStart || x > mWidth - mPadding) { + return -1; + } + // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels + int row = (int) (y - mMonthHeaderSize) / mRowHeight; + int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); + + int day = column - findDayOffset() + 1; + day += row * mNumDays; + if (day < 1 || day > mNumCells) { + return -1; + } + return day; + } + + /** + * Called when the user clicks on a day. Handles callbacks to the + * {@link OnDayClickListener} if one is set. + * + * @param day The day that was clicked + */ + private void onDayClick(int day) { + if (mOnDayClickListener != null) { + Calendar date = Calendar.getInstance(); + date.set(mYear, mMonth, day); + mOnDayClickListener.onDayClick(this, date); + } + + // This is a no-op if accessibility is turned off. + mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); + } + + /** + * @return The date that has accessibility focus, or {@code null} if no date + * has focus + */ + Calendar getAccessibilityFocus() { + final int day = mTouchHelper.getFocusedVirtualView(); + Calendar date = null; + if (day >= 0) { + date = Calendar.getInstance(); + date.set(mYear, mMonth, day); + } + return date; + } + + /** + * Clears accessibility focus within the view. No-op if the view does not + * contain accessibility focus. + */ + public void clearAccessibilityFocus() { + mTouchHelper.clearFocusedVirtualView(); + } + + /** + * Attempts to restore accessibility focus to the specified date. + * + * @param day The date which should receive focus + * @return {@code false} if the date is not valid for this month view, or + * {@code true} if the date received focus + */ + boolean restoreAccessibilityFocus(Calendar day) { + if ((day.get(Calendar.YEAR) != mYear) || (day.get(Calendar.MONTH) != mMonth) || + (day.get(Calendar.DAY_OF_MONTH) > mNumCells)) { + return false; + } + mTouchHelper.setFocusedVirtualView(day.get(Calendar.DAY_OF_MONTH)); + return true; + } + + /** + * Provides a virtual view hierarchy for interfacing with an accessibility + * service. + */ + private class MonthViewTouchHelper extends ExploreByTouchHelper { + private static final String DATE_FORMAT = "dd MMMM yyyy"; + + private final Rect mTempRect = new Rect(); + private final Calendar mTempCalendar = Calendar.getInstance(); + + public MonthViewTouchHelper(View host) { + super(host); + } + + public void setFocusedVirtualView(int virtualViewId) { + getAccessibilityNodeProvider(SimpleMonthView.this).performAction( + virtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + + public void clearFocusedVirtualView() { + final int focusedVirtualView = getFocusedVirtualView(); + if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { + getAccessibilityNodeProvider(SimpleMonthView.this).performAction( + focusedVirtualView, + AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, + null); + } + } + + @Override + protected int getVirtualViewAt(float x, float y) { + final int day = getDayFromLocation(x, y); + if (day >= 0) { + return day; + } + return ExploreByTouchHelper.INVALID_ID; + } + + @Override + protected void getVisibleVirtualViews(List virtualViewIds) { + for (int day = 1; day <= mNumCells; day++) { + virtualViewIds.add(day); + } + } + + @Override + protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + event.setContentDescription(getItemDescription(virtualViewId)); + } + + @Override + protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { + getItemBounds(virtualViewId, mTempRect); + + node.setContentDescription(getItemDescription(virtualViewId)); + node.setBoundsInParent(mTempRect); + node.addAction(AccessibilityNodeInfo.ACTION_CLICK); + + if (virtualViewId == mSelectedDay) { + node.setSelected(true); + } + + } + + @Override + protected boolean onPerformActionForVirtualView(int virtualViewId, int action, + Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: + onDayClick(virtualViewId); + return true; + } + + return false; + } + + /** + * Calculates the bounding rectangle of a given time object. + * + * @param day The day to calculate bounds for + * @param rect The rectangle in which to store the bounds + */ + private void getItemBounds(int day, Rect rect) { + final int offsetX = mPadding; + final int offsetY = mMonthHeaderSize; + final int cellHeight = mRowHeight; + final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays); + final int index = ((day - 1) + findDayOffset()); + final int row = (index / mNumDays); + final int column = (index % mNumDays); + final int x = (offsetX + (column * cellWidth)); + final int y = (offsetY + (row * cellHeight)); + + rect.set(x, y, (x + cellWidth), (y + cellHeight)); + } + + /** + * Generates a description for a given time object. Since this + * description will be spoken, the components are ordered by descending + * specificity as DAY MONTH YEAR. + * + * @param day The day to generate a description for + * @return A description of the time object + */ + private CharSequence getItemDescription(int day) { + mTempCalendar.set(mYear, mMonth, day); + final CharSequence date = DateFormat.format(DATE_FORMAT, + mTempCalendar.getTimeInMillis()); + + if (day == mSelectedDay) { + return getContext().getString(R.string.item_is_selected, date); + } + + return date; + } + } + + /** + * Handles callbacks when the user clicks on a time object. + */ + public interface OnDayClickListener { + public void onDayClick(SimpleMonthView view, Calendar day); + } +} diff --git a/core/java/android/widget/TextViewWithCircularIndicator.java b/core/java/android/widget/TextViewWithCircularIndicator.java new file mode 100644 index 0000000000000..22d770c509473 --- /dev/null +++ b/core/java/android/widget/TextViewWithCircularIndicator.java @@ -0,0 +1,114 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.util.AttributeSet; + +import com.android.internal.R; + +class TextViewWithCircularIndicator extends TextView { + + private static final int SELECTED_CIRCLE_ALPHA = 60; + + private final Paint mCirclePaint = new Paint(); + + private final String mItemIsSelectedText; + private int mCircleColor; + private boolean mDrawIndicator; + + public TextViewWithCircularIndicator(Context context) { + this(context, null); + } + + public TextViewWithCircularIndicator(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TextViewWithCircularIndicator(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public TextViewWithCircularIndicator(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + + super(context, attrs); + Resources res = context.getResources(); + + // Use Theme attributes if possible + final TypedArray a = mContext.obtainStyledAttributes(attrs, + R.styleable.DatePicker, defStyleAttr, defStyleRes); + + final int resId = a.getResourceId( + R.styleable.DatePicker_dateSelectorYearListItemTextAppearance, -1); + if (resId != -1) { + setTextAppearance(context, resId); + } + + mItemIsSelectedText = res.getString(R.string.item_is_selected); + + a.recycle(); + + init(); + } + + private void init() { + mCirclePaint.setTypeface(Typeface.create(mCirclePaint.getTypeface(), Typeface.BOLD)); + mCirclePaint.setAntiAlias(true); + mCirclePaint.setTextAlign(Paint.Align.CENTER); + mCirclePaint.setStyle(Paint.Style.FILL); + } + + public void setCircleColor(int color) { + if (color != mCircleColor) { + mCircleColor = color; + mCirclePaint.setColor(mCircleColor); + mCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA); + requestLayout(); + } + } + + public void setDrawIndicator(boolean drawIndicator) { + mDrawIndicator = drawIndicator; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mDrawIndicator) { + final int width = getWidth(); + final int height = getHeight(); + int radius = Math.min(width, height) / 2; + canvas.drawCircle(width / 2, height / 2, radius, mCirclePaint); + } + } + + @Override + public CharSequence getContentDescription() { + CharSequence itemText = getText(); + if (mDrawIndicator) { + return String.format(mItemIsSelectedText, itemText); + } else { + return itemText; + } + } +} \ No newline at end of file diff --git a/core/java/android/widget/TimePickerDelegate.java b/core/java/android/widget/TimePickerDelegate.java index bf3971cbbf4ec..cd896673a65ab 100644 --- a/core/java/android/widget/TimePickerDelegate.java +++ b/core/java/android/widget/TimePickerDelegate.java @@ -1389,8 +1389,8 @@ class TimePickerDelegate extends TimePicker.AbstractTimePickerDelegate implement final Keyframe k2 = Keyframe.ofFloat(0.69f, increaseRatio); final Keyframe k3 = Keyframe.ofFloat(1f, 1f); - PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe("scaleX", k0, k1, k2, k3); - PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe("scaleY", k0, k1, k2, k3); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe(View.SCALE_X, k0, k1, k2, k3); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe(View.SCALE_Y, k0, k1, k2, k3); ObjectAnimator pulseAnimator = ObjectAnimator.ofPropertyValuesHolder(labelToAnimate, scaleX, scaleY); pulseAnimator.setDuration(PULSE_ANIMATOR_DURATION); diff --git a/core/java/android/widget/YearPickerView.java b/core/java/android/widget/YearPickerView.java new file mode 100644 index 0000000000000..bac93201fe96b --- /dev/null +++ b/core/java/android/widget/YearPickerView.java @@ -0,0 +1,200 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +import java.util.Calendar; + +import com.android.internal.R; + +/** + * Displays a selectable list of years. + */ +class YearPickerView extends ListView implements AdapterView.OnItemClickListener, + OnDateChangedListener { + private static final String TAG = "YearPickerView"; + + private DatePickerController mController; + private YearAdapter mAdapter; + private int mViewSize; + private int mChildSize; + private int mSelectedPosition = -1; + private int mYearSelectedCircleColor; + + public YearPickerView(Context context) { + this(context, null); + } + + public YearPickerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public YearPickerView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public YearPickerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + ViewGroup.LayoutParams frame = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + setLayoutParams(frame); + + Resources res = context.getResources(); + mViewSize = res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height); + mChildSize = res.getDimensionPixelOffset(R.dimen.datepicker_year_label_height); + + setVerticalFadingEdgeEnabled(true); + setFadingEdgeLength(mChildSize / 3); + + final int paddingTop = res.getDimensionPixelSize( + R.dimen.datepicker_year_picker_padding_top); + setPadding(0, paddingTop, 0, 0); + + // Use Theme attributes if possible + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.DatePicker, defStyleAttr, defStyleRes); + + final int colorResId = a.getResourceId( + R.styleable.DatePicker_dateSelectorYearListSelectedCircleColor, + R.color.datepicker_default_circle_background_color_holo_light); + mYearSelectedCircleColor = res.getColor(colorResId); + + a.recycle(); + + setOnItemClickListener(this); + setDividerHeight(0); + } + + public void init(DatePickerController controller) { + mController = controller; + mController.registerOnDateChangedListener(this); + + mAdapter = new YearAdapter(getContext(), R.layout.year_label_text_view); + updateAdapterData(); + setAdapter(mAdapter); + + onDateChanged(); + } + + public void setYearSelectedCircleColor(int color) { + if (color != mYearSelectedCircleColor) { + mYearSelectedCircleColor = color; + } + requestLayout(); + } + + public int getYearSelectedCircleColor() { + return mYearSelectedCircleColor; + } + + private void updateAdapterData() { + mAdapter.clear(); + final int maxYear = mController.getMaxYear(); + for (int year = mController.getMinYear(); year <= maxYear; year++) { + mAdapter.add(year); + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + mController.tryVibrate(); + if (position != mSelectedPosition) { + mSelectedPosition = position; + mAdapter.notifyDataSetChanged(); + } + mController.onYearSelected(mAdapter.getItem(position)); + } + + void setItemTextAppearance(int resId) { + mAdapter.setItemTextAppearance(resId); + } + + private class YearAdapter extends ArrayAdapter { + int mItemTextAppearanceResId; + + public YearAdapter(Context context, int resource) { + super(context, resource); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextViewWithCircularIndicator v = (TextViewWithCircularIndicator) + super.getView(position, convertView, parent); + v.setTextAppearance(getContext(), mItemTextAppearanceResId); + v.requestLayout(); + int year = getItem(position); + boolean selected = mController.getSelectedDay().get(Calendar.YEAR) == year; + v.setDrawIndicator(selected); + if (selected) { + v.setCircleColor(mYearSelectedCircleColor); + } + return v; + } + + public void setItemTextAppearance(int resId) { + mItemTextAppearanceResId = resId; + } + } + + public void postSetSelectionCentered(final int position) { + postSetSelectionFromTop(position, mViewSize / 2 - mChildSize / 2); + } + + public void postSetSelectionFromTop(final int position, final int offset) { + post(new Runnable() { + + @Override + public void run() { + setSelectionFromTop(position, offset); + requestLayout(); + } + }); + } + + public int getFirstPositionOffset() { + final View firstChild = getChildAt(0); + if (firstChild == null) { + return 0; + } + return firstChild.getTop(); + } + + @Override + public void onDateChanged() { + updateAdapterData(); + mAdapter.notifyDataSetChanged(); + postSetSelectionCentered( + mController.getSelectedDay().get(Calendar.YEAR) - mController.getMinYear()); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + event.setFromIndex(0); + event.setToIndex(0); + } + } +} \ No newline at end of file diff --git a/core/java/com/android/internal/widget/AccessibleDateAnimator.java b/core/java/com/android/internal/widget/AccessibleDateAnimator.java new file mode 100644 index 0000000000000..e91a55c9a55e3 --- /dev/null +++ b/core/java/com/android/internal/widget/AccessibleDateAnimator.java @@ -0,0 +1,56 @@ +/* + * 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.android.internal.widget; + +import android.content.Context; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ViewAnimator; + +/** + * @hide + */ +public class AccessibleDateAnimator extends ViewAnimator { + private long mDateMillis; + + public AccessibleDateAnimator(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setDateMillis(long dateMillis) { + mDateMillis = dateMillis; + } + + /** + * Announce the currently-selected date when launched. + */ + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + // Clear the event's current text so that only the current date will be spoken. + event.getText().clear(); + int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | + DateUtils.FORMAT_SHOW_WEEKDAY; + + String dateString = DateUtils.formatDateTime(getContext(), mDateMillis, flags); + event.getText().add(dateString); + return true; + } + return super.dispatchPopulateAccessibilityEvent(event); + } +} diff --git a/core/java/com/android/internal/widget/ExploreByTouchHelper.java b/core/java/com/android/internal/widget/ExploreByTouchHelper.java new file mode 100644 index 0000000000000..11c4ca1f65dd6 --- /dev/null +++ b/core/java/com/android/internal/widget/ExploreByTouchHelper.java @@ -0,0 +1,719 @@ +/* + * 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.android.internal.widget; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.accessibility.*; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +import java.util.LinkedList; +import java.util.List; + +/** + * ExploreByTouchHelper is a utility class for implementing accessibility + * support in custom {@link android.view.View}s that represent a collection of View-like + * logical items. It extends {@link android.view.accessibility.AccessibilityNodeProvider} and + * simplifies many aspects of providing information to accessibility services + * and managing accessibility focus. This class does not currently support + * hierarchies of logical items. + *

+ * This should be applied to the parent view using + * {@link android.view.View#setAccessibilityDelegate}: + * + *

+ * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback);
+ * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper);
+ * 
+ */ +public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { + /** Virtual node identifier value for invalid nodes. */ + public static final int INVALID_ID = Integer.MIN_VALUE; + + /** Default class name used for virtual views. */ + private static final String DEFAULT_CLASS_NAME = View.class.getName(); + + // Temporary, reusable data structures. + private final Rect mTempScreenRect = new Rect(); + private final Rect mTempParentRect = new Rect(); + private final Rect mTempVisibleRect = new Rect(); + private final int[] mTempGlobalRect = new int[2]; + + /** View's context **/ + private Context mContext; + + /** System accessibility manager, used to check state and send events. */ + private final AccessibilityManager mManager; + + /** View whose internal structure is exposed through this helper. */ + private final View mView; + + /** Node provider that handles creating nodes and performing actions. */ + private ExploreByTouchNodeProvider mNodeProvider; + + /** Virtual view id for the currently focused logical item. */ + private int mFocusedVirtualViewId = INVALID_ID; + + /** Virtual view id for the currently hovered logical item. */ + private int mHoveredVirtualViewId = INVALID_ID; + + /** + * Factory method to create a new {@link ExploreByTouchHelper}. + * + * @param forView View whose logical children are exposed by this helper. + */ + public ExploreByTouchHelper(View forView) { + if (forView == null) { + throw new IllegalArgumentException("View may not be null"); + } + + mView = forView; + mContext = forView.getContext(); + mManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + } + + /** + * Returns the {@link android.view.accessibility.AccessibilityNodeProvider} for this helper. + * + * @param host View whose logical children are exposed by this helper. + * @return The accessibility node provider for this helper. + */ + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) { + if (mNodeProvider == null) { + mNodeProvider = new ExploreByTouchNodeProvider(); + } + return mNodeProvider; + } + + /** + * Dispatches hover {@link android.view.MotionEvent}s to the virtual view hierarchy when + * the Explore by Touch feature is enabled. + *

+ * This method should be called by overriding + * {@link View#dispatchHoverEvent}: + * + *

@Override
+     * public boolean dispatchHoverEvent(MotionEvent event) {
+     *   if (mHelper.dispatchHoverEvent(this, event) {
+     *     return true;
+     *   }
+     *   return super.dispatchHoverEvent(event);
+     * }
+     * 
+ * + * @param event The hover event to dispatch to the virtual view hierarchy. + * @return Whether the hover event was handled. + */ + public boolean dispatchHoverEvent(MotionEvent event) { + if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_ENTER: + final int virtualViewId = getVirtualViewAt(event.getX(), event.getY()); + updateHoveredVirtualView(virtualViewId); + return (virtualViewId != INVALID_ID); + case MotionEvent.ACTION_HOVER_EXIT: + if (mFocusedVirtualViewId != INVALID_ID) { + updateHoveredVirtualView(INVALID_ID); + return true; + } + return false; + default: + return false; + } + } + + /** + * Populates an event of the specified type with information about an item + * and attempts to send it up through the view hierarchy. + *

+ * You should call this method after performing a user action that normally + * fires an accessibility event, such as clicking on an item. + * + *

public void performItemClick(T item) {
+     *   ...
+     *   sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED);
+     * }
+     * 
+ * + * @param virtualViewId The virtual view id for which to send an event. + * @param eventType The type of event to send. + * @return true if the event was sent successfully. + */ + public boolean sendEventForVirtualView(int virtualViewId, int eventType) { + if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) { + return false; + } + + final ViewParent parent = mView.getParent(); + if (parent == null) { + return false; + } + + final AccessibilityEvent event = createEvent(virtualViewId, eventType); + return parent.requestSendAccessibilityEvent(mView, event); + } + + /** + * Notifies the accessibility framework that the properties of the parent + * view have changed. + *

+ * You must call this method after adding or removing items from the + * parent view. + */ + public void invalidateRoot() { + invalidateVirtualView(View.NO_ID); + } + + /** + * Notifies the accessibility framework that the properties of a particular + * item have changed. + *

+ * You must call this method after changing any of the properties set + * in {@link #onPopulateNodeForVirtualView}. + * + * @param virtualViewId The virtual view id to invalidate. + */ + public void invalidateVirtualView(int virtualViewId) { + sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + + /** + * Returns the virtual view id for the currently focused item, + * + * @return A virtual view id, or {@link #INVALID_ID} if no item is + * currently focused. + */ + public int getFocusedVirtualView() { + return mFocusedVirtualViewId; + } + + /** + * Sets the currently hovered item, sending hover accessibility events as + * necessary to maintain the correct state. + * + * @param virtualViewId The virtual view id for the item currently being + * hovered, or {@link #INVALID_ID} if no item is hovered within + * the parent view. + */ + private void updateHoveredVirtualView(int virtualViewId) { + if (mHoveredVirtualViewId == virtualViewId) { + return; + } + + final int previousVirtualViewId = mHoveredVirtualViewId; + mHoveredVirtualViewId = virtualViewId; + + // Stay consistent with framework behavior by sending ENTER/EXIT pairs + // in reverse order. This is accurate as of API 18. + sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + + /** + * Constructs and returns an {@link AccessibilityEvent} for the specified + * virtual view id, which includes the host view ({@link View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEvent(int virtualViewId, int eventType) { + switch (virtualViewId) { + case View.NO_ID: + return createEventForHost(eventType); + default: + return createEventForChild(virtualViewId, eventType); + } + } + + /** + * Constructs and returns an {@link AccessibilityEvent} for the host node. + * + * @param eventType The type of event to construct. + * @return An {@link AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForHost(int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + onInitializeAccessibilityEvent(mView, event); + return event; + } + + /** + * Constructs and returns an {@link AccessibilityEvent} populated with + * information about the specified item. + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.setEnabled(true); + event.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the event. + onPopulateEventForVirtualView(virtualViewId, event); + + // Make sure the developer is following the rules. + if (event.getText().isEmpty() && (event.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateEventForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + event.setPackageName(mView.getContext().getPackageName()); + event.setSource(mView, virtualViewId); + + return event; + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the + * specified virtual view id, which includes the host view + * ({@link View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct a node. + * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information + * about the specified item. + */ + private AccessibilityNodeInfo createNode(int virtualViewId) { + switch (virtualViewId) { + case View.NO_ID: + return createNodeForHost(); + default: + return createNodeForChild(virtualViewId); + } + } + + /** + * Constructs and returns an {@link AccessibilityNodeInfo} for the + * host view populated with its virtual descendants. + * + * @return An {@link AccessibilityNodeInfo} for the parent node. + */ + private AccessibilityNodeInfo createNodeForHost() { + final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView); + onInitializeAccessibilityNodeInfo(mView, node); + + // Add the virtual descendants. + final LinkedList virtualViewIds = new LinkedList(); + getVisibleVirtualViews(virtualViewIds); + + for (Integer childVirtualViewId : virtualViewIds) { + node.addChild(mView, childVirtualViewId); + } + + return node; + } + + /** + * Constructs and returns an {@link AccessibilityNodeInfo} for the + * specified item. Automatically manages accessibility focus actions. + *

+ * Allows the implementing class to specify most node properties, but + * overrides the following: + *

    + *
  • {@link AccessibilityNodeInfo#setPackageName} + *
  • {@link AccessibilityNodeInfo#setClassName} + *
  • {@link AccessibilityNodeInfo#setParent(View)} + *
  • {@link AccessibilityNodeInfo#setSource(View, int)} + *
  • {@link AccessibilityNodeInfo#setVisibleToUser} + *
  • {@link AccessibilityNodeInfo#setBoundsInScreen(Rect)} + *
+ *

+ * Uses the bounds of the parent view and the parent-relative bounding + * rectangle specified by + * {@link AccessibilityNodeInfo#getBoundsInParent} to automatically + * update the following properties: + *

    + *
  • {@link AccessibilityNodeInfo#setVisibleToUser} + *
  • {@link AccessibilityNodeInfo#setBoundsInParent} + *
+ * + * @param virtualViewId The virtual view id for item for which to construct + * a node. + * @return An {@link AccessibilityNodeInfo} for the specified item. + */ + private AccessibilityNodeInfo createNodeForChild(int virtualViewId) { + final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(); + + // Ensure the client has good defaults. + node.setEnabled(true); + node.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the node. + onPopulateNodeForVirtualView(virtualViewId, node); + + // Make sure the developer is following the rules. + if ((node.getText() == null) && (node.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateNodeForVirtualViewId()"); + } + + node.getBoundsInParent(mTempParentRect); + if (mTempParentRect.isEmpty()) { + throw new RuntimeException("Callbacks must set parent bounds in " + + "populateNodeForVirtualViewId()"); + } + + final int actions = node.getActions(); + if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + node.setPackageName(mView.getContext().getPackageName()); + node.setSource(mView, virtualViewId); + node.setParent(mView); + + // Manage internal accessibility focus state. + if (mFocusedVirtualViewId == virtualViewId) { + node.setAccessibilityFocused(true); + node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } else { + node.setAccessibilityFocused(false); + node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + + // Set the visibility based on the parent bound. + if (intersectVisibleToUser(mTempParentRect)) { + node.setVisibleToUser(true); + node.setBoundsInParent(mTempParentRect); + } + + // Calculate screen-relative bound. + mView.getLocationOnScreen(mTempGlobalRect); + final int offsetX = mTempGlobalRect[0]; + final int offsetY = mTempGlobalRect[1]; + mTempScreenRect.set(mTempParentRect); + mTempScreenRect.offset(offsetX, offsetY); + node.setBoundsInScreen(mTempScreenRect); + + return node; + } + + private boolean performAction(int virtualViewId, int action, Bundle arguments) { + switch (virtualViewId) { + case View.NO_ID: + return performActionForHost(action, arguments); + default: + return performActionForChild(virtualViewId, action, arguments); + } + } + + private boolean performActionForHost(int action, Bundle arguments) { + return performAccessibilityAction(mView, action, arguments); + } + + private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return manageFocusForChild(virtualViewId, action, arguments); + default: + return onPerformActionForVirtualView(virtualViewId, action, arguments); + } + } + + private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + return requestAccessibilityFocus(virtualViewId); + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return clearAccessibilityFocus(virtualViewId); + default: + return false; + } + } + + /** + * Computes whether the specified {@link Rect} intersects with the visible + * portion of its parent {@link View}. Modifies {@code localRect} to contain + * only the visible portion. + * + * @param localRect A rectangle in local (parent) coordinates. + * @return Whether the specified {@link Rect} is visible on the screen. + */ + private boolean intersectVisibleToUser(Rect localRect) { + // Missing or empty bounds mean this view is not visible. + if ((localRect == null) || localRect.isEmpty()) { + return false; + } + + // Attached to invisible window means this view is not visible. + if (mView.getWindowVisibility() != View.VISIBLE) { + return false; + } + + // An invisible predecessor means that this view is not visible. + ViewParent viewParent = mView.getParent(); + while (viewParent instanceof View) { + final View view = (View) viewParent; + if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) { + return false; + } + viewParent = view.getParent(); + } + + // A null parent implies the view is not visible. + if (viewParent == null) { + return false; + } + + // If no portion of the parent is visible, this view is not visible. + if (!mView.getLocalVisibleRect(mTempVisibleRect)) { + return false; + } + + // Check if the view intersects the visible portion of the parent. + return localRect.intersect(mTempVisibleRect); + } + + /** + * Returns whether this virtual view is accessibility focused. + * + * @return True if the view is accessibility focused. + */ + private boolean isAccessibilityFocused(int virtualViewId) { + return (mFocusedVirtualViewId == virtualViewId); + } + + /** + * Attempts to give accessibility focus to a virtual view. + *

+ * A virtual view will not actually take focus if + * {@link AccessibilityManager#isEnabled()} returns false, + * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false, + * or the view already has accessibility focus. + * + * @param virtualViewId The id of the virtual view on which to place + * accessibility focus. + * @return Whether this virtual view actually took accessibility focus. + */ + private boolean requestAccessibilityFocus(int virtualViewId) { + final AccessibilityManager accessibilityManager = + (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + + if (!mManager.isEnabled() + || !accessibilityManager.isTouchExplorationEnabled()) { + return false; + } + // TODO: Check virtual view visibility. + if (!isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = virtualViewId; + // TODO: Only invalidate virtual view bounds. + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + return true; + } + return false; + } + + /** + * Attempts to clear accessibility focus from a virtual view. + * + * @param virtualViewId The id of the virtual view from which to clear + * accessibility focus. + * @return Whether this virtual view actually cleared accessibility focus. + */ + private boolean clearAccessibilityFocus(int virtualViewId) { + if (isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = INVALID_ID; + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + return true; + } + return false; + } + + /** + * Provides a mapping between view-relative coordinates and logical + * items. + * + * @param x The view-relative x coordinate + * @param y The view-relative y coordinate + * @return virtual view identifier for the logical item under + * coordinates (x,y) + */ + protected abstract int getVirtualViewAt(float x, float y); + + /** + * Populates a list with the view's visible items. The ordering of items + * within {@code virtualViewIds} specifies order of accessibility focus + * traversal. + * + * @param virtualViewIds The list to populate with visible items + */ + protected abstract void getVisibleVirtualViews(List virtualViewIds); + + /** + * Populates an {@link AccessibilityEvent} with information about the + * specified item. + *

+ * Implementations must populate the following required fields: + *

    + *
  • event text, see {@link AccessibilityEvent#getText} or + * {@link AccessibilityEvent#setContentDescription} + *
+ *

+ * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + *

    + *
  • item class name, set to android.view.View, see + * {@link AccessibilityEvent#setClassName} + *
+ *

+ * The following required fields are automatically populated by the + * helper class and may not be overridden: + *

    + *
  • package name, set to the package of the host view's + * {@link Context}, see {@link AccessibilityEvent#setPackageName} + *
  • event source, set to the host view and virtual view identifier, + * see {@link AccessibilityRecord#setSource(View, int)} + *
+ * + * @param virtualViewId The virtual view id for the item for which to + * populate the event + * @param event The event to populate + */ + protected abstract void onPopulateEventForVirtualView( + int virtualViewId, AccessibilityEvent event); + + /** + * Populates an {@link AccessibilityNodeInfo} with information + * about the specified item. + *

+ * Implementations must populate the following required fields: + *

    + *
  • event text, see {@link AccessibilityNodeInfo#setText} or + * {@link AccessibilityNodeInfo#setContentDescription} + *
  • bounds in parent coordinates, see + * {@link AccessibilityNodeInfo#setBoundsInParent} + *
+ *

+ * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + *

    + *
  • enabled state, set to true, see + * {@link AccessibilityNodeInfo#setEnabled} + *
  • item class name, identical to the class name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link AccessibilityNodeInfo#setClassName} + *
+ *

+ * The following required fields are automatically populated by the + * helper class and may not be overridden: + *

    + *
  • package name, identical to the package name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link AccessibilityNodeInfo#setPackageName} + *
  • node source, identical to the event source set in + * {@link #onPopulateEventForVirtualView}, see + * {@link AccessibilityNodeInfo#setSource(View, int)} + *
  • parent view, set to the host view, see + * {@link AccessibilityNodeInfo#setParent(View)} + *
  • visibility, computed based on parent-relative bounds, see + * {@link AccessibilityNodeInfo#setVisibleToUser} + *
  • accessibility focus, computed based on internal helper state, see + * {@link AccessibilityNodeInfo#setAccessibilityFocused} + *
  • bounds in screen coordinates, computed based on host view bounds, + * see {@link AccessibilityNodeInfo#setBoundsInScreen} + *
+ *

+ * Additionally, the helper class automatically handles accessibility + * focus management by adding the appropriate + * {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or + * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * action. Implementations must never manually add these actions. + *

+ * The helper class also automatically modifies parent- and + * screen-relative bounds to reflect the portion of the item visible + * within its parent. + * + * @param virtualViewId The virtual view identifier of the item for + * which to populate the node + * @param node The node to populate + */ + protected abstract void onPopulateNodeForVirtualView( + int virtualViewId, AccessibilityNodeInfo node); + + /** + * Performs the specified accessibility action on the item associated + * with the virtual view identifier. See + * {@link AccessibilityNodeInfo#performAction(int, Bundle)} for + * more information. + *

+ * Implementations must handle any actions added manually in + * {@link #onPopulateNodeForVirtualView}. + *

+ * The helper class automatically handles focus management resulting + * from {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} + * and + * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * actions. + * + * @param virtualViewId The virtual view identifier of the item on which + * to perform the action + * @param action The accessibility action to perform + * @param arguments (Optional) A bundle with additional arguments, or + * null + * @return true if the action was performed + */ + protected abstract boolean onPerformActionForVirtualView( + int virtualViewId, int action, Bundle arguments); + + /** + * Exposes a virtual view hierarchy to the accessibility framework. Only + * used in API 16+. + */ + private class ExploreByTouchNodeProvider extends AccessibilityNodeProvider { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return ExploreByTouchHelper.this.createNode(virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments); + } + } +} diff --git a/core/res/res/color/date_picker_calendar_holo_dark.xml b/core/res/res/color/date_picker_calendar_holo_dark.xml new file mode 100644 index 0000000000000..d29486f573a55 --- /dev/null +++ b/core/res/res/color/date_picker_calendar_holo_dark.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_calendar_holo_light.xml b/core/res/res/color/date_picker_calendar_holo_light.xml new file mode 100644 index 0000000000000..776f39be0e76b --- /dev/null +++ b/core/res/res/color/date_picker_calendar_holo_light.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_calendar_material_dark.xml b/core/res/res/color/date_picker_calendar_material_dark.xml new file mode 100644 index 0000000000000..86a067343f2de --- /dev/null +++ b/core/res/res/color/date_picker_calendar_material_dark.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_calendar_material_light.xml b/core/res/res/color/date_picker_calendar_material_light.xml new file mode 100644 index 0000000000000..015eed7f71bf2 --- /dev/null +++ b/core/res/res/color/date_picker_calendar_material_light.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_selector_holo_dark.xml b/core/res/res/color/date_picker_selector_holo_dark.xml new file mode 100644 index 0000000000000..9e5a5bdf4a778 --- /dev/null +++ b/core/res/res/color/date_picker_selector_holo_dark.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_selector_holo_light.xml b/core/res/res/color/date_picker_selector_holo_light.xml new file mode 100644 index 0000000000000..bf8667c245de2 --- /dev/null +++ b/core/res/res/color/date_picker_selector_holo_light.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_selector_material_dark.xml b/core/res/res/color/date_picker_selector_material_dark.xml new file mode 100644 index 0000000000000..e4073873635c4 --- /dev/null +++ b/core/res/res/color/date_picker_selector_material_dark.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_selector_material_light.xml b/core/res/res/color/date_picker_selector_material_light.xml new file mode 100644 index 0000000000000..b4c6a4741aecf --- /dev/null +++ b/core/res/res/color/date_picker_selector_material_light.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_year_selector_holo_dark.xml b/core/res/res/color/date_picker_year_selector_holo_dark.xml new file mode 100644 index 0000000000000..ce519b2812ee2 --- /dev/null +++ b/core/res/res/color/date_picker_year_selector_holo_dark.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_year_selector_holo_light.xml b/core/res/res/color/date_picker_year_selector_holo_light.xml new file mode 100644 index 0000000000000..c228711bcb25d --- /dev/null +++ b/core/res/res/color/date_picker_year_selector_holo_light.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_year_selector_material_dark.xml b/core/res/res/color/date_picker_year_selector_material_dark.xml new file mode 100644 index 0000000000000..b5ff09af9974b --- /dev/null +++ b/core/res/res/color/date_picker_year_selector_material_dark.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/res/color/date_picker_year_selector_material_light.xml b/core/res/res/color/date_picker_year_selector_material_light.xml new file mode 100644 index 0000000000000..5e329b3578ddc --- /dev/null +++ b/core/res/res/color/date_picker_year_selector_material_light.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/res/layout-land/date_picker_holo.xml b/core/res/res/layout-land/date_picker_holo.xml new file mode 100644 index 0000000000000..98e26ca4a1014 --- /dev/null +++ b/core/res/res/layout-land/date_picker_holo.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/layout-sw600dp/date_picker_holo.xml b/core/res/res/layout-sw600dp/date_picker_holo.xml new file mode 100644 index 0000000000000..847aa3226ca1b --- /dev/null +++ b/core/res/res/layout-sw600dp/date_picker_holo.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/layout/date_picker_done_button.xml b/core/res/res/layout/date_picker_done_button.xml new file mode 100644 index 0000000000000..b8e8c03b64f88 --- /dev/null +++ b/core/res/res/layout/date_picker_done_button.xml @@ -0,0 +1,33 @@ + + + + +