Home | History | Annotate | Download | only in widget
      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 
     17 package android.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.ColorStateList;
     21 import android.content.res.Resources;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Canvas;
     24 import android.graphics.Paint;
     25 import android.graphics.Paint.Align;
     26 import android.graphics.Paint.Style;
     27 import android.graphics.Rect;
     28 import android.graphics.Typeface;
     29 import android.os.Bundle;
     30 import android.text.TextPaint;
     31 import android.text.format.DateFormat;
     32 import android.util.AttributeSet;
     33 import android.util.IntArray;
     34 import android.util.MathUtils;
     35 import android.util.StateSet;
     36 import android.view.MotionEvent;
     37 import android.view.View;
     38 import android.view.accessibility.AccessibilityEvent;
     39 import android.view.accessibility.AccessibilityNodeInfo;
     40 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
     41 
     42 import com.android.internal.R;
     43 import com.android.internal.widget.ExploreByTouchHelper;
     44 
     45 import java.text.NumberFormat;
     46 import java.text.SimpleDateFormat;
     47 import java.util.Calendar;
     48 import 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  */
     54 class 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