SimpleMonthView.java revision ffb46bf2956d89e3190007ccf2ef3ce3eed005fe
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.Configuration;
22import android.content.res.Resources;
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.format.DateFormat;
31import android.text.format.DateUtils;
32import android.text.format.Time;
33import android.util.AttributeSet;
34import android.util.IntArray;
35import android.util.MathUtils;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.AccessibilityNodeInfo;
40
41import com.android.internal.R;
42import com.android.internal.widget.ExploreByTouchHelper;
43
44import java.text.SimpleDateFormat;
45import java.util.Calendar;
46import java.util.Formatter;
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 */
54class SimpleMonthView extends View {
55    private static final String TAG = "SimpleMonthView";
56
57    private static final int DEFAULT_HEIGHT = 32;
58    private static final int MIN_HEIGHT = 10;
59
60    private static final int DEFAULT_SELECTED_DAY = -1;
61    private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
62    private static final int DEFAULT_NUM_DAYS = 7;
63    private static final int DEFAULT_NUM_ROWS = 6;
64    private static final int MAX_NUM_ROWS = 6;
65
66    private static final int SELECTED_CIRCLE_ALPHA = 60;
67
68    private static final int DAY_SEPARATOR_WIDTH = 1;
69
70    private final int mMiniDayNumberTextSize;
71    private final int mMonthLabelTextSize;
72    private final int mMonthDayLabelTextSize;
73    private final int mMonthHeaderSize;
74    private final int mDaySelectedCircleSize;
75
76    // used for scaling to the device density
77    private static float mScale = 0;
78
79    /** Single-letter (when available) formatter for the day of week label. */
80    private SimpleDateFormat mDayFormatter = new SimpleDateFormat("EEEEE", Locale.getDefault());
81
82    // affects the padding on the sides of this view
83    private int mPadding = 0;
84
85    private String mDayOfWeekTypeface;
86    private String mMonthTitleTypeface;
87
88    private Paint mDayNumberPaint;
89    private Paint mDayNumberDisabledPaint;
90    private Paint mDayNumberSelectedPaint;
91
92    private Paint mMonthTitlePaint;
93    private Paint mMonthDayLabelPaint;
94
95    private final Formatter mFormatter;
96    private final StringBuilder mStringBuilder;
97
98    private int mMonth;
99    private int mYear;
100
101    // Quick reference to the width of this view, matches parent
102    private int mWidth;
103
104    // The height this view should draw at in pixels, set by height param
105    private int mRowHeight = DEFAULT_HEIGHT;
106
107    // If this view contains the today
108    private boolean mHasToday = false;
109
110    // Which day is selected [0-6] or -1 if no day is selected
111    private int mSelectedDay = -1;
112
113    // Which day is today [0-6] or -1 if no day is today
114    private int mToday = DEFAULT_SELECTED_DAY;
115
116    // Which day of the week to start on [0-6]
117    private int mWeekStart = DEFAULT_WEEK_START;
118
119    // How many days to display
120    private int mNumDays = DEFAULT_NUM_DAYS;
121
122    // The number of days + a spot for week number if it is displayed
123    private int mNumCells = mNumDays;
124
125    private int mDayOfWeekStart = 0;
126
127    // First enabled day
128    private int mEnabledDayStart = 1;
129
130    // Last enabled day
131    private int mEnabledDayEnd = 31;
132
133    private final Calendar mCalendar = Calendar.getInstance();
134    private final Calendar mDayLabelCalendar = Calendar.getInstance();
135
136    private final MonthViewTouchHelper mTouchHelper;
137
138    private int mNumRows = DEFAULT_NUM_ROWS;
139
140    // Optional listener for handling day click actions
141    private OnDayClickListener mOnDayClickListener;
142
143    // Whether to prevent setting the accessibility delegate
144    private boolean mLockAccessibilityDelegate;
145
146    private int mNormalTextColor;
147    private int mDisabledTextColor;
148    private int mSelectedDayColor;
149
150    public SimpleMonthView(Context context) {
151        this(context, null);
152    }
153
154    public SimpleMonthView(Context context, AttributeSet attrs) {
155        this(context, attrs, R.attr.datePickerStyle);
156    }
157
158    public SimpleMonthView(Context context, AttributeSet attrs, int defStyle) {
159        super(context, attrs);
160
161        final Resources res = context.getResources();
162
163        mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
164        mMonthTitleTypeface = res.getString(R.string.sans_serif);
165
166        mStringBuilder = new StringBuilder(50);
167        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
168
169        mMiniDayNumberTextSize = res.getDimensionPixelSize(R.dimen.datepicker_day_number_size);
170        mMonthLabelTextSize = res.getDimensionPixelSize(R.dimen.datepicker_month_label_size);
171        mMonthDayLabelTextSize = res.getDimensionPixelSize(
172                R.dimen.datepicker_month_day_label_text_size);
173        mMonthHeaderSize = res.getDimensionPixelOffset(
174                R.dimen.datepicker_month_list_item_header_height);
175        mDaySelectedCircleSize = res.getDimensionPixelSize(
176                R.dimen.datepicker_day_number_select_circle_radius);
177
178        mRowHeight = (res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height)
179                - mMonthHeaderSize) / MAX_NUM_ROWS;
180
181        // Set up accessibility components.
182        mTouchHelper = new MonthViewTouchHelper(this);
183        setAccessibilityDelegate(mTouchHelper);
184        setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
185        mLockAccessibilityDelegate = true;
186
187        // Sets up any standard paints that will be used
188        initView();
189    }
190
191    @Override
192    protected void onConfigurationChanged(Configuration newConfig) {
193        super.onConfigurationChanged(newConfig);
194
195        mDayFormatter = new SimpleDateFormat("EEEEE", newConfig.locale);
196    }
197
198    void setTextColor(ColorStateList colors) {
199        final Resources res = getContext().getResources();
200
201        mNormalTextColor = colors.getColorForState(ENABLED_STATE_SET,
202                res.getColor(R.color.datepicker_default_normal_text_color_holo_light));
203        mMonthTitlePaint.setColor(mNormalTextColor);
204        mMonthDayLabelPaint.setColor(mNormalTextColor);
205
206        mDisabledTextColor = colors.getColorForState(EMPTY_STATE_SET,
207                res.getColor(R.color.datepicker_default_disabled_text_color_holo_light));
208        mDayNumberDisabledPaint.setColor(mDisabledTextColor);
209
210        mSelectedDayColor = colors.getColorForState(ENABLED_SELECTED_STATE_SET,
211                res.getColor(R.color.holo_blue_light));
212        mDayNumberSelectedPaint.setColor(mSelectedDayColor);
213        mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA);
214    }
215
216    @Override
217    public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
218        // Workaround for a JB MR1 issue where accessibility delegates on
219        // top-level ListView items are overwritten.
220        if (!mLockAccessibilityDelegate) {
221            super.setAccessibilityDelegate(delegate);
222        }
223    }
224
225    public void setOnDayClickListener(OnDayClickListener listener) {
226        mOnDayClickListener = listener;
227    }
228
229    @Override
230    public boolean dispatchHoverEvent(MotionEvent event) {
231        // First right-of-refusal goes the touch exploration helper.
232        if (mTouchHelper.dispatchHoverEvent(event)) {
233            return true;
234        }
235        return super.dispatchHoverEvent(event);
236    }
237
238    @Override
239    public boolean onTouchEvent(MotionEvent event) {
240        switch (event.getAction()) {
241            case MotionEvent.ACTION_UP:
242                final int day = getDayFromLocation(event.getX(), event.getY());
243                if (day >= 0) {
244                    onDayClick(day);
245                }
246                break;
247        }
248        return true;
249    }
250
251    /**
252     * Sets up the text and style properties for painting.
253     */
254    private void initView() {
255        mMonthTitlePaint = new Paint();
256        mMonthTitlePaint.setAntiAlias(true);
257        mMonthTitlePaint.setColor(mNormalTextColor);
258        mMonthTitlePaint.setTextSize(mMonthLabelTextSize);
259        mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
260        mMonthTitlePaint.setTextAlign(Align.CENTER);
261        mMonthTitlePaint.setStyle(Style.FILL);
262        mMonthTitlePaint.setFakeBoldText(true);
263
264        mMonthDayLabelPaint = new Paint();
265        mMonthDayLabelPaint.setAntiAlias(true);
266        mMonthDayLabelPaint.setColor(mNormalTextColor);
267        mMonthDayLabelPaint.setTextSize(mMonthDayLabelTextSize);
268        mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
269        mMonthDayLabelPaint.setTextAlign(Align.CENTER);
270        mMonthDayLabelPaint.setStyle(Style.FILL);
271        mMonthDayLabelPaint.setFakeBoldText(true);
272
273        mDayNumberSelectedPaint = new Paint();
274        mDayNumberSelectedPaint.setAntiAlias(true);
275        mDayNumberSelectedPaint.setColor(mSelectedDayColor);
276        mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA);
277        mDayNumberSelectedPaint.setTextAlign(Align.CENTER);
278        mDayNumberSelectedPaint.setStyle(Style.FILL);
279        mDayNumberSelectedPaint.setFakeBoldText(true);
280
281        mDayNumberPaint = new Paint();
282        mDayNumberPaint.setAntiAlias(true);
283        mDayNumberPaint.setTextSize(mMiniDayNumberTextSize);
284        mDayNumberPaint.setTextAlign(Align.CENTER);
285        mDayNumberPaint.setStyle(Style.FILL);
286        mDayNumberPaint.setFakeBoldText(false);
287
288        mDayNumberDisabledPaint = new Paint();
289        mDayNumberDisabledPaint.setAntiAlias(true);
290        mDayNumberDisabledPaint.setColor(mDisabledTextColor);
291        mDayNumberDisabledPaint.setTextSize(mMiniDayNumberTextSize);
292        mDayNumberDisabledPaint.setTextAlign(Align.CENTER);
293        mDayNumberDisabledPaint.setStyle(Style.FILL);
294        mDayNumberDisabledPaint.setFakeBoldText(false);
295    }
296
297    @Override
298    protected void onDraw(Canvas canvas) {
299        drawMonthTitle(canvas);
300        drawWeekDayLabels(canvas);
301        drawDays(canvas);
302    }
303
304    private static boolean isValidDayOfWeek(int day) {
305        return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
306    }
307
308    private static boolean isValidMonth(int month) {
309        return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
310    }
311
312    /**
313     * Sets all the parameters for displaying this week. Parameters have a default value and
314     * will only update if a new value is included, except for focus month, which will always
315     * default to no focus month if no value is passed in. The only required parameter is the
316     * week start.
317     *
318     * @param selectedDay the selected day of the month, or -1 for no selection.
319     * @param month the month.
320     * @param year the year.
321     * @param weekStart which day the week should start on. {@link Calendar#SUNDAY} through
322     *        {@link Calendar#SATURDAY}.
323     * @param enabledDayStart the first enabled day.
324     * @param enabledDayEnd the last enabled day.
325     */
326    void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
327            int enabledDayEnd) {
328        if (mRowHeight < MIN_HEIGHT) {
329            mRowHeight = MIN_HEIGHT;
330        }
331
332        mSelectedDay = selectedDay;
333
334        if (isValidMonth(month)) {
335            mMonth = month;
336        }
337        mYear = year;
338
339        // Figure out what day today is
340        final Time today = new Time(Time.getCurrentTimezone());
341        today.setToNow();
342        mHasToday = false;
343        mToday = -1;
344
345        mCalendar.set(Calendar.MONTH, mMonth);
346        mCalendar.set(Calendar.YEAR, mYear);
347        mCalendar.set(Calendar.DAY_OF_MONTH, 1);
348        mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
349
350        if (isValidDayOfWeek(weekStart)) {
351            mWeekStart = weekStart;
352        } else {
353            mWeekStart = mCalendar.getFirstDayOfWeek();
354        }
355
356        if (enabledDayStart > 0 && enabledDayEnd < 32) {
357            mEnabledDayStart = enabledDayStart;
358        }
359        if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) {
360            mEnabledDayEnd = enabledDayEnd;
361        }
362
363        mNumCells = getDaysInMonth(mMonth, mYear);
364        for (int i = 0; i < mNumCells; i++) {
365            final int day = i + 1;
366            if (sameDay(day, today)) {
367                mHasToday = true;
368                mToday = day;
369            }
370        }
371        mNumRows = calculateNumRows();
372
373        // Invalidate cached accessibility information.
374        mTouchHelper.invalidateRoot();
375    }
376
377    private static int getDaysInMonth(int month, int year) {
378        switch (month) {
379            case Calendar.JANUARY:
380            case Calendar.MARCH:
381            case Calendar.MAY:
382            case Calendar.JULY:
383            case Calendar.AUGUST:
384            case Calendar.OCTOBER:
385            case Calendar.DECEMBER:
386                return 31;
387            case Calendar.APRIL:
388            case Calendar.JUNE:
389            case Calendar.SEPTEMBER:
390            case Calendar.NOVEMBER:
391                return 30;
392            case Calendar.FEBRUARY:
393                return (year % 4 == 0) ? 29 : 28;
394            default:
395                throw new IllegalArgumentException("Invalid Month");
396        }
397    }
398
399    public void reuse() {
400        mNumRows = DEFAULT_NUM_ROWS;
401        requestLayout();
402    }
403
404    private int calculateNumRows() {
405        int offset = findDayOffset();
406        int dividend = (offset + mNumCells) / mNumDays;
407        int remainder = (offset + mNumCells) % mNumDays;
408        return (dividend + (remainder > 0 ? 1 : 0));
409    }
410
411    private boolean sameDay(int day, Time today) {
412        return mYear == today.year &&
413                mMonth == today.month &&
414                day == today.monthDay;
415    }
416
417    @Override
418    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
419        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
420                + mMonthHeaderSize);
421    }
422
423    @Override
424    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
425        mWidth = w;
426
427        // Invalidate cached accessibility information.
428        mTouchHelper.invalidateRoot();
429    }
430
431    private String getMonthAndYearString() {
432        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
433                | DateUtils.FORMAT_NO_MONTH_DAY;
434        mStringBuilder.setLength(0);
435        long millis = mCalendar.getTimeInMillis();
436        return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
437                Time.getCurrentTimezone()).toString();
438    }
439
440    private void drawMonthTitle(Canvas canvas) {
441        final float x = (mWidth + 2 * mPadding) / 2f;
442        final float y = (mMonthHeaderSize - mMonthDayLabelTextSize) / 2f;
443        canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
444    }
445
446    private void drawWeekDayLabels(Canvas canvas) {
447        final int y = mMonthHeaderSize - (mMonthDayLabelTextSize / 2);
448        final int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
449
450        for (int i = 0; i < mNumDays; i++) {
451            final int calendarDay = (i + mWeekStart) % mNumDays;
452            mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
453
454            final String dayLabel = mDayFormatter.format(mDayLabelCalendar.getTime());
455            final int x = (2 * i + 1) * dayWidthHalf + mPadding;
456            canvas.drawText(dayLabel, x, y, mMonthDayLabelPaint);
457        }
458    }
459
460    /**
461     * Draws the month days.
462     */
463    private void drawDays(Canvas canvas) {
464        int y = (((mRowHeight + mMiniDayNumberTextSize) / 2) - DAY_SEPARATOR_WIDTH)
465                + mMonthHeaderSize;
466        int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
467        int j = findDayOffset();
468        for (int day = 1; day <= mNumCells; day++) {
469            int x = (2 * j + 1) * dayWidthHalf + mPadding;
470            if (mSelectedDay == day) {
471                canvas.drawCircle(x, y - (mMiniDayNumberTextSize / 3), mDaySelectedCircleSize,
472                        mDayNumberSelectedPaint);
473            }
474
475            if (mHasToday && mToday == day) {
476                mDayNumberPaint.setColor(mSelectedDayColor);
477            } else {
478                mDayNumberPaint.setColor(mNormalTextColor);
479            }
480            final Paint paint = (day < mEnabledDayStart || day > mEnabledDayEnd) ?
481                    mDayNumberDisabledPaint : mDayNumberPaint;
482            canvas.drawText(String.format("%d", day), x, y, paint);
483            j++;
484            if (j == mNumDays) {
485                j = 0;
486                y += mRowHeight;
487            }
488        }
489    }
490
491    private int findDayOffset() {
492        return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
493                - mWeekStart;
494    }
495
496    /**
497     * Calculates the day that the given x position is in, accounting for week
498     * number. Returns the day or -1 if the position wasn't in a day.
499     *
500     * @param x The x position of the touch event
501     * @return The day number, or -1 if the position wasn't in a day
502     */
503    private int getDayFromLocation(float x, float y) {
504        int dayStart = mPadding;
505        if (x < dayStart || x > mWidth - mPadding) {
506            return -1;
507        }
508        // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
509        int row = (int) (y - mMonthHeaderSize) / mRowHeight;
510        int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
511
512        int day = column - findDayOffset() + 1;
513        day += row * mNumDays;
514        if (day < 1 || day > mNumCells) {
515            return -1;
516        }
517        return day;
518    }
519
520    /**
521     * Called when the user clicks on a day. Handles callbacks to the
522     * {@link OnDayClickListener} if one is set.
523     *
524     * @param day The day that was clicked
525     */
526    private void onDayClick(int day) {
527        if (mOnDayClickListener != null) {
528            Calendar date = Calendar.getInstance();
529            date.set(mYear, mMonth, day);
530            mOnDayClickListener.onDayClick(this, date);
531        }
532
533        // This is a no-op if accessibility is turned off.
534        mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
535    }
536
537    /**
538     * @return The date that has accessibility focus, or {@code null} if no date
539     *         has focus
540     */
541    Calendar getAccessibilityFocus() {
542        final int day = mTouchHelper.getFocusedVirtualView();
543        Calendar date = null;
544        if (day >= 0) {
545            date = Calendar.getInstance();
546            date.set(mYear, mMonth, day);
547        }
548        return date;
549    }
550
551    /**
552     * Clears accessibility focus within the view. No-op if the view does not
553     * contain accessibility focus.
554     */
555    public void clearAccessibilityFocus() {
556        mTouchHelper.clearFocusedVirtualView();
557    }
558
559    /**
560     * Attempts to restore accessibility focus to the specified date.
561     *
562     * @param day The date which should receive focus
563     * @return {@code false} if the date is not valid for this month view, or
564     *         {@code true} if the date received focus
565     */
566    boolean restoreAccessibilityFocus(Calendar day) {
567        if ((day.get(Calendar.YEAR) != mYear) || (day.get(Calendar.MONTH) != mMonth) ||
568                (day.get(Calendar.DAY_OF_MONTH) > mNumCells)) {
569            return false;
570        }
571        mTouchHelper.setFocusedVirtualView(day.get(Calendar.DAY_OF_MONTH));
572        return true;
573    }
574
575    /**
576     * Provides a virtual view hierarchy for interfacing with an accessibility
577     * service.
578     */
579    private class MonthViewTouchHelper extends ExploreByTouchHelper {
580        private static final String DATE_FORMAT = "dd MMMM yyyy";
581
582        private final Rect mTempRect = new Rect();
583        private final Calendar mTempCalendar = Calendar.getInstance();
584
585        public MonthViewTouchHelper(View host) {
586            super(host);
587        }
588
589        public void setFocusedVirtualView(int virtualViewId) {
590            getAccessibilityNodeProvider(SimpleMonthView.this).performAction(
591                    virtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
592        }
593
594        public void clearFocusedVirtualView() {
595            final int focusedVirtualView = getFocusedVirtualView();
596            if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
597                getAccessibilityNodeProvider(SimpleMonthView.this).performAction(
598                        focusedVirtualView,
599                        AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
600                        null);
601            }
602        }
603
604        @Override
605        protected int getVirtualViewAt(float x, float y) {
606            final int day = getDayFromLocation(x, y);
607            if (day >= 0) {
608                return day;
609            }
610            return ExploreByTouchHelper.INVALID_ID;
611        }
612
613        @Override
614        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
615            for (int day = 1; day <= mNumCells; day++) {
616                virtualViewIds.add(day);
617            }
618        }
619
620        @Override
621        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
622            event.setContentDescription(getItemDescription(virtualViewId));
623        }
624
625        @Override
626        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
627            getItemBounds(virtualViewId, mTempRect);
628
629            node.setContentDescription(getItemDescription(virtualViewId));
630            node.setBoundsInParent(mTempRect);
631            node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
632
633            if (virtualViewId == mSelectedDay) {
634                node.setSelected(true);
635            }
636
637        }
638
639        @Override
640        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
641                Bundle arguments) {
642            switch (action) {
643                case AccessibilityNodeInfo.ACTION_CLICK:
644                    onDayClick(virtualViewId);
645                    return true;
646            }
647
648            return false;
649        }
650
651        /**
652         * Calculates the bounding rectangle of a given time object.
653         *
654         * @param day The day to calculate bounds for
655         * @param rect The rectangle in which to store the bounds
656         */
657        private void getItemBounds(int day, Rect rect) {
658            final int offsetX = mPadding;
659            final int offsetY = mMonthHeaderSize;
660            final int cellHeight = mRowHeight;
661            final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays);
662            final int index = ((day - 1) + findDayOffset());
663            final int row = (index / mNumDays);
664            final int column = (index % mNumDays);
665            final int x = (offsetX + (column * cellWidth));
666            final int y = (offsetY + (row * cellHeight));
667
668            rect.set(x, y, (x + cellWidth), (y + cellHeight));
669        }
670
671        /**
672         * Generates a description for a given time object. Since this
673         * description will be spoken, the components are ordered by descending
674         * specificity as DAY MONTH YEAR.
675         *
676         * @param day The day to generate a description for
677         * @return A description of the time object
678         */
679        private CharSequence getItemDescription(int day) {
680            mTempCalendar.set(mYear, mMonth, day);
681            final CharSequence date = DateFormat.format(DATE_FORMAT,
682                    mTempCalendar.getTimeInMillis());
683
684            if (day == mSelectedDay) {
685                return getContext().getString(R.string.item_is_selected, date);
686            }
687
688            return date;
689        }
690    }
691
692    /**
693     * Handles callbacks when the user clicks on a time object.
694     */
695    public interface OnDayClickListener {
696        public void onDayClick(SimpleMonthView view, Calendar day);
697    }
698}
699