MonthView.java revision 5cfe197091de6e7b5550fc2503e00fe554bef5cd
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.datetimepicker.date;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.Paint.Align;
24import android.graphics.Paint.Style;
25import android.graphics.Rect;
26import android.graphics.Typeface;
27import android.os.Bundle;
28import android.support.v4.view.ViewCompat;
29import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
30import android.support.v4.widget.ExploreByTouchHelper;
31import android.text.format.DateFormat;
32import android.text.format.DateUtils;
33import android.text.format.Time;
34import android.view.MotionEvent;
35import android.view.View;
36import android.view.accessibility.AccessibilityEvent;
37import android.view.accessibility.AccessibilityNodeInfo;
38
39import com.android.datetimepicker.R;
40import com.android.datetimepicker.Utils;
41import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
42
43import java.security.InvalidParameterException;
44import java.util.Calendar;
45import java.util.Formatter;
46import java.util.HashMap;
47import java.util.List;
48import java.util.Locale;
49
50/**
51 * A calendar-like view displaying a specified month and the appropriate selectable day numbers
52 * within the specified month.
53 */
54public abstract class MonthView extends View {
55    private static final String TAG = "MonthView";
56
57    /**
58     * These params can be passed into the view to control how it appears.
59     * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default
60     * values are unlikely to fit most layouts correctly.
61     */
62    /**
63     * This sets the height of this week in pixels
64     */
65    public static final String VIEW_PARAMS_HEIGHT = "height";
66    /**
67     * This specifies the position (or weeks since the epoch) of this week,
68     * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
69     */
70    public static final String VIEW_PARAMS_MONTH = "month";
71    /**
72     * This specifies the position (or weeks since the epoch) of this week,
73     * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
74     */
75    public static final String VIEW_PARAMS_YEAR = "year";
76    /**
77     * This sets one of the days in this view as selected {@link Time#SUNDAY}
78     * through {@link Time#SATURDAY}.
79     */
80    public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day";
81    /**
82     * Which day the week should start on. {@link Time#SUNDAY} through
83     * {@link Time#SATURDAY}.
84     */
85    public static final String VIEW_PARAMS_WEEK_START = "week_start";
86    /**
87     * How many days to display at a time. Days will be displayed starting with
88     * {@link #mWeekStart}.
89     */
90    public static final String VIEW_PARAMS_NUM_DAYS = "num_days";
91    /**
92     * Which month is currently in focus, as defined by {@link Time#month}
93     * [0-11].
94     */
95    public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month";
96    /**
97     * If this month should display week numbers. false if 0, true otherwise.
98     */
99    public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num";
100
101    protected static int DEFAULT_HEIGHT = 32;
102    protected static int MIN_HEIGHT = 10;
103    protected static final int DEFAULT_SELECTED_DAY = -1;
104    protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
105    protected static final int DEFAULT_NUM_DAYS = 7;
106    protected static final int DEFAULT_SHOW_WK_NUM = 0;
107    protected static final int DEFAULT_FOCUS_MONTH = -1;
108    protected static final int DEFAULT_NUM_ROWS = 6;
109    protected static final int MAX_NUM_ROWS = 6;
110
111    private static final int SELECTED_CIRCLE_ALPHA = 60;
112
113    protected static int DAY_SEPARATOR_WIDTH = 1;
114    protected static int MINI_DAY_NUMBER_TEXT_SIZE;
115    protected static int MONTH_LABEL_TEXT_SIZE;
116    protected static int MONTH_DAY_LABEL_TEXT_SIZE;
117    protected static int MONTH_HEADER_SIZE;
118    protected static int DAY_SELECTED_CIRCLE_SIZE;
119
120    // used for scaling to the device density
121    protected static float mScale = 0;
122
123    // affects the padding on the sides of this view
124    protected int mPadding = 0;
125
126    private String mDayOfWeekTypeface;
127    private String mMonthTitleTypeface;
128
129    protected Paint mMonthNumPaint;
130    protected Paint mMonthTitlePaint;
131    protected Paint mMonthTitleBGPaint;
132    protected Paint mSelectedCirclePaint;
133    protected Paint mMonthDayLabelPaint;
134
135    private final Formatter mFormatter;
136    private final StringBuilder mStringBuilder;
137
138    // The Julian day of the first day displayed by this item
139    protected int mFirstJulianDay = -1;
140    // The month of the first day in this week
141    protected int mFirstMonth = -1;
142    // The month of the last day in this week
143    protected int mLastMonth = -1;
144
145    protected int mMonth;
146
147    protected int mYear;
148    // Quick reference to the width of this view, matches parent
149    protected int mWidth;
150    // The height this view should draw at in pixels, set by height param
151    protected int mRowHeight = DEFAULT_HEIGHT;
152    // If this view contains the today
153    protected boolean mHasToday = false;
154    // Which day is selected [0-6] or -1 if no day is selected
155    protected int mSelectedDay = -1;
156    // Which day is today [0-6] or -1 if no day is today
157    protected int mToday = DEFAULT_SELECTED_DAY;
158    // Which day of the week to start on [0-6]
159    protected int mWeekStart = DEFAULT_WEEK_START;
160    // How many days to display
161    protected int mNumDays = DEFAULT_NUM_DAYS;
162    // The number of days + a spot for week number if it is displayed
163    protected int mNumCells = mNumDays;
164    // The left edge of the selected day
165    protected int mSelectedLeft = -1;
166    // The right edge of the selected day
167    protected int mSelectedRight = -1;
168
169    private final Calendar mCalendar;
170    private final Calendar mDayLabelCalendar;
171    private final MonthViewTouchHelper mTouchHelper;
172
173    private int mNumRows = DEFAULT_NUM_ROWS;
174
175    // Optional listener for handling day click actions
176    private OnDayClickListener mOnDayClickListener;
177    // Whether to prevent setting the accessibility delegate
178    private boolean mLockAccessibilityDelegate;
179
180    protected int mDayTextColor;
181    protected int mTodayNumberColor;
182    protected int mMonthTitleColor;
183    protected int mMonthTitleBGColor;
184
185    public MonthView(Context context) {
186        super(context);
187
188        Resources res = context.getResources();
189
190        mDayLabelCalendar = Calendar.getInstance();
191        mCalendar = Calendar.getInstance();
192
193        mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
194        mMonthTitleTypeface = res.getString(R.string.sans_serif);
195
196        mDayTextColor = res.getColor(R.color.date_picker_text_normal);
197        mTodayNumberColor = res.getColor(R.color.blue);
198        mMonthTitleColor = res.getColor(R.color.white);
199        mMonthTitleBGColor = res.getColor(R.color.circle_background);
200
201        mStringBuilder = new StringBuilder(50);
202        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
203
204        MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size);
205        MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size);
206        MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size);
207        MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height);
208        DAY_SELECTED_CIRCLE_SIZE = res
209                .getDimensionPixelSize(R.dimen.day_number_select_circle_radius);
210
211        mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height)
212                - MONTH_HEADER_SIZE) / MAX_NUM_ROWS;
213
214        // Set up accessibility components.
215        mTouchHelper = new MonthViewTouchHelper(this);
216        ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
217        ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
218        mLockAccessibilityDelegate = true;
219
220        // Sets up any standard paints that will be used
221        initView();
222    }
223
224    @Override
225    public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
226        // Workaround for a JB MR1 issue where accessibility delegates on
227        // top-level ListView items are overwritten.
228        if (!mLockAccessibilityDelegate) {
229            super.setAccessibilityDelegate(delegate);
230        }
231    }
232
233    public void setOnDayClickListener(OnDayClickListener listener) {
234        mOnDayClickListener = listener;
235    }
236
237    @Override
238    public boolean dispatchHoverEvent(MotionEvent event) {
239        // First right-of-refusal goes the touch exploration helper.
240        if (mTouchHelper.dispatchHoverEvent(event)) {
241            return true;
242        }
243        return super.dispatchHoverEvent(event);
244    }
245
246    @Override
247    public boolean onTouchEvent(MotionEvent event) {
248        switch (event.getAction()) {
249            case MotionEvent.ACTION_UP:
250                final int day = getDayFromLocation(event.getX(), event.getY());
251                if (day >= 0) {
252                    onDayClick(day);
253                }
254                break;
255        }
256        return true;
257    }
258
259    /**
260     * Sets up the text and style properties for painting. Override this if you
261     * want to use a different paint.
262     */
263    protected void initView() {
264        mMonthTitlePaint = new Paint();
265        mMonthTitlePaint.setFakeBoldText(true);
266        mMonthTitlePaint.setAntiAlias(true);
267        mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
268        mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
269        mMonthTitlePaint.setColor(mDayTextColor);
270        mMonthTitlePaint.setTextAlign(Align.CENTER);
271        mMonthTitlePaint.setStyle(Style.FILL);
272
273        mMonthTitleBGPaint = new Paint();
274        mMonthTitleBGPaint.setFakeBoldText(true);
275        mMonthTitleBGPaint.setAntiAlias(true);
276        mMonthTitleBGPaint.setColor(mMonthTitleBGColor);
277        mMonthTitleBGPaint.setTextAlign(Align.CENTER);
278        mMonthTitleBGPaint.setStyle(Style.FILL);
279
280        mSelectedCirclePaint = new Paint();
281        mSelectedCirclePaint.setFakeBoldText(true);
282        mSelectedCirclePaint.setAntiAlias(true);
283        mSelectedCirclePaint.setColor(mTodayNumberColor);
284        mSelectedCirclePaint.setTextAlign(Align.CENTER);
285        mSelectedCirclePaint.setStyle(Style.FILL);
286        mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
287
288        mMonthDayLabelPaint = new Paint();
289        mMonthDayLabelPaint.setAntiAlias(true);
290        mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
291        mMonthDayLabelPaint.setColor(mDayTextColor);
292        mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
293        mMonthDayLabelPaint.setStyle(Style.FILL);
294        mMonthDayLabelPaint.setTextAlign(Align.CENTER);
295        mMonthDayLabelPaint.setFakeBoldText(true);
296
297        mMonthNumPaint = new Paint();
298        mMonthNumPaint.setAntiAlias(true);
299        mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
300        mMonthNumPaint.setStyle(Style.FILL);
301        mMonthNumPaint.setTextAlign(Align.CENTER);
302        mMonthNumPaint.setFakeBoldText(false);
303    }
304
305    @Override
306    protected void onDraw(Canvas canvas) {
307        drawMonthTitle(canvas);
308        drawMonthDayLabels(canvas);
309        drawMonthNums(canvas);
310    }
311
312    private int mDayOfWeekStart = 0;
313
314    /**
315     * Sets all the parameters for displaying this week. The only required
316     * parameter is the week number. Other parameters have a default value and
317     * will only update if a new value is included, except for focus month,
318     * which will always default to no focus month if no value is passed in. See
319     * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters.
320     *
321     * @param params A map of the new parameters, see
322     *            {@link #VIEW_PARAMS_HEIGHT}
323     */
324    public void setMonthParams(HashMap<String, Integer> params) {
325        if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) {
326            throw new InvalidParameterException("You must specify month and year for this view");
327        }
328        setTag(params);
329        // We keep the current value for any params not present
330        if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
331            mRowHeight = params.get(VIEW_PARAMS_HEIGHT);
332            if (mRowHeight < MIN_HEIGHT) {
333                mRowHeight = MIN_HEIGHT;
334            }
335        }
336        if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
337            mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
338        }
339
340        // Allocate space for caching the day numbers and focus values
341        mMonth = params.get(VIEW_PARAMS_MONTH);
342        mYear = params.get(VIEW_PARAMS_YEAR);
343
344        // Figure out what day today is
345        final Time today = new Time(Time.getCurrentTimezone());
346        today.setToNow();
347        mHasToday = false;
348        mToday = -1;
349
350        mCalendar.set(Calendar.MONTH, mMonth);
351        mCalendar.set(Calendar.YEAR, mYear);
352        mCalendar.set(Calendar.DAY_OF_MONTH, 1);
353        mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
354
355        if (params.containsKey(VIEW_PARAMS_WEEK_START)) {
356            mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
357        } else {
358            mWeekStart = mCalendar.getFirstDayOfWeek();
359        }
360
361        mNumCells = Utils.getDaysInMonth(mMonth, mYear);
362        for (int i = 0; i < mNumCells; i++) {
363            final int day = i + 1;
364            if (sameDay(day, today)) {
365                mHasToday = true;
366                mToday = day;
367            }
368        }
369        mNumRows = calculateNumRows();
370
371        // Invalidate cached accessibility information.
372        mTouchHelper.invalidateRoot();
373    }
374
375    public void setSelectedDay(int day) {
376        mSelectedDay = day;
377    }
378
379    public void reuse() {
380        mNumRows = DEFAULT_NUM_ROWS;
381        requestLayout();
382    }
383
384    private int calculateNumRows() {
385        int offset = findDayOffset();
386        int dividend = (offset + mNumCells) / mNumDays;
387        int remainder = (offset + mNumCells) % mNumDays;
388        return (dividend + (remainder > 0 ? 1 : 0));
389    }
390
391    private boolean sameDay(int day, Time today) {
392        return mYear == today.year &&
393                mMonth == today.month &&
394                day == today.monthDay;
395    }
396
397    @Override
398    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
399        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
400                + MONTH_HEADER_SIZE);
401    }
402
403    @Override
404    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
405        mWidth = w;
406
407        // Invalidate cached accessibility information.
408        mTouchHelper.invalidateRoot();
409    }
410
411    public int getMonth() {
412        return mMonth;
413    }
414
415    public int getYear() {
416        return mYear;
417    }
418
419    private String getMonthAndYearString() {
420        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
421                | DateUtils.FORMAT_NO_MONTH_DAY;
422        mStringBuilder.setLength(0);
423        long millis = mCalendar.getTimeInMillis();
424        return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
425                Time.getCurrentTimezone()).toString();
426    }
427
428    private void drawMonthTitle(Canvas canvas) {
429        int x = (mWidth + 2 * mPadding) / 2;
430        int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3);
431        canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
432    }
433
434    private void drawMonthDayLabels(Canvas canvas) {
435        int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
436        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
437
438        for (int i = 0; i < mNumDays; i++) {
439            int calendarDay = (i + mWeekStart) % mNumDays;
440            int x = (2 * i + 1) * dayWidthHalf + mPadding;
441            mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
442            canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
443                    Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
444                    mMonthDayLabelPaint);
445        }
446    }
447
448    /**
449     * Draws the week and month day numbers for this week. Override this method
450     * if you need different placement.
451     *
452     * @param canvas The canvas to draw on
453     */
454    protected void drawMonthNums(Canvas canvas) {
455        int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
456                + MONTH_HEADER_SIZE;
457        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
458        int j = findDayOffset();
459        for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
460            int x = (2 * j + 1) * dayWidthHalf + mPadding;
461
462            int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH;
463
464            int startX = x - dayWidthHalf;
465            int stopX = x + dayWidthHalf;
466            int startY = y - yRelativeToDay;
467            int stopY = startY + mRowHeight;
468
469            drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY);
470
471            j++;
472            if (j == mNumDays) {
473                j = 0;
474                y += mRowHeight;
475            }
476        }
477    }
478
479    /**
480     * This method should draw the month day.  Implemented by sub-classes to allow customization.
481     *
482     * @param canvas  The canvas to draw on
483     * @param year  The year of this month day
484     * @param month  The month of this month day
485     * @param day  The day number of this month day
486     * @param x  The default x position to draw the day number
487     * @param y  The default y position to draw the day number
488     * @param startX  The left boundary of the day number rect
489     * @param stopX  The right boundary of the day number rect
490     * @param startY  The top boundary of the day number rect
491     * @param stopY  The bottom boundary of the day number rect
492     */
493    public abstract void drawMonthDay(Canvas canvas, int year, int month, int day,
494            int x, int y, int startX, int stopX, int startY, int stopY);
495
496    private int findDayOffset() {
497        return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
498                - mWeekStart;
499    }
500
501
502    /**
503     * Calculates the day that the given x position is in, accounting for week
504     * number. Returns the day or -1 if the position wasn't in a day.
505     *
506     * @param x The x position of the touch event
507     * @return The day number, or -1 if the position wasn't in a day
508     */
509    public int getDayFromLocation(float x, float y) {
510        int dayStart = mPadding;
511        if (x < dayStart || x > mWidth - mPadding) {
512            return -1;
513        }
514        // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
515        int row = (int) (y - MONTH_HEADER_SIZE) / mRowHeight;
516        int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
517
518        int day = column - findDayOffset() + 1;
519        day += row * mNumDays;
520        if (day < 1 || day > mNumCells) {
521            return -1;
522        }
523        return day;
524    }
525
526    /**
527     * Called when the user clicks on a day. Handles callbacks to the
528     * {@link OnDayClickListener} if one is set.
529     *
530     * @param day The day that was clicked
531     */
532    private void onDayClick(int day) {
533        if (mOnDayClickListener != null) {
534            mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day));
535        }
536
537        // This is a no-op if accessibility is turned off.
538        mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
539    }
540
541    /**
542     * @return The date that has accessibility focus, or {@code null} if no date
543     *         has focus
544     */
545    public CalendarDay getAccessibilityFocus() {
546        final int day = mTouchHelper.getFocusedVirtualView();
547        if (day >= 0) {
548            return new CalendarDay(mYear, mMonth, day);
549        }
550        return null;
551    }
552
553    /**
554     * Clears accessibility focus within the view. No-op if the view does not
555     * contain accessibility focus.
556     */
557    public void clearAccessibilityFocus() {
558        mTouchHelper.clearFocusedVirtualView();
559    }
560
561    /**
562     * Attempts to restore accessibility focus to the specified date.
563     *
564     * @param day The date which should receive focus
565     * @return {@code false} if the date is not valid for this month view, or
566     *         {@code true} if the date received focus
567     */
568    public boolean restoreAccessibilityFocus(CalendarDay day) {
569        if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) {
570            return false;
571        }
572        mTouchHelper.setFocusedVirtualView(day.day);
573        return true;
574    }
575
576    /**
577     * Provides a virtual view hierarchy for interfacing with an accessibility
578     * service.
579     */
580    private class MonthViewTouchHelper extends ExploreByTouchHelper {
581        private static final String DATE_FORMAT = "dd MMMM yyyy";
582
583        private final Rect mTempRect = new Rect();
584        private final Calendar mTempCalendar = Calendar.getInstance();
585
586        public MonthViewTouchHelper(View host) {
587            super(host);
588        }
589
590        public void setFocusedVirtualView(int virtualViewId) {
591            getAccessibilityNodeProvider(MonthView.this).performAction(
592                    virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
593        }
594
595        public void clearFocusedVirtualView() {
596            final int focusedVirtualView = getFocusedVirtualView();
597            if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
598                getAccessibilityNodeProvider(MonthView.this).performAction(
599                        focusedVirtualView,
600                        AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
601                        null);
602            }
603        }
604
605        @Override
606        protected int getVirtualViewAt(float x, float y) {
607            final int day = getDayFromLocation(x, y);
608            if (day >= 0) {
609                return day;
610            }
611            return ExploreByTouchHelper.INVALID_ID;
612        }
613
614        @Override
615        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
616            for (int day = 1; day <= mNumCells; day++) {
617                virtualViewIds.add(day);
618            }
619        }
620
621        @Override
622        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
623            event.setContentDescription(getItemDescription(virtualViewId));
624        }
625
626        @Override
627        protected void onPopulateNodeForVirtualView(int virtualViewId,
628                AccessibilityNodeInfoCompat node) {
629            getItemBounds(virtualViewId, mTempRect);
630
631            node.setContentDescription(getItemDescription(virtualViewId));
632            node.setBoundsInParent(mTempRect);
633            node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
634
635            if (virtualViewId == mSelectedDay) {
636                node.setSelected(true);
637            }
638
639        }
640
641        @Override
642        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
643                Bundle arguments) {
644            switch (action) {
645                case AccessibilityNodeInfo.ACTION_CLICK:
646                    onDayClick(virtualViewId);
647                    return true;
648            }
649
650            return false;
651        }
652
653        /**
654         * Calculates the bounding rectangle of a given time object.
655         *
656         * @param day The day to calculate bounds for
657         * @param rect The rectangle in which to store the bounds
658         */
659        private void getItemBounds(int day, Rect rect) {
660            final int offsetX = mPadding;
661            final int offsetY = MONTH_HEADER_SIZE;
662            final int cellHeight = mRowHeight;
663            final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays);
664            final int index = ((day - 1) + findDayOffset());
665            final int row = (index / mNumDays);
666            final int column = (index % mNumDays);
667            final int x = (offsetX + (column * cellWidth));
668            final int y = (offsetY + (row * cellHeight));
669
670            rect.set(x, y, (x + cellWidth), (y + cellHeight));
671        }
672
673        /**
674         * Generates a description for a given time object. Since this
675         * description will be spoken, the components are ordered by descending
676         * specificity as DAY MONTH YEAR.
677         *
678         * @param day The day to generate a description for
679         * @return A description of the time object
680         */
681        private CharSequence getItemDescription(int day) {
682            mTempCalendar.set(mYear, mMonth, day);
683            final CharSequence date = DateFormat.format(DATE_FORMAT,
684                    mTempCalendar.getTimeInMillis());
685
686            if (day == mSelectedDay) {
687                return getContext().getString(R.string.item_is_selected, date);
688            }
689
690            return date;
691        }
692    }
693
694    /**
695     * Handles callbacks when the user clicks on a time object.
696     */
697    public interface OnDayClickListener {
698        public void onDayClick(MonthView view, CalendarDay day);
699    }
700}
701