MonthView.java revision e0a0cb288106e3a25441ea57a123a812929ec79c
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    protected final DatePickerController mController;
124
125    // affects the padding on the sides of this view
126    protected int mPadding = 0;
127
128    private String mDayOfWeekTypeface;
129    private String mMonthTitleTypeface;
130
131    protected Paint mMonthNumPaint;
132    protected Paint mMonthTitlePaint;
133    protected Paint mMonthTitleBGPaint;
134    protected Paint mSelectedCirclePaint;
135    protected Paint mMonthDayLabelPaint;
136
137    private final Formatter mFormatter;
138    private final StringBuilder mStringBuilder;
139
140    // The Julian day of the first day displayed by this item
141    protected int mFirstJulianDay = -1;
142    // The month of the first day in this week
143    protected int mFirstMonth = -1;
144    // The month of the last day in this week
145    protected int mLastMonth = -1;
146
147    protected int mMonth;
148
149    protected int mYear;
150    // Quick reference to the width of this view, matches parent
151    protected int mWidth;
152    // The height this view should draw at in pixels, set by height param
153    protected int mRowHeight = DEFAULT_HEIGHT;
154    // If this view contains the today
155    protected boolean mHasToday = false;
156    // Which day is selected [0-6] or -1 if no day is selected
157    protected int mSelectedDay = -1;
158    // Which day is today [0-6] or -1 if no day is today
159    protected int mToday = DEFAULT_SELECTED_DAY;
160    // Which day of the week to start on [0-6]
161    protected int mWeekStart = DEFAULT_WEEK_START;
162    // How many days to display
163    protected int mNumDays = DEFAULT_NUM_DAYS;
164    // The number of days + a spot for week number if it is displayed
165    protected int mNumCells = mNumDays;
166    // The left edge of the selected day
167    protected int mSelectedLeft = -1;
168    // The right edge of the selected day
169    protected int mSelectedRight = -1;
170
171    private final Calendar mCalendar;
172    protected final Calendar mDayLabelCalendar;
173    private final MonthViewTouchHelper mTouchHelper;
174
175    private int mNumRows = DEFAULT_NUM_ROWS;
176
177    // Optional listener for handling day click actions
178    private OnDayClickListener mOnDayClickListener;
179    // Whether to prevent setting the accessibility delegate
180    private boolean mLockAccessibilityDelegate;
181
182    protected int mDayTextColor;
183    protected int mTodayNumberColor;
184    protected int mDisabledDayTextColor;
185    protected int mMonthTitleColor;
186    protected int mMonthTitleBGColor;
187
188    public MonthView(Context context) {
189        this(context, null);
190    }
191
192    public MonthView(Context context, DatePickerController controller) {
193        super(context);
194
195        mController = controller;
196
197        Resources res = context.getResources();
198
199        mDayLabelCalendar = Calendar.getInstance();
200        mCalendar = Calendar.getInstance();
201
202        mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
203        mMonthTitleTypeface = res.getString(R.string.sans_serif);
204
205        mDayTextColor = res.getColor(R.color.date_picker_text_normal);
206        mTodayNumberColor = res.getColor(R.color.blue);
207        mDisabledDayTextColor = res.getColor(R.color.date_picker_text_disabled);
208        mMonthTitleColor = res.getColor(R.color.white);
209        mMonthTitleBGColor = res.getColor(R.color.circle_background);
210
211        mStringBuilder = new StringBuilder(50);
212        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
213
214        MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size);
215        MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size);
216        MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size);
217        MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height);
218        DAY_SELECTED_CIRCLE_SIZE = res
219                .getDimensionPixelSize(R.dimen.day_number_select_circle_radius);
220
221        mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height)
222                - MONTH_HEADER_SIZE) / MAX_NUM_ROWS;
223
224        // Set up accessibility components.
225        mTouchHelper = getMonthViewTouchHelper();
226        ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
227        ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
228        mLockAccessibilityDelegate = true;
229
230        // Sets up any standard paints that will be used
231        initView();
232    }
233
234    protected MonthViewTouchHelper getMonthViewTouchHelper() {
235        return new MonthViewTouchHelper(this);
236    }
237
238    @Override
239    public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
240        // Workaround for a JB MR1 issue where accessibility delegates on
241        // top-level ListView items are overwritten.
242        if (!mLockAccessibilityDelegate) {
243            super.setAccessibilityDelegate(delegate);
244        }
245    }
246
247    public void setOnDayClickListener(OnDayClickListener listener) {
248        mOnDayClickListener = listener;
249    }
250
251    @Override
252    public boolean dispatchHoverEvent(MotionEvent event) {
253        // First right-of-refusal goes the touch exploration helper.
254        if (mTouchHelper.dispatchHoverEvent(event)) {
255            return true;
256        }
257        return super.dispatchHoverEvent(event);
258    }
259
260    @Override
261    public boolean onTouchEvent(MotionEvent event) {
262        switch (event.getAction()) {
263            case MotionEvent.ACTION_UP:
264                final int day = getDayFromLocation(event.getX(), event.getY());
265                if (day >= 0) {
266                    onDayClick(day);
267                }
268                break;
269        }
270        return true;
271    }
272
273    /**
274     * Sets up the text and style properties for painting. Override this if you
275     * want to use a different paint.
276     */
277    protected void initView() {
278        mMonthTitlePaint = new Paint();
279        mMonthTitlePaint.setFakeBoldText(true);
280        mMonthTitlePaint.setAntiAlias(true);
281        mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
282        mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
283        mMonthTitlePaint.setColor(mDayTextColor);
284        mMonthTitlePaint.setTextAlign(Align.CENTER);
285        mMonthTitlePaint.setStyle(Style.FILL);
286
287        mMonthTitleBGPaint = new Paint();
288        mMonthTitleBGPaint.setFakeBoldText(true);
289        mMonthTitleBGPaint.setAntiAlias(true);
290        mMonthTitleBGPaint.setColor(mMonthTitleBGColor);
291        mMonthTitleBGPaint.setTextAlign(Align.CENTER);
292        mMonthTitleBGPaint.setStyle(Style.FILL);
293
294        mSelectedCirclePaint = new Paint();
295        mSelectedCirclePaint.setFakeBoldText(true);
296        mSelectedCirclePaint.setAntiAlias(true);
297        mSelectedCirclePaint.setColor(mTodayNumberColor);
298        mSelectedCirclePaint.setTextAlign(Align.CENTER);
299        mSelectedCirclePaint.setStyle(Style.FILL);
300        mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
301
302        mMonthDayLabelPaint = new Paint();
303        mMonthDayLabelPaint.setAntiAlias(true);
304        mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
305        mMonthDayLabelPaint.setColor(mDayTextColor);
306        mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
307        mMonthDayLabelPaint.setStyle(Style.FILL);
308        mMonthDayLabelPaint.setTextAlign(Align.CENTER);
309        mMonthDayLabelPaint.setFakeBoldText(true);
310
311        mMonthNumPaint = new Paint();
312        mMonthNumPaint.setAntiAlias(true);
313        mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
314        mMonthNumPaint.setStyle(Style.FILL);
315        mMonthNumPaint.setTextAlign(Align.CENTER);
316        mMonthNumPaint.setFakeBoldText(false);
317    }
318
319    @Override
320    protected void onDraw(Canvas canvas) {
321        drawMonthTitle(canvas);
322        drawMonthDayLabels(canvas);
323        drawMonthNums(canvas);
324    }
325
326    private int mDayOfWeekStart = 0;
327
328    /**
329     * Sets all the parameters for displaying this week. The only required
330     * parameter is the week number. Other parameters have a default value and
331     * will only update if a new value is included, except for focus month,
332     * which will always default to no focus month if no value is passed in. See
333     * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters.
334     *
335     * @param params A map of the new parameters, see
336     *            {@link #VIEW_PARAMS_HEIGHT}
337     */
338    public void setMonthParams(HashMap<String, Integer> params) {
339        if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) {
340            throw new InvalidParameterException("You must specify month and year for this view");
341        }
342        setTag(params);
343        // We keep the current value for any params not present
344        if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
345            mRowHeight = params.get(VIEW_PARAMS_HEIGHT);
346            if (mRowHeight < MIN_HEIGHT) {
347                mRowHeight = MIN_HEIGHT;
348            }
349        }
350        if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
351            mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
352        }
353
354        // Allocate space for caching the day numbers and focus values
355        mMonth = params.get(VIEW_PARAMS_MONTH);
356        mYear = params.get(VIEW_PARAMS_YEAR);
357
358        // Figure out what day today is
359        final Time today = new Time(Time.getCurrentTimezone());
360        today.setToNow();
361        mHasToday = false;
362        mToday = -1;
363
364        mCalendar.set(Calendar.MONTH, mMonth);
365        mCalendar.set(Calendar.YEAR, mYear);
366        mCalendar.set(Calendar.DAY_OF_MONTH, 1);
367        mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
368
369        if (params.containsKey(VIEW_PARAMS_WEEK_START)) {
370            mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
371        } else {
372            mWeekStart = mCalendar.getFirstDayOfWeek();
373        }
374
375        mNumCells = Utils.getDaysInMonth(mMonth, mYear);
376        for (int i = 0; i < mNumCells; i++) {
377            final int day = i + 1;
378            if (sameDay(day, today)) {
379                mHasToday = true;
380                mToday = day;
381            }
382        }
383        mNumRows = calculateNumRows();
384
385        // Invalidate cached accessibility information.
386        mTouchHelper.invalidateRoot();
387    }
388
389    public void setSelectedDay(int day) {
390        mSelectedDay = day;
391    }
392
393    public void reuse() {
394        mNumRows = DEFAULT_NUM_ROWS;
395        requestLayout();
396    }
397
398    private int calculateNumRows() {
399        int offset = findDayOffset();
400        int dividend = (offset + mNumCells) / mNumDays;
401        int remainder = (offset + mNumCells) % mNumDays;
402        return (dividend + (remainder > 0 ? 1 : 0));
403    }
404
405    private boolean sameDay(int day, Time today) {
406        return mYear == today.year &&
407                mMonth == today.month &&
408                day == today.monthDay;
409    }
410
411    @Override
412    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
413        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
414                + MONTH_HEADER_SIZE);
415    }
416
417    @Override
418    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
419        mWidth = w;
420
421        // Invalidate cached accessibility information.
422        mTouchHelper.invalidateRoot();
423    }
424
425    public int getMonth() {
426        return mMonth;
427    }
428
429    public int getYear() {
430        return mYear;
431    }
432
433    private String getMonthAndYearString() {
434        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
435                | DateUtils.FORMAT_NO_MONTH_DAY;
436        mStringBuilder.setLength(0);
437        long millis = mCalendar.getTimeInMillis();
438        return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
439                Time.getCurrentTimezone()).toString();
440    }
441
442    protected void drawMonthTitle(Canvas canvas) {
443        int x = (mWidth + 2 * mPadding) / 2;
444        int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3);
445        canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
446    }
447
448    protected void drawMonthDayLabels(Canvas canvas) {
449        int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
450        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
451
452        for (int i = 0; i < mNumDays; i++) {
453            int calendarDay = (i + mWeekStart) % mNumDays;
454            int x = (2 * i + 1) * dayWidthHalf + mPadding;
455            mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
456            canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
457                    Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
458                    mMonthDayLabelPaint);
459        }
460    }
461
462    /**
463     * Draws the week and month day numbers for this week. Override this method
464     * if you need different placement.
465     *
466     * @param canvas The canvas to draw on
467     */
468    protected void drawMonthNums(Canvas canvas) {
469        int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
470                + MONTH_HEADER_SIZE;
471        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
472        int j = findDayOffset();
473        for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
474            int x = (2 * j + 1) * dayWidthHalf + mPadding;
475
476            int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH;
477
478            int startX = x - dayWidthHalf;
479            int stopX = x + dayWidthHalf;
480            int startY = y - yRelativeToDay;
481            int stopY = startY + mRowHeight;
482
483            drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY);
484
485            j++;
486            if (j == mNumDays) {
487                j = 0;
488                y += mRowHeight;
489            }
490        }
491    }
492
493    /**
494     * This method should draw the month day.  Implemented by sub-classes to allow customization.
495     *
496     * @param canvas  The canvas to draw on
497     * @param year  The year of this month day
498     * @param month  The month of this month day
499     * @param day  The day number of this month day
500     * @param x  The default x position to draw the day number
501     * @param y  The default y position to draw the day number
502     * @param startX  The left boundary of the day number rect
503     * @param stopX  The right boundary of the day number rect
504     * @param startY  The top boundary of the day number rect
505     * @param stopY  The bottom boundary of the day number rect
506     */
507    public abstract void drawMonthDay(Canvas canvas, int year, int month, int day,
508            int x, int y, int startX, int stopX, int startY, int stopY);
509
510    protected int findDayOffset() {
511        return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
512                - mWeekStart;
513    }
514
515
516    /**
517     * Calculates the day that the given x position is in, accounting for week
518     * number. Returns the day or -1 if the position wasn't in a day.
519     *
520     * @param x The x position of the touch event
521     * @return The day number, or -1 if the position wasn't in a day
522     */
523    public int getDayFromLocation(float x, float y) {
524        int dayStart = mPadding;
525        if (x < dayStart || x > mWidth - mPadding) {
526            return -1;
527        }
528        // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
529        int row = (int) (y - MONTH_HEADER_SIZE) / mRowHeight;
530        int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
531
532        int day = column - findDayOffset() + 1;
533        day += row * mNumDays;
534        if (day < 1 || day > mNumCells) {
535            return -1;
536        }
537        return day;
538    }
539
540    /**
541     * Called when the user clicks on a day. Handles callbacks to the
542     * {@link OnDayClickListener} if one is set.
543     * <p/>
544     * If the day is out of the range set by minDate and/or maxDate, this is a no-op.
545     *
546     * @param day The day that was clicked
547     */
548    private void onDayClick(int day) {
549        // If the min / max date are set, only process the click if it's a valid selection.
550        if (isOutOfRange(mYear, mMonth, day)) {
551            return;
552        }
553
554
555        if (mOnDayClickListener != null) {
556            mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day));
557        }
558
559        // This is a no-op if accessibility is turned off.
560        mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
561    }
562
563    /**
564     * @return true if the specified year/month/day are within the range set by minDate and maxDate.
565     * If one or either have not been set, they are considered as Integer.MIN_VALUE and
566     * Integer.MAX_VALUE.
567     */
568    protected boolean isOutOfRange(int year, int month, int day) {
569        if (isBeforeMin(year, month, day)) {
570            return true;
571        } else if (isAfterMax(year, month, day)) {
572            return true;
573        }
574
575        return false;
576    }
577
578    private boolean isBeforeMin(int year, int month, int day) {
579        if (mController == null) {
580            return false;
581        }
582        Calendar minDate = mController.getMinDate();
583        if (minDate == null) {
584            return false;
585        }
586
587        if (year < minDate.get(Calendar.YEAR)) {
588            return true;
589        } else if (year > minDate.get(Calendar.YEAR)) {
590            return false;
591        }
592
593        if (month < minDate.get(Calendar.MONTH)) {
594            return true;
595        } else if (month > minDate.get(Calendar.MONTH)) {
596            return false;
597        }
598
599        if (day < minDate.get(Calendar.DAY_OF_MONTH)) {
600            return true;
601        } else {
602            return false;
603        }
604    }
605
606    private boolean isAfterMax(int year, int month, int day) {
607        if (mController == null) {
608            return false;
609        }
610        Calendar maxDate = mController.getMaxDate();
611        if (maxDate == null) {
612            return false;
613        }
614
615        if (year > maxDate.get(Calendar.YEAR)) {
616            return true;
617        } else if (year < maxDate.get(Calendar.YEAR)) {
618            return false;
619        }
620
621        if (month > maxDate.get(Calendar.MONTH)) {
622            return true;
623        } else if (month < maxDate.get(Calendar.MONTH)) {
624            return false;
625        }
626
627        if (day > maxDate.get(Calendar.DAY_OF_MONTH)) {
628            return true;
629        } else {
630            return false;
631        }
632    }
633
634    /**
635     * @return The date that has accessibility focus, or {@code null} if no date
636     *         has focus
637     */
638    public CalendarDay getAccessibilityFocus() {
639        final int day = mTouchHelper.getFocusedVirtualView();
640        if (day >= 0) {
641            return new CalendarDay(mYear, mMonth, day);
642        }
643        return null;
644    }
645
646    /**
647     * Clears accessibility focus within the view. No-op if the view does not
648     * contain accessibility focus.
649     */
650    public void clearAccessibilityFocus() {
651        mTouchHelper.clearFocusedVirtualView();
652    }
653
654    /**
655     * Attempts to restore accessibility focus to the specified date.
656     *
657     * @param day The date which should receive focus
658     * @return {@code false} if the date is not valid for this month view, or
659     *         {@code true} if the date received focus
660     */
661    public boolean restoreAccessibilityFocus(CalendarDay day) {
662        if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) {
663            return false;
664        }
665        mTouchHelper.setFocusedVirtualView(day.day);
666        return true;
667    }
668
669    /**
670     * Provides a virtual view hierarchy for interfacing with an accessibility
671     * service.
672     */
673    protected class MonthViewTouchHelper extends ExploreByTouchHelper {
674        private static final String DATE_FORMAT = "dd MMMM yyyy";
675
676        private final Rect mTempRect = new Rect();
677        private final Calendar mTempCalendar = Calendar.getInstance();
678
679        public MonthViewTouchHelper(View host) {
680            super(host);
681        }
682
683        public void setFocusedVirtualView(int virtualViewId) {
684            getAccessibilityNodeProvider(MonthView.this).performAction(
685                    virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
686        }
687
688        public void clearFocusedVirtualView() {
689            final int focusedVirtualView = getFocusedVirtualView();
690            if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
691                getAccessibilityNodeProvider(MonthView.this).performAction(
692                        focusedVirtualView,
693                        AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
694                        null);
695            }
696        }
697
698        @Override
699        protected int getVirtualViewAt(float x, float y) {
700            final int day = getDayFromLocation(x, y);
701            if (day >= 0) {
702                return day;
703            }
704            return ExploreByTouchHelper.INVALID_ID;
705        }
706
707        @Override
708        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
709            for (int day = 1; day <= mNumCells; day++) {
710                virtualViewIds.add(day);
711            }
712        }
713
714        @Override
715        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
716            event.setContentDescription(getItemDescription(virtualViewId));
717        }
718
719        @Override
720        protected void onPopulateNodeForVirtualView(int virtualViewId,
721                AccessibilityNodeInfoCompat node) {
722            getItemBounds(virtualViewId, mTempRect);
723
724            node.setContentDescription(getItemDescription(virtualViewId));
725            node.setBoundsInParent(mTempRect);
726            node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
727
728            if (virtualViewId == mSelectedDay) {
729                node.setSelected(true);
730            }
731
732        }
733
734        @Override
735        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
736                Bundle arguments) {
737            switch (action) {
738                case AccessibilityNodeInfo.ACTION_CLICK:
739                    onDayClick(virtualViewId);
740                    return true;
741            }
742
743            return false;
744        }
745
746        /**
747         * Calculates the bounding rectangle of a given time object.
748         *
749         * @param day The day to calculate bounds for
750         * @param rect The rectangle in which to store the bounds
751         */
752        protected void getItemBounds(int day, Rect rect) {
753            final int offsetX = mPadding;
754            final int offsetY = MONTH_HEADER_SIZE;
755            final int cellHeight = mRowHeight;
756            final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays);
757            final int index = ((day - 1) + findDayOffset());
758            final int row = (index / mNumDays);
759            final int column = (index % mNumDays);
760            final int x = (offsetX + (column * cellWidth));
761            final int y = (offsetY + (row * cellHeight));
762
763            rect.set(x, y, (x + cellWidth), (y + cellHeight));
764        }
765
766        /**
767         * Generates a description for a given time object. Since this
768         * description will be spoken, the components are ordered by descending
769         * specificity as DAY MONTH YEAR.
770         *
771         * @param day The day to generate a description for
772         * @return A description of the time object
773         */
774        protected CharSequence getItemDescription(int day) {
775            mTempCalendar.set(mYear, mMonth, day);
776            final CharSequence date = DateFormat.format(DATE_FORMAT,
777                    mTempCalendar.getTimeInMillis());
778
779            if (day == mSelectedDay) {
780                return getContext().getString(R.string.item_is_selected, date);
781            }
782
783            return date;
784        }
785    }
786
787    /**
788     * Handles callbacks when the user clicks on a time object.
789     */
790    public interface OnDayClickListener {
791        public void onDayClick(MonthView view, CalendarDay day);
792    }
793}
794