SimpleMonthView.java revision 1ea4715cecf8d1a17a21f3d21ea4df56519e5ea6
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.Resources;
22import android.content.res.TypedArray;
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.TextPaint;
31import android.text.format.DateFormat;
32import android.util.AttributeSet;
33import android.util.IntArray;
34import android.util.MathUtils;
35import android.util.StateSet;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.AccessibilityNodeInfo;
40import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
41
42import com.android.internal.R;
43import com.android.internal.widget.ExploreByTouchHelper;
44
45import java.text.NumberFormat;
46import java.text.SimpleDateFormat;
47import java.util.Calendar;
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 DAYS_IN_WEEK = 7;
56    private static final int MAX_WEEKS_IN_MONTH = 6;
57
58    private static final int DEFAULT_SELECTED_DAY = -1;
59    private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
60
61    private static final String DEFAULT_TITLE_FORMAT = "MMMMy";
62    private static final String DAY_OF_WEEK_FORMAT = "EEEEE";
63
64    private final TextPaint mMonthPaint = new TextPaint();
65    private final TextPaint mDayOfWeekPaint = new TextPaint();
66    private final TextPaint mDayPaint = new TextPaint();
67    private final Paint mDaySelectorPaint = new Paint();
68    private final Paint mDayHighlightPaint = new Paint();
69
70    private final Calendar mCalendar = Calendar.getInstance();
71    private final Calendar mDayOfWeekLabelCalendar = Calendar.getInstance();
72
73    private final MonthViewTouchHelper mTouchHelper;
74
75    private final SimpleDateFormat mTitleFormatter;
76    private final SimpleDateFormat mDayOfWeekFormatter;
77    private final NumberFormat mDayFormatter;
78
79    // Desired dimensions.
80    private final int mDesiredMonthHeight;
81    private final int mDesiredDayOfWeekHeight;
82    private final int mDesiredDayHeight;
83    private final int mDesiredCellWidth;
84    private final int mDesiredDaySelectorRadius;
85
86    private CharSequence mTitle;
87
88    private int mMonth;
89    private int mYear;
90
91    // Dimensions as laid out.
92    private int mMonthHeight;
93    private int mDayOfWeekHeight;
94    private int mDayHeight;
95    private int mCellWidth;
96    private int mDaySelectorRadius;
97
98    private int mPaddedWidth;
99    private int mPaddedHeight;
100
101    /** The day of month for the selected day, or -1 if no day is selected. */
102    private int mActivatedDay = -1;
103
104    /**
105     * The day of month for today, or -1 if the today is not in the current
106     * month.
107     */
108    private int mToday = DEFAULT_SELECTED_DAY;
109
110    /** The first day of the week (ex. Calendar.SUNDAY). */
111    private int mWeekStart = DEFAULT_WEEK_START;
112
113    /** The number of days (ex. 28) in the current month. */
114    private int mDaysInMonth;
115
116    /**
117     * The day of week (ex. Calendar.SUNDAY) for the first day of the current
118     * month.
119     */
120    private int mDayOfWeekStart;
121
122    /** The day of month for the first (inclusive) enabled day. */
123    private int mEnabledDayStart = 1;
124
125    /** The day of month for the last (inclusive) enabled day. */
126    private int mEnabledDayEnd = 31;
127
128    /** Optional listener for handling day click actions. */
129    private OnDayClickListener mOnDayClickListener;
130
131    private ColorStateList mDayTextColor;
132
133    private int mTouchedItem = -1;
134
135    public SimpleMonthView(Context context) {
136        this(context, null);
137    }
138
139    public SimpleMonthView(Context context, AttributeSet attrs) {
140        this(context, attrs, R.attr.datePickerStyle);
141    }
142
143    public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
144        this(context, attrs, defStyleAttr, 0);
145    }
146
147    public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
148        super(context, attrs, defStyleAttr, defStyleRes);
149
150        final Resources res = context.getResources();
151        mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
152        mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
153        mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
154        mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
155        mDesiredDaySelectorRadius = res.getDimensionPixelSize(
156                R.dimen.date_picker_day_selector_radius);
157
158        // Set up accessibility components.
159        mTouchHelper = new MonthViewTouchHelper(this);
160        setAccessibilityDelegate(mTouchHelper);
161        setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
162
163        final Locale locale = res.getConfiguration().locale;
164        final String titleFormat = DateFormat.getBestDateTimePattern(locale, DEFAULT_TITLE_FORMAT);
165        mTitleFormatter = new SimpleDateFormat(titleFormat, locale);
166        mDayOfWeekFormatter = new SimpleDateFormat(DAY_OF_WEEK_FORMAT, locale);
167        mDayFormatter = NumberFormat.getIntegerInstance(locale);
168
169        initPaints(res);
170    }
171
172    /**
173     * Applies the specified text appearance resource to a paint, returning the
174     * text color if one is set in the text appearance.
175     *
176     * @param p the paint to modify
177     * @param resId the resource ID of the text appearance
178     * @return the text color, if available
179     */
180    private ColorStateList applyTextAppearance(Paint p, int resId) {
181        final TypedArray ta = mContext.obtainStyledAttributes(null,
182                R.styleable.TextAppearance, 0, resId);
183
184        final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
185        if (fontFamily != null) {
186            p.setTypeface(Typeface.create(fontFamily, 0));
187        }
188
189        p.setTextSize(ta.getDimensionPixelSize(
190                R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
191
192        final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
193        if (textColor != null) {
194            final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
195            p.setColor(enabledColor);
196        }
197
198        ta.recycle();
199
200        return textColor;
201    }
202
203    public int getMonthHeight() {
204        return mMonthHeight;
205    }
206
207    public int getCellWidth() {
208        return mCellWidth;
209    }
210
211    public void setMonthTextAppearance(int resId) {
212        applyTextAppearance(mMonthPaint, resId);
213
214        invalidate();
215    }
216
217    public void setDayOfWeekTextAppearance(int resId) {
218        applyTextAppearance(mDayOfWeekPaint, resId);
219        invalidate();
220    }
221
222    public void setDayTextAppearance(int resId) {
223        final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
224        if (textColor != null) {
225            mDayTextColor = textColor;
226        }
227
228        invalidate();
229    }
230
231    public CharSequence getTitle() {
232        if (mTitle == null) {
233            mTitle = mTitleFormatter.format(mCalendar.getTime());
234        }
235        return mTitle;
236    }
237
238    /**
239     * Sets up the text and style properties for painting.
240     */
241    private void initPaints(Resources res) {
242        final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
243        final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
244        final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
245
246        final int monthTextSize = res.getDimensionPixelSize(
247                R.dimen.date_picker_month_text_size);
248        final int dayOfWeekTextSize = res.getDimensionPixelSize(
249                R.dimen.date_picker_day_of_week_text_size);
250        final int dayTextSize = res.getDimensionPixelSize(
251                R.dimen.date_picker_day_text_size);
252
253        mMonthPaint.setAntiAlias(true);
254        mMonthPaint.setTextSize(monthTextSize);
255        mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
256        mMonthPaint.setTextAlign(Align.CENTER);
257        mMonthPaint.setStyle(Style.FILL);
258
259        mDayOfWeekPaint.setAntiAlias(true);
260        mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
261        mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
262        mDayOfWeekPaint.setTextAlign(Align.CENTER);
263        mDayOfWeekPaint.setStyle(Style.FILL);
264
265        mDaySelectorPaint.setAntiAlias(true);
266        mDaySelectorPaint.setStyle(Style.FILL);
267
268        mDayHighlightPaint.setAntiAlias(true);
269        mDayHighlightPaint.setStyle(Style.FILL);
270
271        mDayPaint.setAntiAlias(true);
272        mDayPaint.setTextSize(dayTextSize);
273        mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
274        mDayPaint.setTextAlign(Align.CENTER);
275        mDayPaint.setStyle(Style.FILL);
276    }
277
278    void setMonthTextColor(ColorStateList monthTextColor) {
279        final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
280        mMonthPaint.setColor(enabledColor);
281        invalidate();
282    }
283
284    void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
285        final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
286        mDayOfWeekPaint.setColor(enabledColor);
287        invalidate();
288    }
289
290    void setDayTextColor(ColorStateList dayTextColor) {
291        mDayTextColor = dayTextColor;
292        invalidate();
293    }
294
295    void setDaySelectorColor(ColorStateList dayBackgroundColor) {
296        final int activatedColor = dayBackgroundColor.getColorForState(
297                StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
298        mDaySelectorPaint.setColor(activatedColor);
299        invalidate();
300    }
301
302    void setDayHighlightColor(ColorStateList dayHighlightColor) {
303        final int pressedColor = dayHighlightColor.getColorForState(
304                StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
305        mDayHighlightPaint.setColor(pressedColor);
306        invalidate();
307    }
308
309    public void setOnDayClickListener(OnDayClickListener listener) {
310        mOnDayClickListener = listener;
311    }
312
313    @Override
314    public boolean dispatchHoverEvent(MotionEvent event) {
315        // First right-of-refusal goes the touch exploration helper.
316        return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
317    }
318
319    @Override
320    public boolean onTouchEvent(MotionEvent event) {
321        final int x = (int) (event.getX() + 0.5f);
322        final int y = (int) (event.getY() + 0.5f);
323
324        final int action = event.getAction();
325        switch (action) {
326            case MotionEvent.ACTION_DOWN:
327            case MotionEvent.ACTION_MOVE:
328                final int touchedItem = getDayAtLocation(x, y);
329                if (mTouchedItem != touchedItem) {
330                    mTouchedItem = touchedItem;
331                    invalidate();
332                }
333                if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) {
334                    // Touch something that's not an item, reject event.
335                    return false;
336                }
337                break;
338
339            case MotionEvent.ACTION_UP:
340                final int clickedDay = getDayAtLocation(x, y);
341                onDayClicked(clickedDay);
342                // Fall through.
343            case MotionEvent.ACTION_CANCEL:
344                // Reset touched day on stream end.
345                mTouchedItem = -1;
346                invalidate();
347                break;
348        }
349        return true;
350    }
351
352    @Override
353    protected void onDraw(Canvas canvas) {
354        final int paddingLeft = getPaddingLeft();
355        final int paddingTop = getPaddingTop();
356        canvas.translate(paddingLeft, paddingTop);
357
358        drawMonth(canvas);
359        drawDaysOfWeek(canvas);
360        drawDays(canvas);
361
362        canvas.translate(-paddingLeft, -paddingTop);
363    }
364
365    private void drawMonth(Canvas canvas) {
366        final float x = mPaddedWidth / 2f;
367
368        // Vertically centered within the month header height.
369        final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
370        final float y = (mMonthHeight - lineHeight) / 2f;
371
372        canvas.drawText(getTitle().toString(), x, y, mMonthPaint);
373    }
374
375    private void drawDaysOfWeek(Canvas canvas) {
376        final TextPaint p = mDayOfWeekPaint;
377        final int headerHeight = mMonthHeight;
378        final int rowHeight = mDayOfWeekHeight;
379        final int colWidth = mCellWidth;
380
381        // Text is vertically centered within the day of week height.
382        final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
383        final int rowCenter = headerHeight + rowHeight / 2;
384
385        for (int col = 0; col < DAYS_IN_WEEK; col++) {
386            final int colCenter = colWidth * col + colWidth / 2;
387            final int colCenterRtl;
388            if (isLayoutRtl()) {
389                colCenterRtl = mPaddedWidth - colCenter;
390            } else {
391                colCenterRtl = colCenter;
392            }
393
394            final int dayOfWeek = (col + mWeekStart) % DAYS_IN_WEEK;
395            final String label = getDayOfWeekLabel(dayOfWeek);
396            canvas.drawText(label, colCenterRtl, rowCenter - halfLineHeight, p);
397        }
398    }
399
400    private String getDayOfWeekLabel(int dayOfWeek) {
401        mDayOfWeekLabelCalendar.set(Calendar.DAY_OF_WEEK, dayOfWeek);
402        return mDayOfWeekFormatter.format(mDayOfWeekLabelCalendar.getTime());
403    }
404
405    /**
406     * Draws the month days.
407     */
408    private void drawDays(Canvas canvas) {
409        final TextPaint p = mDayPaint;
410        final int headerHeight = mMonthHeight + mDayOfWeekHeight;
411        final int rowHeight = mDayHeight;
412        final int colWidth = mCellWidth;
413
414        // Text is vertically centered within the row height.
415        final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
416        int rowCenter = headerHeight + rowHeight / 2;
417
418        for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) {
419            final int colCenter = colWidth * col + colWidth / 2;
420            final int colCenterRtl;
421            if (isLayoutRtl()) {
422                colCenterRtl = mPaddedWidth - colCenter;
423            } else {
424                colCenterRtl = colCenter;
425            }
426
427            int stateMask = 0;
428
429            final boolean isDayEnabled = isDayEnabled(day);
430            if (isDayEnabled) {
431                stateMask |= StateSet.VIEW_STATE_ENABLED;
432            }
433
434            final boolean isDayActivated = mActivatedDay == day;
435            if (isDayActivated) {
436                stateMask |= StateSet.VIEW_STATE_ACTIVATED;
437
438                // Adjust the circle to be centered on the row.
439                canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, mDaySelectorPaint);
440            } else if (mTouchedItem == day) {
441                stateMask |= StateSet.VIEW_STATE_PRESSED;
442
443                if (isDayEnabled) {
444                    // Adjust the circle to be centered on the row.
445                    canvas.drawCircle(colCenterRtl, rowCenter,
446                            mDaySelectorRadius, mDayHighlightPaint);
447                }
448            }
449
450            final boolean isDayToday = mToday == day;
451            final int dayTextColor;
452            if (isDayToday && !isDayActivated) {
453                dayTextColor = mDaySelectorPaint.getColor();
454            } else {
455                final int[] stateSet = StateSet.get(stateMask);
456                dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
457            }
458            p.setColor(dayTextColor);
459
460            canvas.drawText(mDayFormatter.format(day), colCenterRtl, rowCenter - halfLineHeight, p);
461
462            col++;
463
464            if (col == DAYS_IN_WEEK) {
465                col = 0;
466                rowCenter += rowHeight;
467            }
468        }
469    }
470
471    private boolean isDayEnabled(int day) {
472        return day >= mEnabledDayStart && day <= mEnabledDayEnd;
473    }
474
475    private boolean isValidDayOfMonth(int day) {
476        return day >= 1 && day <= mDaysInMonth;
477    }
478
479    private static boolean isValidDayOfWeek(int day) {
480        return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
481    }
482
483    private static boolean isValidMonth(int month) {
484        return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
485    }
486
487    /**
488     * Sets the selected day.
489     *
490     * @param dayOfMonth the selected day of the month, or {@code -1} to clear
491     *                   the selection
492     */
493    public void setSelectedDay(int dayOfMonth) {
494        mActivatedDay = dayOfMonth;
495
496        // Invalidate cached accessibility information.
497        mTouchHelper.invalidateRoot();
498        invalidate();
499    }
500
501    /**
502     * Sets the first day of the week.
503     *
504     * @param weekStart which day the week should start on, valid values are
505     *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
506     */
507    public void setFirstDayOfWeek(int weekStart) {
508        if (isValidDayOfWeek(weekStart)) {
509            mWeekStart = weekStart;
510        } else {
511            mWeekStart = mCalendar.getFirstDayOfWeek();
512        }
513
514        // Invalidate cached accessibility information.
515        mTouchHelper.invalidateRoot();
516        invalidate();
517    }
518
519    /**
520     * Sets all the parameters for displaying this week.
521     * <p>
522     * Parameters have a default value and will only update if a new value is
523     * included, except for focus month, which will always default to no focus
524     * month if no value is passed in. The only required parameter is the week
525     * start.
526     *
527     * @param selectedDay the selected day of the month, or -1 for no selection
528     * @param month the month
529     * @param year the year
530     * @param weekStart which day the week should start on, valid values are
531     *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
532     * @param enabledDayStart the first enabled day
533     * @param enabledDayEnd the last enabled day
534     */
535    void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
536            int enabledDayEnd) {
537        mActivatedDay = selectedDay;
538
539        if (isValidMonth(month)) {
540            mMonth = month;
541        }
542        mYear = year;
543
544        mCalendar.set(Calendar.MONTH, mMonth);
545        mCalendar.set(Calendar.YEAR, mYear);
546        mCalendar.set(Calendar.DAY_OF_MONTH, 1);
547        mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
548
549        if (isValidDayOfWeek(weekStart)) {
550            mWeekStart = weekStart;
551        } else {
552            mWeekStart = mCalendar.getFirstDayOfWeek();
553        }
554
555        // Figure out what day today is.
556        final Calendar today = Calendar.getInstance();
557        mToday = -1;
558        mDaysInMonth = getDaysInMonth(mMonth, mYear);
559        for (int i = 0; i < mDaysInMonth; i++) {
560            final int day = i + 1;
561            if (sameDay(day, today)) {
562                mToday = day;
563            }
564        }
565
566        mEnabledDayStart = MathUtils.constrain(enabledDayStart, 1, mDaysInMonth);
567        mEnabledDayEnd = MathUtils.constrain(enabledDayEnd, mEnabledDayStart, mDaysInMonth);
568
569        // Invalidate the old title.
570        mTitle = null;
571
572        // Invalidate cached accessibility information.
573        mTouchHelper.invalidateRoot();
574    }
575
576    private static int getDaysInMonth(int month, int year) {
577        switch (month) {
578            case Calendar.JANUARY:
579            case Calendar.MARCH:
580            case Calendar.MAY:
581            case Calendar.JULY:
582            case Calendar.AUGUST:
583            case Calendar.OCTOBER:
584            case Calendar.DECEMBER:
585                return 31;
586            case Calendar.APRIL:
587            case Calendar.JUNE:
588            case Calendar.SEPTEMBER:
589            case Calendar.NOVEMBER:
590                return 30;
591            case Calendar.FEBRUARY:
592                return (year % 4 == 0) ? 29 : 28;
593            default:
594                throw new IllegalArgumentException("Invalid Month");
595        }
596    }
597
598    private boolean sameDay(int day, Calendar today) {
599        return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
600                && day == today.get(Calendar.DAY_OF_MONTH);
601    }
602
603    @Override
604    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
605        final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH
606                + mDesiredDayOfWeekHeight + mDesiredMonthHeight
607                + getPaddingTop() + getPaddingBottom();
608        final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK
609                + getPaddingStart() + getPaddingEnd();
610        final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
611        final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
612        setMeasuredDimension(resolvedWidth, resolvedHeight);
613    }
614
615    @Override
616    public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
617        super.onRtlPropertiesChanged(layoutDirection);
618
619        requestLayout();
620    }
621
622    @Override
623    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
624        if (!changed) {
625            return;
626        }
627
628        // Let's initialize a completely reasonable number of variables.
629        final int w = right - left;
630        final int h = bottom - top;
631        final int paddingLeft = getPaddingLeft();
632        final int paddingTop = getPaddingTop();
633        final int paddingRight = getPaddingRight();
634        final int paddingBottom = getPaddingBottom();
635        final int paddedRight = w - paddingRight;
636        final int paddedBottom = h - paddingBottom;
637        final int paddedWidth = paddedRight - paddingLeft;
638        final int paddedHeight = paddedBottom - paddingTop;
639        if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) {
640            return;
641        }
642
643        mPaddedWidth = paddedWidth;
644        mPaddedHeight = paddedHeight;
645
646        // We may have been laid out smaller than our preferred size. If so,
647        // scale all dimensions to fit.
648        final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom;
649        final float scaleH = paddedHeight / (float) measuredPaddedHeight;
650        final int monthHeight = (int) (mDesiredMonthHeight * scaleH);
651        final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
652        mMonthHeight = monthHeight;
653        mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH);
654        mDayHeight = (int) (mDesiredDayHeight * scaleH);
655        mCellWidth = cellWidth;
656
657        // Compute the largest day selector radius that's still within the clip
658        // bounds and desired selector radius.
659        final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
660        final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
661        mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
662                Math.min(maxSelectorWidth, maxSelectorHeight));
663
664        // Invalidate cached accessibility information.
665        mTouchHelper.invalidateRoot();
666    }
667
668    private int findDayOffset() {
669        final int offset = mDayOfWeekStart - mWeekStart;
670        if (mDayOfWeekStart < mWeekStart) {
671            return offset + DAYS_IN_WEEK;
672        }
673        return offset;
674    }
675
676    /**
677     * Calculates the day of the month at the specified touch position. Returns
678     * the day of the month or -1 if the position wasn't in a valid day.
679     *
680     * @param x the x position of the touch event
681     * @param y the y position of the touch event
682     * @return the day of the month at (x, y), or -1 if the position wasn't in
683     *         a valid day
684     */
685    private int getDayAtLocation(int x, int y) {
686        final int paddedX = x - getPaddingLeft();
687        if (paddedX < 0 || paddedX >= mPaddedWidth) {
688            return -1;
689        }
690
691        final int headerHeight = mMonthHeight + mDayOfWeekHeight;
692        final int paddedY = y - getPaddingTop();
693        if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
694            return -1;
695        }
696
697        // Adjust for RTL after applying padding.
698        final int paddedXRtl;
699        if (isLayoutRtl()) {
700            paddedXRtl = mPaddedWidth - paddedX;
701        } else {
702            paddedXRtl = paddedX;
703        }
704
705        final int row = (paddedY - headerHeight) / mDayHeight;
706        final int col = (paddedXRtl * DAYS_IN_WEEK) / mPaddedWidth;
707        final int index = col + row * DAYS_IN_WEEK;
708        final int day = index + 1 - findDayOffset();
709        if (!isValidDayOfMonth(day)) {
710            return -1;
711        }
712
713        return day;
714    }
715
716    /**
717     * Calculates the bounds of the specified day.
718     *
719     * @param id the day of the month
720     * @param outBounds the rect to populate with bounds
721     */
722    private boolean getBoundsForDay(int id, Rect outBounds) {
723        if (!isValidDayOfMonth(id)) {
724            return false;
725        }
726
727        final int index = id - 1 + findDayOffset();
728
729        // Compute left edge, taking into account RTL.
730        final int col = index % DAYS_IN_WEEK;
731        final int colWidth = mCellWidth;
732        final int left;
733        if (isLayoutRtl()) {
734            left = getWidth() - getPaddingRight() - (col + 1) * colWidth;
735        } else {
736            left = getPaddingLeft() + col * colWidth;
737        }
738
739        // Compute top edge.
740        final int row = index / DAYS_IN_WEEK;
741        final int rowHeight = mDayHeight;
742        final int headerHeight = mMonthHeight + mDayOfWeekHeight;
743        final int top = getPaddingTop() + headerHeight + row * rowHeight;
744
745        outBounds.set(left, top, left + colWidth, top + rowHeight);
746
747        return true;
748    }
749
750    /**
751     * Called when the user clicks on a day. Handles callbacks to the
752     * {@link OnDayClickListener} if one is set.
753     *
754     * @param day the day that was clicked
755     */
756    private boolean onDayClicked(int day) {
757        if (!isValidDayOfMonth(day) || !isDayEnabled(day)) {
758            return false;
759        }
760
761        if (mOnDayClickListener != null) {
762            final Calendar date = Calendar.getInstance();
763            date.set(mYear, mMonth, day);
764            mOnDayClickListener.onDayClick(this, date);
765        }
766
767        // This is a no-op if accessibility is turned off.
768        mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
769        return true;
770    }
771
772    /**
773     * Provides a virtual view hierarchy for interfacing with an accessibility
774     * service.
775     */
776    private class MonthViewTouchHelper extends ExploreByTouchHelper {
777        private static final String DATE_FORMAT = "dd MMMM yyyy";
778
779        private final Rect mTempRect = new Rect();
780        private final Calendar mTempCalendar = Calendar.getInstance();
781
782        public MonthViewTouchHelper(View host) {
783            super(host);
784        }
785
786        @Override
787        protected int getVirtualViewAt(float x, float y) {
788            final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f));
789            if (day != -1) {
790                return day;
791            }
792            return ExploreByTouchHelper.INVALID_ID;
793        }
794
795        @Override
796        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
797            for (int day = 1; day <= mDaysInMonth; day++) {
798                virtualViewIds.add(day);
799            }
800        }
801
802        @Override
803        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
804            event.setContentDescription(getDayDescription(virtualViewId));
805        }
806
807        @Override
808        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
809            final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect);
810
811            if (!hasBounds) {
812                // The day is invalid, kill the node.
813                mTempRect.setEmpty();
814                node.setContentDescription("");
815                node.setBoundsInParent(mTempRect);
816                node.setVisibleToUser(false);
817                return;
818            }
819
820            node.setText(getDayText(virtualViewId));
821            node.setContentDescription(getDayDescription(virtualViewId));
822            node.setBoundsInParent(mTempRect);
823
824            final boolean isDayEnabled = isDayEnabled(virtualViewId);
825            if (isDayEnabled) {
826                node.addAction(AccessibilityAction.ACTION_CLICK);
827            }
828
829            node.setEnabled(isDayEnabled);
830
831            if (virtualViewId == mActivatedDay) {
832                // TODO: This should use activated once that's supported.
833                node.setChecked(true);
834            }
835
836        }
837
838        @Override
839        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
840                Bundle arguments) {
841            switch (action) {
842                case AccessibilityNodeInfo.ACTION_CLICK:
843                    return onDayClicked(virtualViewId);
844            }
845
846            return false;
847        }
848
849        /**
850         * Generates a description for a given virtual view.
851         *
852         * @param id the day to generate a description for
853         * @return a description of the virtual view
854         */
855        private CharSequence getDayDescription(int id) {
856            if (isValidDayOfMonth(id)) {
857                mTempCalendar.set(mYear, mMonth, id);
858                return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
859            }
860
861            return "";
862        }
863
864        /**
865         * Generates displayed text for a given virtual view.
866         *
867         * @param id the day to generate text for
868         * @return the visible text of the virtual view
869         */
870        private CharSequence getDayText(int id) {
871            if (isValidDayOfMonth(id)) {
872                return mDayFormatter.format(id);
873            }
874
875            return null;
876        }
877    }
878
879    /**
880     * Handles callbacks when the user clicks on a time object.
881     */
882    public interface OnDayClickListener {
883        void onDayClick(SimpleMonthView view, Calendar day);
884    }
885}
886