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