SimpleMonthView.java revision 2994491d65ecb60debc2671535516d3225261049
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.SimpleMonthAdapter.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 class SimpleMonthView extends View {
55    private static final String TAG = "SimpleMonthView";
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 SimpleMonthView(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 the 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 reuse() {
376        mNumRows = DEFAULT_NUM_ROWS;
377        requestLayout();
378    }
379
380    private int calculateNumRows() {
381        int offset = findDayOffset();
382        int dividend = (offset + mNumCells) / mNumDays;
383        int remainder = (offset + mNumCells) % mNumDays;
384        return (dividend + (remainder > 0 ? 1 : 0));
385    }
386
387    private boolean sameDay(int day, Time today) {
388        return mYear == today.year &&
389                mMonth == today.month &&
390                day == today.monthDay;
391    }
392
393    @Override
394    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
395        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
396                + MONTH_HEADER_SIZE);
397    }
398
399    @Override
400    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
401        mWidth = w;
402
403        // Invalidate cached accessibility information.
404        mTouchHelper.invalidateRoot();
405    }
406
407    private String getMonthAndYearString() {
408        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
409                | DateUtils.FORMAT_NO_MONTH_DAY;
410        mStringBuilder.setLength(0);
411        long millis = mCalendar.getTimeInMillis();
412        return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
413                Time.getCurrentTimezone()).toString();
414    }
415
416    private void drawMonthTitle(Canvas canvas) {
417        int x = (mWidth + 2 * mPadding) / 2;
418        int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3);
419        canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
420    }
421
422    private void drawMonthDayLabels(Canvas canvas) {
423        int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
424        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
425
426        for (int i = 0; i < mNumDays; i++) {
427            int calendarDay = (i + mWeekStart) % mNumDays;
428            int x = (2 * i + 1) * dayWidthHalf + mPadding;
429            mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
430            canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
431                    Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
432                    mMonthDayLabelPaint);
433        }
434    }
435
436    /**
437     * Draws the week and month day numbers for this week. Override this method
438     * if you need different placement.
439     *
440     * @param canvas The canvas to draw on
441     */
442    protected void drawMonthNums(Canvas canvas) {
443        int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
444                + MONTH_HEADER_SIZE;
445        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
446        int j = findDayOffset();
447        for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
448            int x = (2 * j + 1) * dayWidthHalf + mPadding;
449            if (mSelectedDay == dayNumber) {
450                canvas.drawCircle(x, y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE,
451                        mSelectedCirclePaint);
452            }
453
454            if (mHasToday && mToday == dayNumber) {
455                mMonthNumPaint.setColor(mTodayNumberColor);
456            } else {
457                mMonthNumPaint.setColor(mDayTextColor);
458            }
459            canvas.drawText(String.format("%d", dayNumber), x, y, mMonthNumPaint);
460            j++;
461            if (j == mNumDays) {
462                j = 0;
463                y += mRowHeight;
464            }
465        }
466    }
467
468    private int findDayOffset() {
469        return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
470                - mWeekStart;
471    }
472
473
474    /**
475     * Calculates the day that the given x position is in, accounting for week
476     * number. Returns the day or -1 if the position wasn't in a day.
477     *
478     * @param x The x position of the touch event
479     * @return The day number, or -1 if the position wasn't in a day
480     */
481    public int getDayFromLocation(float x, float y) {
482        int dayStart = mPadding;
483        if (x < dayStart || x > mWidth - mPadding) {
484            return -1;
485        }
486        // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
487        int row = (int) (y - MONTH_HEADER_SIZE) / mRowHeight;
488        int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
489
490        int day = column - findDayOffset() + 1;
491        day += row * mNumDays;
492        if (day < 1 || day > mNumCells) {
493            return -1;
494        }
495        return day;
496    }
497
498    /**
499     * Called when the user clicks on a day. Handles callbacks to the
500     * {@link OnDayClickListener} if one is set.
501     *
502     * @param day The day that was clicked
503     */
504    private void onDayClick(int day) {
505        if (mOnDayClickListener != null) {
506            mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day));
507        }
508
509        // This is a no-op if accessibility is turned off.
510        mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
511    }
512
513    /**
514     * @return The date that has accessibility focus, or {@code null} if no date
515     *         has focus
516     */
517    public CalendarDay getAccessibilityFocus() {
518        final int day = mTouchHelper.getFocusedVirtualView();
519        if (day >= 0) {
520            return new CalendarDay(mYear, mMonth, day);
521        }
522        return null;
523    }
524
525    /**
526     * Clears accessibility focus within the view. No-op if the view does not
527     * contain accessibility focus.
528     */
529    public void clearAccessibilityFocus() {
530        mTouchHelper.clearFocusedVirtualView();
531    }
532
533    /**
534     * Attempts to restore accessibility focus to the specified date.
535     *
536     * @param day The date which should receive focus
537     * @return {@code false} if the date is not valid for this month view, or
538     *         {@code true} if the date received focus
539     */
540    public boolean restoreAccessibilityFocus(CalendarDay day) {
541        if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) {
542            return false;
543        }
544        mTouchHelper.setFocusedVirtualView(day.day);
545        return true;
546    }
547
548    /**
549     * Provides a virtual view hierarchy for interfacing with an accessibility
550     * service.
551     */
552    private class MonthViewTouchHelper extends ExploreByTouchHelper {
553        private static final String DATE_FORMAT = "dd MMMM yyyy";
554
555        private final Rect mTempRect = new Rect();
556        private final Calendar mTempCalendar = Calendar.getInstance();
557
558        public MonthViewTouchHelper(View host) {
559            super(host);
560        }
561
562        public void setFocusedVirtualView(int virtualViewId) {
563            getAccessibilityNodeProvider(SimpleMonthView.this).performAction(
564                    virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
565        }
566
567        public void clearFocusedVirtualView() {
568            final int focusedVirtualView = getFocusedVirtualView();
569            if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
570                getAccessibilityNodeProvider(SimpleMonthView.this).performAction(
571                        focusedVirtualView, AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
572            }
573        }
574
575        @Override
576        protected int getVirtualViewAt(float x, float y) {
577            final int day = getDayFromLocation(x, y);
578            if (day >= 0) {
579                return day;
580            }
581            return ExploreByTouchHelper.INVALID_ID;
582        }
583
584        @Override
585        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
586            for (int day = 1; day <= mNumCells; day++) {
587                virtualViewIds.add(day);
588            }
589        }
590
591        @Override
592        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
593            event.setContentDescription(getItemDescription(virtualViewId));
594        }
595
596        @Override
597        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) {
598            getItemBounds(virtualViewId, mTempRect);
599
600            node.setContentDescription(getItemDescription(virtualViewId));
601            node.setBoundsInParent(mTempRect);
602            node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
603
604            if (virtualViewId == mSelectedDay) {
605                node.setSelected(true);
606            }
607
608        }
609
610        @Override
611        protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) {
612            switch (action) {
613                case AccessibilityNodeInfo.ACTION_CLICK:
614                    onDayClick(virtualViewId);
615                    return true;
616            }
617
618            return false;
619        }
620
621        /**
622         * Calculates the bounding rectangle of a given time object.
623         *
624         * @param day The day to calculate bounds for
625         * @param rect The rectangle in which to store the bounds
626         */
627        private void getItemBounds(int day, Rect rect) {
628            final int offsetX = mPadding;
629            final int offsetY = MONTH_HEADER_SIZE;
630            final int cellHeight = mRowHeight;
631            final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays);
632            final int index = ((day - 1) + findDayOffset());
633            final int row = (index / mNumDays);
634            final int column = (index % mNumDays);
635            final int x = (offsetX + (column * cellWidth));
636            final int y = (offsetY + (row * cellHeight));
637
638            rect.set(x, y, (x + cellWidth), (y + cellHeight));
639        }
640
641        /**
642         * Generates a description for a given time object. Since this
643         * description will be spoken, the components are ordered by descending
644         * specificity as DAY MONTH YEAR.
645         *
646         * @param day The day to generate a description for
647         * @return A description of the time object
648         */
649        private CharSequence getItemDescription(int day) {
650            mTempCalendar.set(mYear, mMonth, day);
651            final CharSequence date = DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
652
653            if (day == mSelectedDay) {
654                return getContext().getString(R.string.item_is_selected, date);
655            }
656
657            return date;
658        }
659    }
660
661    /**
662     * Handles callbacks when the user clicks on a time object.
663     */
664    public interface OnDayClickListener {
665        public void onDayClick(SimpleMonthView view, CalendarDay day);
666    }
667}
668