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