SimpleMonthView.java revision 0ef59ac0e57e9b99d174d4a53f7d9639357743ac
1/*
2 * Copyright (C) 2014 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 android.widget;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Paint.Align;
26import android.graphics.Paint.Style;
27import android.graphics.Rect;
28import android.graphics.Typeface;
29import android.os.Bundle;
30import android.text.TextPaint;
31import android.text.format.DateFormat;
32import android.util.AttributeSet;
33import android.util.IntArray;
34import android.util.StateSet;
35import android.view.MotionEvent;
36import android.view.View;
37import android.view.accessibility.AccessibilityEvent;
38import android.view.accessibility.AccessibilityNodeInfo;
39import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
40
41import com.android.internal.R;
42import com.android.internal.widget.ExploreByTouchHelper;
43
44import java.text.SimpleDateFormat;
45import java.util.Calendar;
46import java.util.Locale;
47
48/**
49 * A calendar-like view displaying a specified month and the appropriate selectable day numbers
50 * within the specified month.
51 */
52class SimpleMonthView extends View {
53    private static final int DAYS_IN_WEEK = 7;
54    private static final int MAX_WEEKS_IN_MONTH = 6;
55
56    private static final int DEFAULT_SELECTED_DAY = -1;
57    private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
58
59    private static final String DEFAULT_TITLE_FORMAT = "MMMMy";
60    private static final String DAY_OF_WEEK_FORMAT = "EEEEE";
61
62    private final TextPaint mMonthPaint = new TextPaint();
63    private final TextPaint mDayOfWeekPaint = new TextPaint();
64    private final TextPaint mDayPaint = new TextPaint();
65    private final Paint mDaySelectorPaint = new Paint();
66    private final Paint mDayHighlightPaint = new Paint();
67
68    private final Calendar mCalendar = Calendar.getInstance();
69    private final Calendar mDayLabelCalendar = Calendar.getInstance();
70
71    private final MonthViewTouchHelper mTouchHelper;
72
73    private final SimpleDateFormat mTitleFormatter;
74    private final SimpleDateFormat mDayOfWeekFormatter;
75
76    private CharSequence mTitle;
77
78    private int mMonth;
79    private int mYear;
80
81    private int mPaddedWidth;
82    private int mPaddedHeight;
83
84    private final int mMonthHeight;
85    private final int mDayOfWeekHeight;
86    private final int mDayHeight;
87    private final int mCellWidth;
88    private final int mDaySelectorRadius;
89
90    /** The day of month for the selected day, or -1 if no day is selected. */
91    private int mActivatedDay = -1;
92
93    /**
94     * The day of month for today, or -1 if the today is not in the current
95     * month.
96     */
97    private int mToday = DEFAULT_SELECTED_DAY;
98
99    /** The first day of the week (ex. Calendar.SUNDAY). */
100    private int mWeekStart = DEFAULT_WEEK_START;
101
102    /** The number of days (ex. 28) in the current month. */
103    private int mDaysInMonth;
104
105    /**
106     * The day of week (ex. Calendar.SUNDAY) for the first day of the current
107     * month.
108     */
109    private int mDayOfWeekStart;
110
111    /** The day of month for the first (inclusive) enabled day. */
112    private int mEnabledDayStart = 1;
113
114    /** The day of month for the last (inclusive) enabled day. */
115    private int mEnabledDayEnd = 31;
116
117    /** The number of week rows needed to display the current month. */
118    private int mNumWeeks = MAX_WEEKS_IN_MONTH;
119
120    /** Optional listener for handling day click actions. */
121    private OnDayClickListener mOnDayClickListener;
122
123    private ColorStateList mDayTextColor;
124
125    private int mTouchedDay = -1;
126
127    public SimpleMonthView(Context context) {
128        this(context, null);
129    }
130
131    public SimpleMonthView(Context context, AttributeSet attrs) {
132        this(context, attrs, R.attr.datePickerStyle);
133    }
134
135    public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
136        this(context, attrs, defStyleAttr, 0);
137    }
138
139    public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
140        super(context, attrs, defStyleAttr, defStyleRes);
141
142        final Resources res = context.getResources();
143        mMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
144        mDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
145        mDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
146        mCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
147        mDaySelectorRadius = res.getDimensionPixelSize(R.dimen.date_picker_day_selector_radius);
148
149        // Set up accessibility components.
150        mTouchHelper = new MonthViewTouchHelper(this);
151        setAccessibilityDelegate(mTouchHelper);
152        setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
153
154        final Locale locale = res.getConfiguration().locale;
155        final String titleFormat = DateFormat.getBestDateTimePattern(locale, DEFAULT_TITLE_FORMAT);
156        mTitleFormatter = new SimpleDateFormat(titleFormat, locale);
157        mDayOfWeekFormatter = new SimpleDateFormat(DAY_OF_WEEK_FORMAT, locale);
158
159        setClickable(true);
160        initPaints(res);
161    }
162
163    /**
164     * Applies the specified text appearance resource to a paint, returning the
165     * text color if one is set in the text appearance.
166     *
167     * @param p the paint to modify
168     * @param resId the resource ID of the text appearance
169     * @return the text color, if available
170     */
171    private ColorStateList applyTextAppearance(Paint p, int resId) {
172        final TypedArray ta = mContext.obtainStyledAttributes(null,
173                R.styleable.TextAppearance, 0, resId);
174
175        final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
176        if (fontFamily != null) {
177            p.setTypeface(Typeface.create(fontFamily, 0));
178        }
179
180        p.setTextSize(ta.getDimensionPixelSize(
181                R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
182
183        final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
184        if (textColor != null) {
185            final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
186            p.setColor(enabledColor);
187        }
188
189        ta.recycle();
190
191        return textColor;
192    }
193
194    public void setMonthTextAppearance(int resId) {
195        applyTextAppearance(mMonthPaint, resId);
196        invalidate();
197    }
198
199    public void setDayOfWeekTextAppearance(int resId) {
200        applyTextAppearance(mDayOfWeekPaint, resId);
201        invalidate();
202    }
203
204    public void setDayTextAppearance(int resId) {
205        final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
206        if (textColor != null) {
207            mDayTextColor = textColor;
208        }
209
210        invalidate();
211    }
212
213    public CharSequence getTitle() {
214        if (mTitle == null) {
215            mTitle = mTitleFormatter.format(mCalendar.getTime());
216        }
217        return mTitle;
218    }
219
220    /**
221     * Sets up the text and style properties for painting.
222     */
223    private void initPaints(Resources res) {
224        final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
225        final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
226        final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
227
228        final int monthTextSize = res.getDimensionPixelSize(
229                R.dimen.date_picker_month_text_size);
230        final int dayOfWeekTextSize = res.getDimensionPixelSize(
231                R.dimen.date_picker_day_of_week_text_size);
232        final int dayTextSize = res.getDimensionPixelSize(
233                R.dimen.date_picker_day_text_size);
234
235        mMonthPaint.setAntiAlias(true);
236        mMonthPaint.setTextSize(monthTextSize);
237        mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
238        mMonthPaint.setTextAlign(Align.CENTER);
239        mMonthPaint.setStyle(Style.FILL);
240
241        mDayOfWeekPaint.setAntiAlias(true);
242        mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
243        mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
244        mDayOfWeekPaint.setTextAlign(Align.CENTER);
245        mDayOfWeekPaint.setStyle(Style.FILL);
246
247        mDaySelectorPaint.setAntiAlias(true);
248        mDaySelectorPaint.setStyle(Style.FILL);
249
250        mDayHighlightPaint.setAntiAlias(true);
251        mDayHighlightPaint.setStyle(Style.FILL);
252
253        mDayPaint.setAntiAlias(true);
254        mDayPaint.setTextSize(dayTextSize);
255        mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
256        mDayPaint.setTextAlign(Align.CENTER);
257        mDayPaint.setStyle(Style.FILL);
258    }
259
260    void setMonthTextColor(ColorStateList monthTextColor) {
261        final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
262        mMonthPaint.setColor(enabledColor);
263        invalidate();
264    }
265
266    void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
267        final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
268        mDayOfWeekPaint.setColor(enabledColor);
269        invalidate();
270    }
271
272    void setDayTextColor(ColorStateList dayTextColor) {
273        mDayTextColor = dayTextColor;
274        invalidate();
275    }
276
277    void setDaySelectorColor(ColorStateList dayBackgroundColor) {
278        final int activatedColor = dayBackgroundColor.getColorForState(
279                StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
280        mDaySelectorPaint.setColor(activatedColor);
281        invalidate();
282    }
283
284    void setDayHighlightColor(ColorStateList dayHighlightColor) {
285        final int pressedColor = dayHighlightColor.getColorForState(
286                StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
287        mDayHighlightPaint.setColor(pressedColor);
288        invalidate();
289    }
290
291    public void setOnDayClickListener(OnDayClickListener listener) {
292        mOnDayClickListener = listener;
293    }
294
295    @Override
296    public boolean dispatchHoverEvent(MotionEvent event) {
297        // First right-of-refusal goes the touch exploration helper.
298        return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
299    }
300
301    @Override
302    public boolean onTouchEvent(MotionEvent event) {
303        switch (event.getAction()) {
304            case MotionEvent.ACTION_DOWN:
305            case MotionEvent.ACTION_MOVE:
306                final int touchedDay = getDayAtLocation(event.getX(), event.getY());
307                if (mTouchedDay != touchedDay) {
308                    mTouchedDay = touchedDay;
309                    invalidate();
310                }
311                break;
312
313            case MotionEvent.ACTION_UP:
314                final int clickedDay = getDayAtLocation(event.getX(), event.getY());
315                onDayClicked(clickedDay);
316                // Fall through.
317            case MotionEvent.ACTION_CANCEL:
318                // Reset touched day on stream end.
319                mTouchedDay = -1;
320                invalidate();
321                break;
322        }
323        return true;
324    }
325
326    @Override
327    protected void onDraw(Canvas canvas) {
328        final int paddingLeft = getPaddingLeft();
329        final int paddingTop = getPaddingTop();
330        canvas.translate(paddingLeft, paddingTop);
331
332        drawMonth(canvas);
333        drawDaysOfWeek(canvas);
334        drawDays(canvas);
335
336        canvas.translate(-paddingLeft, -paddingTop);
337    }
338
339    private void drawMonth(Canvas canvas) {
340        final float x = mPaddedWidth / 2f;
341
342        // Vertically centered within the month header height.
343        final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
344        final float y = (mMonthHeight - lineHeight) / 2f;
345
346        canvas.drawText(getTitle().toString(), x, y, mMonthPaint);
347    }
348
349    private void drawDaysOfWeek(Canvas canvas) {
350        final float cellWidthHalf = mPaddedWidth / (DAYS_IN_WEEK * 2);
351
352        // Vertically centered within the cell height.
353        final float lineHeight = mDayOfWeekPaint.ascent() + mDayOfWeekPaint.descent();
354        final float y = mMonthHeight + (mDayOfWeekHeight - lineHeight) / 2f;
355
356        for (int i = 0; i < DAYS_IN_WEEK; i++) {
357            final int calendarDay = (i + mWeekStart) % DAYS_IN_WEEK;
358            mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
359
360            final String dayLabel = mDayOfWeekFormatter.format(mDayLabelCalendar.getTime());
361            final float x = (2 * i + 1) * cellWidthHalf;
362            canvas.drawText(dayLabel, x, y, mDayOfWeekPaint);
363        }
364    }
365
366    /**
367     * Draws the month days.
368     */
369    private void drawDays(Canvas canvas) {
370        final int cellWidthHalf = mPaddedWidth / (DAYS_IN_WEEK * 2);
371
372        // Vertically centered within the cell height.
373        final float halfLineHeight = (mDayPaint.ascent() + mDayPaint.descent()) / 2;
374        float centerY = mMonthHeight + mDayOfWeekHeight + mDayHeight / 2f;
375
376        for (int day = 1, j = findDayOffset(); day <= mDaysInMonth; day++) {
377            final int x = (2 * j + 1) * cellWidthHalf;
378            int stateMask = 0;
379
380            if (day >= mEnabledDayStart && day <= mEnabledDayEnd) {
381                stateMask |= StateSet.VIEW_STATE_ENABLED;
382            }
383
384            final boolean isDayActivated = mActivatedDay == day;
385            if (isDayActivated) {
386                stateMask |= StateSet.VIEW_STATE_ACTIVATED;
387
388                // Adjust the circle to be centered on the row.
389                canvas.drawCircle(x, centerY, mDaySelectorRadius, mDaySelectorPaint);
390            } else if (mTouchedDay == day) {
391                stateMask |= StateSet.VIEW_STATE_PRESSED;
392
393                // Adjust the circle to be centered on the row.
394                canvas.drawCircle(x, centerY, mDaySelectorRadius, mDayHighlightPaint);
395            }
396
397            final boolean isDayToday = mToday == day;
398            final int dayTextColor;
399            if (isDayToday && !isDayActivated) {
400                dayTextColor = mDaySelectorPaint.getColor();
401            } else {
402                final int[] stateSet = StateSet.get(stateMask);
403                dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
404            }
405            mDayPaint.setColor(dayTextColor);
406
407            canvas.drawText("" + day, x, centerY - halfLineHeight, mDayPaint);
408
409            j++;
410
411            if (j == DAYS_IN_WEEK) {
412                j = 0;
413                centerY += mDayHeight;
414            }
415        }
416    }
417
418    private static boolean isValidDayOfWeek(int day) {
419        return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
420    }
421
422    private static boolean isValidMonth(int month) {
423        return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
424    }
425
426    /**
427     * Sets the selected day.
428     *
429     * @param dayOfMonth the selected day of the month, or {@code -1} to clear
430     *                   the selection
431     */
432    public void setSelectedDay(int dayOfMonth) {
433        mActivatedDay = dayOfMonth;
434
435        // Invalidate cached accessibility information.
436        mTouchHelper.invalidateRoot();
437        invalidate();
438    }
439
440    /**
441     * Sets the first day of the week.
442     *
443     * @param weekStart which day the week should start on, valid values are
444     *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
445     */
446    public void setFirstDayOfWeek(int weekStart) {
447        if (isValidDayOfWeek(weekStart)) {
448            mWeekStart = weekStart;
449        } else {
450            mWeekStart = mCalendar.getFirstDayOfWeek();
451        }
452
453        // Invalidate cached accessibility information.
454        mTouchHelper.invalidateRoot();
455        invalidate();
456    }
457
458    /**
459     * Sets all the parameters for displaying this week.
460     * <p>
461     * Parameters have a default value and will only update if a new value is
462     * included, except for focus month, which will always default to no focus
463     * month if no value is passed in. The only required parameter is the week
464     * start.
465     *
466     * @param selectedDay the selected day of the month, or -1 for no selection
467     * @param month the month
468     * @param year the year
469     * @param weekStart which day the week should start on, valid values are
470     *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
471     * @param enabledDayStart the first enabled day
472     * @param enabledDayEnd the last enabled day
473     */
474    void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
475            int enabledDayEnd) {
476        mActivatedDay = selectedDay;
477
478        if (isValidMonth(month)) {
479            mMonth = month;
480        }
481        mYear = year;
482
483        mCalendar.set(Calendar.MONTH, mMonth);
484        mCalendar.set(Calendar.YEAR, mYear);
485        mCalendar.set(Calendar.DAY_OF_MONTH, 1);
486        mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
487
488        if (isValidDayOfWeek(weekStart)) {
489            mWeekStart = weekStart;
490        } else {
491            mWeekStart = mCalendar.getFirstDayOfWeek();
492        }
493
494        if (enabledDayStart > 0 && enabledDayEnd < 32) {
495            mEnabledDayStart = enabledDayStart;
496        }
497        if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) {
498            mEnabledDayEnd = enabledDayEnd;
499        }
500
501        // Figure out what day today is.
502        final Calendar today = Calendar.getInstance();
503        mToday = -1;
504        mDaysInMonth = getDaysInMonth(mMonth, mYear);
505        for (int i = 0; i < mDaysInMonth; i++) {
506            final int day = i + 1;
507            if (sameDay(day, today)) {
508                mToday = day;
509            }
510        }
511        mNumWeeks = calculateNumRows();
512
513        // Invalidate the old title.
514        mTitle = null;
515
516        // Invalidate cached accessibility information.
517        mTouchHelper.invalidateRoot();
518    }
519
520    private static int getDaysInMonth(int month, int year) {
521        switch (month) {
522            case Calendar.JANUARY:
523            case Calendar.MARCH:
524            case Calendar.MAY:
525            case Calendar.JULY:
526            case Calendar.AUGUST:
527            case Calendar.OCTOBER:
528            case Calendar.DECEMBER:
529                return 31;
530            case Calendar.APRIL:
531            case Calendar.JUNE:
532            case Calendar.SEPTEMBER:
533            case Calendar.NOVEMBER:
534                return 30;
535            case Calendar.FEBRUARY:
536                return (year % 4 == 0) ? 29 : 28;
537            default:
538                throw new IllegalArgumentException("Invalid Month");
539        }
540    }
541
542    public void reuse() {
543        mNumWeeks = MAX_WEEKS_IN_MONTH;
544        requestLayout();
545    }
546
547    private int calculateNumRows() {
548        final int offset = findDayOffset();
549        final int dividend = (offset + mDaysInMonth) / DAYS_IN_WEEK;
550        final int remainder = (offset + mDaysInMonth) % DAYS_IN_WEEK;
551        return dividend + (remainder > 0 ? 1 : 0);
552    }
553
554    private boolean sameDay(int day, Calendar today) {
555        return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
556                && day == today.get(Calendar.DAY_OF_MONTH);
557    }
558
559    @Override
560    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
561        final int preferredHeight = mDayHeight * mNumWeeks + mDayOfWeekHeight + mMonthHeight
562                + getPaddingTop() + getPaddingBottom();
563        final int preferredWidth = mCellWidth * DAYS_IN_WEEK
564                + getPaddingStart() + getPaddingEnd();
565        final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
566        final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
567        setMeasuredDimension(resolvedWidth, resolvedHeight);
568    }
569
570    @Override
571    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
572        mPaddedWidth = w - getPaddingLeft() - getPaddingRight();
573        mPaddedHeight = w - getPaddingTop() - getPaddingBottom();
574
575        // Invalidate cached accessibility information.
576        mTouchHelper.invalidateRoot();
577    }
578
579    private int findDayOffset() {
580        final int offset = mDayOfWeekStart - mWeekStart;
581        if (mDayOfWeekStart < mWeekStart) {
582            return offset + DAYS_IN_WEEK;
583        }
584        return offset;
585    }
586
587    /**
588     * Calculates the day of the month at the specified touch position. Returns
589     * the day of the month or -1 if the position wasn't in a valid day.
590     *
591     * @param x the x position of the touch event
592     * @param y the y position of the touch event
593     * @return the day of the month at (x, y) or -1 if the position wasn't in a
594     *         valid day
595     */
596    private int getDayAtLocation(float x, float y) {
597        final int paddedX = (int) (x - getPaddingLeft() + 0.5f);
598        if (paddedX < 0 || paddedX >= mPaddedWidth) {
599            return -1;
600        }
601
602        final int headerHeight = mMonthHeight + mDayOfWeekHeight;
603        final int paddedY = (int) (y - getPaddingTop() + 0.5f);
604        if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
605            return -1;
606        }
607
608        final int row = (paddedY - headerHeight) / mDayHeight;
609        final int col = (paddedX * DAYS_IN_WEEK) / mPaddedWidth;
610        final int index = col + row * DAYS_IN_WEEK;
611        final int day = index + 1 - findDayOffset();
612        if (day < 1 || day > mDaysInMonth) {
613            return -1;
614        }
615
616        return day;
617    }
618
619    /**
620     * Calculates the bounds of the specified day.
621     *
622     * @param day the day of the month
623     * @param outBounds the rect to populate with bounds
624     */
625    private boolean getBoundsForDay(int day, Rect outBounds) {
626        if (day < 1 || day > mDaysInMonth) {
627            return false;
628        }
629
630        final int index = day - 1 + findDayOffset();
631        final int row = index / DAYS_IN_WEEK;
632        final int col = index % DAYS_IN_WEEK;
633
634        final int headerHeight = mMonthHeight + mDayOfWeekHeight;
635        final int paddedY = row * mDayHeight + headerHeight;
636        final int paddedX = col * mPaddedWidth;
637
638        final int y = paddedY + getPaddingTop();
639        final int x = paddedX + getPaddingLeft();
640
641        final int cellHeight = mDayHeight;
642        final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
643        outBounds.set(x, y, (x + cellWidth), (y + cellHeight));
644
645        return true;
646    }
647
648    /**
649     * Called when the user clicks on a day. Handles callbacks to the
650     * {@link OnDayClickListener} if one is set.
651     *
652     * @param day the day that was clicked
653     */
654    private void onDayClicked(int day) {
655        if (mOnDayClickListener != null) {
656            Calendar date = Calendar.getInstance();
657            date.set(mYear, mMonth, day);
658            mOnDayClickListener.onDayClick(this, date);
659        }
660
661        // This is a no-op if accessibility is turned off.
662        mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
663    }
664
665    /**
666     * Provides a virtual view hierarchy for interfacing with an accessibility
667     * service.
668     */
669    private class MonthViewTouchHelper extends ExploreByTouchHelper {
670        private static final String DATE_FORMAT = "dd MMMM yyyy";
671
672        private final Rect mTempRect = new Rect();
673        private final Calendar mTempCalendar = Calendar.getInstance();
674
675        public MonthViewTouchHelper(View host) {
676            super(host);
677        }
678
679        @Override
680        protected int getVirtualViewAt(float x, float y) {
681            final int day = getDayAtLocation(x, y);
682            if (day >= 0) {
683                return day;
684            }
685            return ExploreByTouchHelper.INVALID_ID;
686        }
687
688        @Override
689        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
690            for (int day = 1; day <= mDaysInMonth; day++) {
691                virtualViewIds.add(day);
692            }
693        }
694
695        @Override
696        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
697            event.setContentDescription(getItemDescription(virtualViewId));
698        }
699
700        @Override
701        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
702            final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect);
703
704            if (!hasBounds) {
705                // The day is invalid, kill the node.
706                mTempRect.setEmpty();
707                node.setContentDescription("");
708                node.setBoundsInParent(mTempRect);
709                node.setVisibleToUser(false);
710                return;
711            }
712
713            node.setContentDescription(getItemDescription(virtualViewId));
714            node.setBoundsInParent(mTempRect);
715            node.addAction(AccessibilityAction.ACTION_CLICK);
716
717            if (virtualViewId == mActivatedDay) {
718                node.setSelected(true);
719            }
720
721        }
722
723        @Override
724        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
725                Bundle arguments) {
726            switch (action) {
727                case AccessibilityNodeInfo.ACTION_CLICK:
728                    onDayClicked(virtualViewId);
729                    return true;
730            }
731
732            return false;
733        }
734
735        /**
736         * Generates a description for a given time object. Since this
737         * description will be spoken, the components are ordered by descending
738         * specificity as DAY MONTH YEAR.
739         *
740         * @param day The day to generate a description for
741         * @return A description of the time object
742         */
743        private CharSequence getItemDescription(int day) {
744            mTempCalendar.set(mYear, mMonth, day);
745            final CharSequence date = DateFormat.format(DATE_FORMAT,
746                    mTempCalendar.getTimeInMillis());
747
748            if (day == mActivatedDay) {
749                return getContext().getString(R.string.item_is_selected, date);
750            }
751
752            return date;
753        }
754    }
755
756    /**
757     * Handles callbacks when the user clicks on a time object.
758     */
759    public interface OnDayClickListener {
760        public void onDayClick(SimpleMonthView view, Calendar day);
761    }
762}
763