summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget/SimpleMonthView.java
diff options
context:
space:
mode:
authorFabrice Di Meglio <fdimeglio@google.com>2013-10-01 11:21:31 -0700
committerFabrice Di Meglio <fdimeglio@google.com>2014-07-15 20:26:21 +0000
commitbd9152f6ee156ee473f05f6f05f238605996fca4 (patch)
tree57ccd34b9e185f0a2ecf2ad8e56933b3db65a808 /core/java/android/widget/SimpleMonthView.java
parentd8176941eb6466ebe26816d79b37a808103fd81d (diff)
downloadframeworks_base-bd9152f6ee156ee473f05f6f05f238605996fca4.zip
frameworks_base-bd9152f6ee156ee473f05f6f05f238605996fca4.tar.gz
frameworks_base-bd9152f6ee156ee473f05f6f05f238605996fca4.tar.bz2
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
Diffstat (limited to 'core/java/android/widget/SimpleMonthView.java')
-rw-r--r--core/java/android/widget/SimpleMonthView.java720
1 files changed, 720 insertions, 0 deletions
diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java
new file mode 100644
index 0000000..7589711
--- /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<String, Integer> 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<Integer> 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);
+ }
+}