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.annotation.Nullable;
     20 import android.content.Context;
     21 import android.content.res.ColorStateList;
     22 import android.content.res.Resources;
     23 import android.content.res.TypedArray;
     24 import android.graphics.Canvas;
     25 import android.graphics.Paint;
     26 import android.graphics.Paint.Align;
     27 import android.graphics.Paint.Style;
     28 import android.graphics.Rect;
     29 import android.graphics.Typeface;
     30 import android.icu.text.DisplayContext;
     31 import android.icu.text.SimpleDateFormat;
     32 import android.icu.util.Calendar;
     33 import android.os.Bundle;
     34 import android.text.TextPaint;
     35 import android.text.format.DateFormat;
     36 import android.util.AttributeSet;
     37 import android.util.IntArray;
     38 import android.util.MathUtils;
     39 import android.util.StateSet;
     40 import android.view.KeyEvent;
     41 import android.view.MotionEvent;
     42 import android.view.PointerIcon;
     43 import android.view.View;
     44 import android.view.ViewParent;
     45 import android.view.accessibility.AccessibilityEvent;
     46 import android.view.accessibility.AccessibilityNodeInfo;
     47 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
     48 
     49 import com.android.internal.R;
     50 import com.android.internal.widget.ExploreByTouchHelper;
     51 
     52 import libcore.icu.LocaleData;
     53 
     54 import java.text.NumberFormat;
     55 import java.util.Locale;
     56 
     57 /**
     58  * A calendar-like view displaying a specified month and the appropriate selectable day numbers
     59  * within the specified month.
     60  */
     61 class SimpleMonthView extends View {
     62     private static final int DAYS_IN_WEEK = 7;
     63     private static final int MAX_WEEKS_IN_MONTH = 6;
     64 
     65     private static final int DEFAULT_SELECTED_DAY = -1;
     66     private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
     67 
     68     private static final String MONTH_YEAR_FORMAT = "MMMMy";
     69 
     70     private static final int SELECTED_HIGHLIGHT_ALPHA = 0xB0;
     71 
     72     private final TextPaint mMonthPaint = new TextPaint();
     73     private final TextPaint mDayOfWeekPaint = new TextPaint();
     74     private final TextPaint mDayPaint = new TextPaint();
     75     private final Paint mDaySelectorPaint = new Paint();
     76     private final Paint mDayHighlightPaint = new Paint();
     77     private final Paint mDayHighlightSelectorPaint = new Paint();
     78 
     79     /** Array of single-character weekday labels ordered by column index. */
     80     private final String[] mDayOfWeekLabels = new String[7];
     81 
     82     private final Calendar mCalendar;
     83     private final Locale mLocale;
     84 
     85     private final MonthViewTouchHelper mTouchHelper;
     86 
     87     private final NumberFormat mDayFormatter;
     88 
     89     // Desired dimensions.
     90     private final int mDesiredMonthHeight;
     91     private final int mDesiredDayOfWeekHeight;
     92     private final int mDesiredDayHeight;
     93     private final int mDesiredCellWidth;
     94     private final int mDesiredDaySelectorRadius;
     95 
     96     private String mMonthYearLabel;
     97 
     98     private int mMonth;
     99     private int mYear;
    100 
    101     // Dimensions as laid out.
    102     private int mMonthHeight;
    103     private int mDayOfWeekHeight;
    104     private int mDayHeight;
    105     private int mCellWidth;
    106     private int mDaySelectorRadius;
    107 
    108     private int mPaddedWidth;
    109     private int mPaddedHeight;
    110 
    111     /** The day of month for the selected day, or -1 if no day is selected. */
    112     private int mActivatedDay = -1;
    113 
    114     /**
    115      * The day of month for today, or -1 if the today is not in the current
    116      * month.
    117      */
    118     private int mToday = DEFAULT_SELECTED_DAY;
    119 
    120     /** The first day of the week (ex. Calendar.SUNDAY) indexed from one. */
    121     private int mWeekStart = DEFAULT_WEEK_START;
    122 
    123     /** The number of days (ex. 28) in the current month. */
    124     private int mDaysInMonth;
    125 
    126     /**
    127      * The day of week (ex. Calendar.SUNDAY) for the first day of the current
    128      * month.
    129      */
    130     private int mDayOfWeekStart;
    131 
    132     /** The day of month for the first (inclusive) enabled day. */
    133     private int mEnabledDayStart = 1;
    134 
    135     /** The day of month for the last (inclusive) enabled day. */
    136     private int mEnabledDayEnd = 31;
    137 
    138     /** Optional listener for handling day click actions. */
    139     private OnDayClickListener mOnDayClickListener;
    140 
    141     private ColorStateList mDayTextColor;
    142 
    143     private int mHighlightedDay = -1;
    144     private int mPreviouslyHighlightedDay = -1;
    145     private boolean mIsTouchHighlighted = false;
    146 
    147     public SimpleMonthView(Context context) {
    148         this(context, null);
    149     }
    150 
    151     public SimpleMonthView(Context context, AttributeSet attrs) {
    152         this(context, attrs, R.attr.datePickerStyle);
    153     }
    154 
    155     public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
    156         this(context, attrs, defStyleAttr, 0);
    157     }
    158 
    159     public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    160         super(context, attrs, defStyleAttr, defStyleRes);
    161 
    162         final Resources res = context.getResources();
    163         mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
    164         mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
    165         mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
    166         mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
    167         mDesiredDaySelectorRadius = res.getDimensionPixelSize(
    168                 R.dimen.date_picker_day_selector_radius);
    169 
    170         // Set up accessibility components.
    171         mTouchHelper = new MonthViewTouchHelper(this);
    172         setAccessibilityDelegate(mTouchHelper);
    173         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    174 
    175         mLocale = res.getConfiguration().locale;
    176         mCalendar = Calendar.getInstance(mLocale);
    177 
    178         mDayFormatter = NumberFormat.getIntegerInstance(mLocale);
    179 
    180         updateMonthYearLabel();
    181         updateDayOfWeekLabels();
    182 
    183         initPaints(res);
    184     }
    185 
    186     private void updateMonthYearLabel() {
    187         final String format = DateFormat.getBestDateTimePattern(mLocale, MONTH_YEAR_FORMAT);
    188         final SimpleDateFormat formatter = new SimpleDateFormat(format, mLocale);
    189         formatter.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
    190         mMonthYearLabel = formatter.format(mCalendar.getTime());
    191     }
    192 
    193     private void updateDayOfWeekLabels() {
    194         // Use tiny (e.g. single-character) weekday names from ICU. The indices
    195         // for this list correspond to Calendar days, e.g. SUNDAY is index 1.
    196         final String[] tinyWeekdayNames = LocaleData.get(mLocale).tinyWeekdayNames;
    197         for (int i = 0; i < DAYS_IN_WEEK; i++) {
    198             mDayOfWeekLabels[i] = tinyWeekdayNames[(mWeekStart + i - 1) % DAYS_IN_WEEK + 1];
    199         }
    200     }
    201 
    202     /**
    203      * Applies the specified text appearance resource to a paint, returning the
    204      * text color if one is set in the text appearance.
    205      *
    206      * @param p the paint to modify
    207      * @param resId the resource ID of the text appearance
    208      * @return the text color, if available
    209      */
    210     private ColorStateList applyTextAppearance(Paint p, int resId) {
    211         final TypedArray ta = mContext.obtainStyledAttributes(null,
    212                 R.styleable.TextAppearance, 0, resId);
    213 
    214         final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
    215         if (fontFamily != null) {
    216             p.setTypeface(Typeface.create(fontFamily, 0));
    217         }
    218 
    219         p.setTextSize(ta.getDimensionPixelSize(
    220                 R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
    221 
    222         final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
    223         if (textColor != null) {
    224             final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
    225             p.setColor(enabledColor);
    226         }
    227 
    228         ta.recycle();
    229 
    230         return textColor;
    231     }
    232 
    233     public int getMonthHeight() {
    234         return mMonthHeight;
    235     }
    236 
    237     public int getCellWidth() {
    238         return mCellWidth;
    239     }
    240 
    241     public void setMonthTextAppearance(int resId) {
    242         applyTextAppearance(mMonthPaint, resId);
    243 
    244         invalidate();
    245     }
    246 
    247     public void setDayOfWeekTextAppearance(int resId) {
    248         applyTextAppearance(mDayOfWeekPaint, resId);
    249         invalidate();
    250     }
    251 
    252     public void setDayTextAppearance(int resId) {
    253         final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
    254         if (textColor != null) {
    255             mDayTextColor = textColor;
    256         }
    257 
    258         invalidate();
    259     }
    260 
    261     /**
    262      * Sets up the text and style properties for painting.
    263      */
    264     private void initPaints(Resources res) {
    265         final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
    266         final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
    267         final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
    268 
    269         final int monthTextSize = res.getDimensionPixelSize(
    270                 R.dimen.date_picker_month_text_size);
    271         final int dayOfWeekTextSize = res.getDimensionPixelSize(
    272                 R.dimen.date_picker_day_of_week_text_size);
    273         final int dayTextSize = res.getDimensionPixelSize(
    274                 R.dimen.date_picker_day_text_size);
    275 
    276         mMonthPaint.setAntiAlias(true);
    277         mMonthPaint.setTextSize(monthTextSize);
    278         mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
    279         mMonthPaint.setTextAlign(Align.CENTER);
    280         mMonthPaint.setStyle(Style.FILL);
    281 
    282         mDayOfWeekPaint.setAntiAlias(true);
    283         mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
    284         mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
    285         mDayOfWeekPaint.setTextAlign(Align.CENTER);
    286         mDayOfWeekPaint.setStyle(Style.FILL);
    287 
    288         mDaySelectorPaint.setAntiAlias(true);
    289         mDaySelectorPaint.setStyle(Style.FILL);
    290 
    291         mDayHighlightPaint.setAntiAlias(true);
    292         mDayHighlightPaint.setStyle(Style.FILL);
    293 
    294         mDayHighlightSelectorPaint.setAntiAlias(true);
    295         mDayHighlightSelectorPaint.setStyle(Style.FILL);
    296 
    297         mDayPaint.setAntiAlias(true);
    298         mDayPaint.setTextSize(dayTextSize);
    299         mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
    300         mDayPaint.setTextAlign(Align.CENTER);
    301         mDayPaint.setStyle(Style.FILL);
    302     }
    303 
    304     void setMonthTextColor(ColorStateList monthTextColor) {
    305         final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
    306         mMonthPaint.setColor(enabledColor);
    307         invalidate();
    308     }
    309 
    310     void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
    311         final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
    312         mDayOfWeekPaint.setColor(enabledColor);
    313         invalidate();
    314     }
    315 
    316     void setDayTextColor(ColorStateList dayTextColor) {
    317         mDayTextColor = dayTextColor;
    318         invalidate();
    319     }
    320 
    321     void setDaySelectorColor(ColorStateList dayBackgroundColor) {
    322         final int activatedColor = dayBackgroundColor.getColorForState(
    323                 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
    324         mDaySelectorPaint.setColor(activatedColor);
    325         mDayHighlightSelectorPaint.setColor(activatedColor);
    326         mDayHighlightSelectorPaint.setAlpha(SELECTED_HIGHLIGHT_ALPHA);
    327         invalidate();
    328     }
    329 
    330     void setDayHighlightColor(ColorStateList dayHighlightColor) {
    331         final int pressedColor = dayHighlightColor.getColorForState(
    332                 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
    333         mDayHighlightPaint.setColor(pressedColor);
    334         invalidate();
    335     }
    336 
    337     public void setOnDayClickListener(OnDayClickListener listener) {
    338         mOnDayClickListener = listener;
    339     }
    340 
    341     @Override
    342     public boolean dispatchHoverEvent(MotionEvent event) {
    343         // First right-of-refusal goes the touch exploration helper.
    344         return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
    345     }
    346 
    347     @Override
    348     public boolean onTouchEvent(MotionEvent event) {
    349         final int x = (int) (event.getX() + 0.5f);
    350         final int y = (int) (event.getY() + 0.5f);
    351 
    352         final int action = event.getAction();
    353         switch (action) {
    354             case MotionEvent.ACTION_DOWN:
    355             case MotionEvent.ACTION_MOVE:
    356                 final int touchedItem = getDayAtLocation(x, y);
    357                 mIsTouchHighlighted = true;
    358                 if (mHighlightedDay != touchedItem) {
    359                     mHighlightedDay = touchedItem;
    360                     mPreviouslyHighlightedDay = touchedItem;
    361                     invalidate();
    362                 }
    363                 if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) {
    364                     // Touch something that's not an item, reject event.
    365                     return false;
    366                 }
    367                 break;
    368 
    369             case MotionEvent.ACTION_UP:
    370                 final int clickedDay = getDayAtLocation(x, y);
    371                 onDayClicked(clickedDay);
    372                 // Fall through.
    373             case MotionEvent.ACTION_CANCEL:
    374                 // Reset touched day on stream end.
    375                 mHighlightedDay = -1;
    376                 mIsTouchHighlighted = false;
    377                 invalidate();
    378                 break;
    379         }
    380         return true;
    381     }
    382 
    383     @Override
    384     public boolean onKeyDown(int keyCode, KeyEvent event) {
    385         // We need to handle focus change within the SimpleMonthView because we are simulating
    386         // multiple Views. The arrow keys will move between days until there is no space (no
    387         // day to the left, top, right, or bottom). Focus forward and back jumps out of the
    388         // SimpleMonthView, skipping over other SimpleMonthViews in the parent ViewPager
    389         // to the next focusable View in the hierarchy.
    390         boolean focusChanged = false;
    391         switch (event.getKeyCode()) {
    392             case KeyEvent.KEYCODE_DPAD_LEFT:
    393                 if (event.hasNoModifiers()) {
    394                     focusChanged = moveOneDay(isLayoutRtl());
    395                 }
    396                 break;
    397             case KeyEvent.KEYCODE_DPAD_RIGHT:
    398                 if (event.hasNoModifiers()) {
    399                     focusChanged = moveOneDay(!isLayoutRtl());
    400                 }
    401                 break;
    402             case KeyEvent.KEYCODE_DPAD_UP:
    403                 if (event.hasNoModifiers()) {
    404                     ensureFocusedDay();
    405                     if (mHighlightedDay > 7) {
    406                         mHighlightedDay -= 7;
    407                         focusChanged = true;
    408                     }
    409                 }
    410                 break;
    411             case KeyEvent.KEYCODE_DPAD_DOWN:
    412                 if (event.hasNoModifiers()) {
    413                     ensureFocusedDay();
    414                     if (mHighlightedDay <= mDaysInMonth - 7) {
    415                         mHighlightedDay += 7;
    416                         focusChanged = true;
    417                     }
    418                 }
    419                 break;
    420             case KeyEvent.KEYCODE_DPAD_CENTER:
    421             case KeyEvent.KEYCODE_ENTER:
    422                 if (mHighlightedDay != -1) {
    423                     onDayClicked(mHighlightedDay);
    424                     return true;
    425                 }
    426                 break;
    427             case KeyEvent.KEYCODE_TAB: {
    428                 int focusChangeDirection = 0;
    429                 if (event.hasNoModifiers()) {
    430                     focusChangeDirection = View.FOCUS_FORWARD;
    431                 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
    432                     focusChangeDirection = View.FOCUS_BACKWARD;
    433                 }
    434                 if (focusChangeDirection != 0) {
    435                     final ViewParent parent = getParent();
    436                     // move out of the ViewPager next/previous
    437                     View nextFocus = this;
    438                     do {
    439                         nextFocus = nextFocus.focusSearch(focusChangeDirection);
    440                     } while (nextFocus != null && nextFocus != this &&
    441                             nextFocus.getParent() == parent);
    442                     if (nextFocus != null) {
    443                         nextFocus.requestFocus();
    444                         return true;
    445                     }
    446                 }
    447                 break;
    448             }
    449         }
    450         if (focusChanged) {
    451             invalidate();
    452             return true;
    453         } else {
    454             return super.onKeyDown(keyCode, event);
    455         }
    456     }
    457 
    458     private boolean moveOneDay(boolean positive) {
    459         ensureFocusedDay();
    460         boolean focusChanged = false;
    461         if (positive) {
    462             if (!isLastDayOfWeek(mHighlightedDay) && mHighlightedDay < mDaysInMonth) {
    463                 mHighlightedDay++;
    464                 focusChanged = true;
    465             }
    466         } else {
    467             if (!isFirstDayOfWeek(mHighlightedDay) && mHighlightedDay > 1) {
    468                 mHighlightedDay--;
    469                 focusChanged = true;
    470             }
    471         }
    472         return focusChanged;
    473     }
    474 
    475     @Override
    476     protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
    477             @Nullable Rect previouslyFocusedRect) {
    478         if (gainFocus) {
    479             // If we've gained focus through arrow keys, we should find the day closest
    480             // to the focus rect. If we've gained focus through forward/back, we should
    481             // focus on the selected day if there is one.
    482             final int offset = findDayOffset();
    483             switch(direction) {
    484                 case View.FOCUS_RIGHT: {
    485                     int row = findClosestRow(previouslyFocusedRect);
    486                     mHighlightedDay = row == 0 ? 1 : (row * DAYS_IN_WEEK) - offset + 1;
    487                     break;
    488                 }
    489                 case View.FOCUS_LEFT: {
    490                     int row = findClosestRow(previouslyFocusedRect) + 1;
    491                     mHighlightedDay = Math.min(mDaysInMonth, (row * DAYS_IN_WEEK) - offset);
    492                     break;
    493                 }
    494                 case View.FOCUS_DOWN: {
    495                     final int col = findClosestColumn(previouslyFocusedRect);
    496                     final int day = col - offset + 1;
    497                     mHighlightedDay = day < 1 ? day + DAYS_IN_WEEK : day;
    498                     break;
    499                 }
    500                 case View.FOCUS_UP: {
    501                     final int col = findClosestColumn(previouslyFocusedRect);
    502                     final int maxWeeks = (offset + mDaysInMonth) / DAYS_IN_WEEK;
    503                     final int day = col - offset + (DAYS_IN_WEEK * maxWeeks) + 1;
    504                     mHighlightedDay = day > mDaysInMonth ? day - DAYS_IN_WEEK : day;
    505                     break;
    506                 }
    507             }
    508             ensureFocusedDay();
    509             invalidate();
    510         }
    511         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    512     }
    513 
    514     /**
    515      * Returns the row (0 indexed) closest to previouslyFocusedRect or center if null.
    516      */
    517     private int findClosestRow(@Nullable Rect previouslyFocusedRect) {
    518         if (previouslyFocusedRect == null) {
    519             return 3;
    520         } else if (mDayHeight == 0) {
    521             return 0; // There hasn't been a layout, so just choose the first row
    522         } else {
    523             int centerY = previouslyFocusedRect.centerY();
    524 
    525             final TextPaint p = mDayPaint;
    526             final int headerHeight = mMonthHeight + mDayOfWeekHeight;
    527             final int rowHeight = mDayHeight;
    528 
    529             // Text is vertically centered within the row height.
    530             final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
    531             final int rowCenter = headerHeight + rowHeight / 2;
    532 
    533             centerY -= rowCenter - halfLineHeight;
    534             int row = Math.round(centerY / (float) rowHeight);
    535             final int maxDay = findDayOffset() + mDaysInMonth;
    536             final int maxRows = (maxDay / DAYS_IN_WEEK) - ((maxDay % DAYS_IN_WEEK == 0) ? 1 : 0);
    537 
    538             row = MathUtils.constrain(row, 0, maxRows);
    539             return row;
    540         }
    541     }
    542 
    543     /**
    544      * Returns the column (0 indexed) closest to the previouslyFocusedRect or center if null.
    545      * The 0 index is related to the first day of the week.
    546      */
    547     private int findClosestColumn(@Nullable Rect previouslyFocusedRect) {
    548         if (previouslyFocusedRect == null) {
    549             return DAYS_IN_WEEK / 2;
    550         } else if (mCellWidth == 0) {
    551             return 0; // There hasn't been a layout, so we can just choose the first column
    552         } else {
    553             int centerX = previouslyFocusedRect.centerX() - mPaddingLeft;
    554             final int columnFromLeft =
    555                     MathUtils.constrain(centerX / mCellWidth, 0, DAYS_IN_WEEK - 1);
    556             return isLayoutRtl() ? DAYS_IN_WEEK - columnFromLeft - 1: columnFromLeft;
    557         }
    558     }
    559 
    560     @Override
    561     public void getFocusedRect(Rect r) {
    562         if (mHighlightedDay > 0) {
    563             getBoundsForDay(mHighlightedDay, r);
    564         } else {
    565             super.getFocusedRect(r);
    566         }
    567     }
    568 
    569     @Override
    570     protected void onFocusLost() {
    571         if (!mIsTouchHighlighted) {
    572             // Unhighlight a day.
    573             mPreviouslyHighlightedDay = mHighlightedDay;
    574             mHighlightedDay = -1;
    575             invalidate();
    576         }
    577         super.onFocusLost();
    578     }
    579 
    580     /**
    581      * Ensure some day is highlighted. If a day isn't highlighted, it chooses the selected day,
    582      * if possible, or the first day of the month if not.
    583      */
    584     private void ensureFocusedDay() {
    585         if (mHighlightedDay != -1) {
    586             return;
    587         }
    588         if (mPreviouslyHighlightedDay != -1) {
    589             mHighlightedDay = mPreviouslyHighlightedDay;
    590             return;
    591         }
    592         if (mActivatedDay != -1) {
    593             mHighlightedDay = mActivatedDay;
    594             return;
    595         }
    596         mHighlightedDay = 1;
    597     }
    598 
    599     private boolean isFirstDayOfWeek(int day) {
    600         final int offset = findDayOffset();
    601         return (offset + day - 1) % DAYS_IN_WEEK == 0;
    602     }
    603 
    604     private boolean isLastDayOfWeek(int day) {
    605         final int offset = findDayOffset();
    606         return (offset + day) % DAYS_IN_WEEK == 0;
    607     }
    608 
    609     @Override
    610     protected void onDraw(Canvas canvas) {
    611         final int paddingLeft = getPaddingLeft();
    612         final int paddingTop = getPaddingTop();
    613         canvas.translate(paddingLeft, paddingTop);
    614 
    615         drawMonth(canvas);
    616         drawDaysOfWeek(canvas);
    617         drawDays(canvas);
    618 
    619         canvas.translate(-paddingLeft, -paddingTop);
    620     }
    621 
    622     private void drawMonth(Canvas canvas) {
    623         final float x = mPaddedWidth / 2f;
    624 
    625         // Vertically centered within the month header height.
    626         final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
    627         final float y = (mMonthHeight - lineHeight) / 2f;
    628 
    629         canvas.drawText(mMonthYearLabel, x, y, mMonthPaint);
    630     }
    631 
    632     public String getMonthYearLabel() {
    633         return mMonthYearLabel;
    634     }
    635 
    636     private void drawDaysOfWeek(Canvas canvas) {
    637         final TextPaint p = mDayOfWeekPaint;
    638         final int headerHeight = mMonthHeight;
    639         final int rowHeight = mDayOfWeekHeight;
    640         final int colWidth = mCellWidth;
    641 
    642         // Text is vertically centered within the day of week height.
    643         final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
    644         final int rowCenter = headerHeight + rowHeight / 2;
    645 
    646         for (int col = 0; col < DAYS_IN_WEEK; col++) {
    647             final int colCenter = colWidth * col + colWidth / 2;
    648             final int colCenterRtl;
    649             if (isLayoutRtl()) {
    650                 colCenterRtl = mPaddedWidth - colCenter;
    651             } else {
    652                 colCenterRtl = colCenter;
    653             }
    654 
    655             final String label = mDayOfWeekLabels[col];
    656             canvas.drawText(label, colCenterRtl, rowCenter - halfLineHeight, p);
    657         }
    658     }
    659 
    660     /**
    661      * Draws the month days.
    662      */
    663     private void drawDays(Canvas canvas) {
    664         final TextPaint p = mDayPaint;
    665         final int headerHeight = mMonthHeight + mDayOfWeekHeight;
    666         final int rowHeight = mDayHeight;
    667         final int colWidth = mCellWidth;
    668 
    669         // Text is vertically centered within the row height.
    670         final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
    671         int rowCenter = headerHeight + rowHeight / 2;
    672 
    673         for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) {
    674             final int colCenter = colWidth * col + colWidth / 2;
    675             final int colCenterRtl;
    676             if (isLayoutRtl()) {
    677                 colCenterRtl = mPaddedWidth - colCenter;
    678             } else {
    679                 colCenterRtl = colCenter;
    680             }
    681 
    682             int stateMask = 0;
    683 
    684             final boolean isDayEnabled = isDayEnabled(day);
    685             if (isDayEnabled) {
    686                 stateMask |= StateSet.VIEW_STATE_ENABLED;
    687             }
    688 
    689             final boolean isDayActivated = mActivatedDay == day;
    690             final boolean isDayHighlighted = mHighlightedDay == day;
    691             if (isDayActivated) {
    692                 stateMask |= StateSet.VIEW_STATE_ACTIVATED;
    693 
    694                 // Adjust the circle to be centered on the row.
    695                 final Paint paint = isDayHighlighted ? mDayHighlightSelectorPaint :
    696                         mDaySelectorPaint;
    697                 canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, paint);
    698             } else if (isDayHighlighted) {
    699                 stateMask |= StateSet.VIEW_STATE_PRESSED;
    700 
    701                 if (isDayEnabled) {
    702                     // Adjust the circle to be centered on the row.
    703                     canvas.drawCircle(colCenterRtl, rowCenter,
    704                             mDaySelectorRadius, mDayHighlightPaint);
    705                 }
    706             }
    707 
    708             final boolean isDayToday = mToday == day;
    709             final int dayTextColor;
    710             if (isDayToday && !isDayActivated) {
    711                 dayTextColor = mDaySelectorPaint.getColor();
    712             } else {
    713                 final int[] stateSet = StateSet.get(stateMask);
    714                 dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
    715             }
    716             p.setColor(dayTextColor);
    717 
    718             canvas.drawText(mDayFormatter.format(day), colCenterRtl, rowCenter - halfLineHeight, p);
    719 
    720             col++;
    721 
    722             if (col == DAYS_IN_WEEK) {
    723                 col = 0;
    724                 rowCenter += rowHeight;
    725             }
    726         }
    727     }
    728 
    729     private boolean isDayEnabled(int day) {
    730         return day >= mEnabledDayStart && day <= mEnabledDayEnd;
    731     }
    732 
    733     private boolean isValidDayOfMonth(int day) {
    734         return day >= 1 && day <= mDaysInMonth;
    735     }
    736 
    737     private static boolean isValidDayOfWeek(int day) {
    738         return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
    739     }
    740 
    741     private static boolean isValidMonth(int month) {
    742         return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
    743     }
    744 
    745     /**
    746      * Sets the selected day.
    747      *
    748      * @param dayOfMonth the selected day of the month, or {@code -1} to clear
    749      *                   the selection
    750      */
    751     public void setSelectedDay(int dayOfMonth) {
    752         mActivatedDay = dayOfMonth;
    753 
    754         // Invalidate cached accessibility information.
    755         mTouchHelper.invalidateRoot();
    756         invalidate();
    757     }
    758 
    759     /**
    760      * Sets the first day of the week.
    761      *
    762      * @param weekStart which day the week should start on, valid values are
    763      *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
    764      */
    765     public void setFirstDayOfWeek(int weekStart) {
    766         if (isValidDayOfWeek(weekStart)) {
    767             mWeekStart = weekStart;
    768         } else {
    769             mWeekStart = mCalendar.getFirstDayOfWeek();
    770         }
    771 
    772         updateDayOfWeekLabels();
    773 
    774         // Invalidate cached accessibility information.
    775         mTouchHelper.invalidateRoot();
    776         invalidate();
    777     }
    778 
    779     /**
    780      * Sets all the parameters for displaying this week.
    781      * <p>
    782      * Parameters have a default value and will only update if a new value is
    783      * included, except for focus month, which will always default to no focus
    784      * month if no value is passed in. The only required parameter is the week
    785      * start.
    786      *
    787      * @param selectedDay the selected day of the month, or -1 for no selection
    788      * @param month the month
    789      * @param year the year
    790      * @param weekStart which day the week should start on, valid values are
    791      *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
    792      * @param enabledDayStart the first enabled day
    793      * @param enabledDayEnd the last enabled day
    794      */
    795     void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
    796             int enabledDayEnd) {
    797         mActivatedDay = selectedDay;
    798 
    799         if (isValidMonth(month)) {
    800             mMonth = month;
    801         }
    802         mYear = year;
    803 
    804         mCalendar.set(Calendar.MONTH, mMonth);
    805         mCalendar.set(Calendar.YEAR, mYear);
    806         mCalendar.set(Calendar.DAY_OF_MONTH, 1);
    807         mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
    808 
    809         if (isValidDayOfWeek(weekStart)) {
    810             mWeekStart = weekStart;
    811         } else {
    812             mWeekStart = mCalendar.getFirstDayOfWeek();
    813         }
    814 
    815         // Figure out what day today is.
    816         final Calendar today = Calendar.getInstance();
    817         mToday = -1;
    818         mDaysInMonth = getDaysInMonth(mMonth, mYear);
    819         for (int i = 0; i < mDaysInMonth; i++) {
    820             final int day = i + 1;
    821             if (sameDay(day, today)) {
    822                 mToday = day;
    823             }
    824         }
    825 
    826         mEnabledDayStart = MathUtils.constrain(enabledDayStart, 1, mDaysInMonth);
    827         mEnabledDayEnd = MathUtils.constrain(enabledDayEnd, mEnabledDayStart, mDaysInMonth);
    828 
    829         updateMonthYearLabel();
    830         updateDayOfWeekLabels();
    831 
    832         // Invalidate cached accessibility information.
    833         mTouchHelper.invalidateRoot();
    834         invalidate();
    835     }
    836 
    837     private static int getDaysInMonth(int month, int year) {
    838         switch (month) {
    839             case Calendar.JANUARY:
    840             case Calendar.MARCH:
    841             case Calendar.MAY:
    842             case Calendar.JULY:
    843             case Calendar.AUGUST:
    844             case Calendar.OCTOBER:
    845             case Calendar.DECEMBER:
    846                 return 31;
    847             case Calendar.APRIL:
    848             case Calendar.JUNE:
    849             case Calendar.SEPTEMBER:
    850             case Calendar.NOVEMBER:
    851                 return 30;
    852             case Calendar.FEBRUARY:
    853                 return (year % 4 == 0) ? 29 : 28;
    854             default:
    855                 throw new IllegalArgumentException("Invalid Month");
    856         }
    857     }
    858 
    859     private boolean sameDay(int day, Calendar today) {
    860         return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
    861                 && day == today.get(Calendar.DAY_OF_MONTH);
    862     }
    863 
    864     @Override
    865     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    866         final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH
    867                 + mDesiredDayOfWeekHeight + mDesiredMonthHeight
    868                 + getPaddingTop() + getPaddingBottom();
    869         final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK
    870                 + getPaddingStart() + getPaddingEnd();
    871         final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
    872         final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
    873         setMeasuredDimension(resolvedWidth, resolvedHeight);
    874     }
    875 
    876     @Override
    877     public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
    878         super.onRtlPropertiesChanged(layoutDirection);
    879 
    880         requestLayout();
    881     }
    882 
    883     @Override
    884     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    885         if (!changed) {
    886             return;
    887         }
    888 
    889         // Let's initialize a completely reasonable number of variables.
    890         final int w = right - left;
    891         final int h = bottom - top;
    892         final int paddingLeft = getPaddingLeft();
    893         final int paddingTop = getPaddingTop();
    894         final int paddingRight = getPaddingRight();
    895         final int paddingBottom = getPaddingBottom();
    896         final int paddedRight = w - paddingRight;
    897         final int paddedBottom = h - paddingBottom;
    898         final int paddedWidth = paddedRight - paddingLeft;
    899         final int paddedHeight = paddedBottom - paddingTop;
    900         if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) {
    901             return;
    902         }
    903 
    904         mPaddedWidth = paddedWidth;
    905         mPaddedHeight = paddedHeight;
    906 
    907         // We may have been laid out smaller than our preferred size. If so,
    908         // scale all dimensions to fit.
    909         final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom;
    910         final float scaleH = paddedHeight / (float) measuredPaddedHeight;
    911         final int monthHeight = (int) (mDesiredMonthHeight * scaleH);
    912         final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
    913         mMonthHeight = monthHeight;
    914         mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH);
    915         mDayHeight = (int) (mDesiredDayHeight * scaleH);
    916         mCellWidth = cellWidth;
    917 
    918         // Compute the largest day selector radius that's still within the clip
    919         // bounds and desired selector radius.
    920         final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
    921         final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
    922         mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
    923                 Math.min(maxSelectorWidth, maxSelectorHeight));
    924 
    925         // Invalidate cached accessibility information.
    926         mTouchHelper.invalidateRoot();
    927     }
    928 
    929     private int findDayOffset() {
    930         final int offset = mDayOfWeekStart - mWeekStart;
    931         if (mDayOfWeekStart < mWeekStart) {
    932             return offset + DAYS_IN_WEEK;
    933         }
    934         return offset;
    935     }
    936 
    937     /**
    938      * Calculates the day of the month at the specified touch position. Returns
    939      * the day of the month or -1 if the position wasn't in a valid day.
    940      *
    941      * @param x the x position of the touch event
    942      * @param y the y position of the touch event
    943      * @return the day of the month at (x, y), or -1 if the position wasn't in
    944      *         a valid day
    945      */
    946     private int getDayAtLocation(int x, int y) {
    947         final int paddedX = x - getPaddingLeft();
    948         if (paddedX < 0 || paddedX >= mPaddedWidth) {
    949             return -1;
    950         }
    951 
    952         final int headerHeight = mMonthHeight + mDayOfWeekHeight;
    953         final int paddedY = y - getPaddingTop();
    954         if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
    955             return -1;
    956         }
    957 
    958         // Adjust for RTL after applying padding.
    959         final int paddedXRtl;
    960         if (isLayoutRtl()) {
    961             paddedXRtl = mPaddedWidth - paddedX;
    962         } else {
    963             paddedXRtl = paddedX;
    964         }
    965 
    966         final int row = (paddedY - headerHeight) / mDayHeight;
    967         final int col = (paddedXRtl * DAYS_IN_WEEK) / mPaddedWidth;
    968         final int index = col + row * DAYS_IN_WEEK;
    969         final int day = index + 1 - findDayOffset();
    970         if (!isValidDayOfMonth(day)) {
    971             return -1;
    972         }
    973 
    974         return day;
    975     }
    976 
    977     /**
    978      * Calculates the bounds of the specified day.
    979      *
    980      * @param id the day of the month
    981      * @param outBounds the rect to populate with bounds
    982      */
    983     public boolean getBoundsForDay(int id, Rect outBounds) {
    984         if (!isValidDayOfMonth(id)) {
    985             return false;
    986         }
    987 
    988         final int index = id - 1 + findDayOffset();
    989 
    990         // Compute left edge, taking into account RTL.
    991         final int col = index % DAYS_IN_WEEK;
    992         final int colWidth = mCellWidth;
    993         final int left;
    994         if (isLayoutRtl()) {
    995             left = getWidth() - getPaddingRight() - (col + 1) * colWidth;
    996         } else {
    997             left = getPaddingLeft() + col * colWidth;
    998         }
    999 
   1000         // Compute top edge.
   1001         final int row = index / DAYS_IN_WEEK;
   1002         final int rowHeight = mDayHeight;
   1003         final int headerHeight = mMonthHeight + mDayOfWeekHeight;
   1004         final int top = getPaddingTop() + headerHeight + row * rowHeight;
   1005 
   1006         outBounds.set(left, top, left + colWidth, top + rowHeight);
   1007 
   1008         return true;
   1009     }
   1010 
   1011     /**
   1012      * Called when the user clicks on a day. Handles callbacks to the
   1013      * {@link OnDayClickListener} if one is set.
   1014      *
   1015      * @param day the day that was clicked
   1016      */
   1017     private boolean onDayClicked(int day) {
   1018         if (!isValidDayOfMonth(day) || !isDayEnabled(day)) {
   1019             return false;
   1020         }
   1021 
   1022         if (mOnDayClickListener != null) {
   1023             final Calendar date = Calendar.getInstance();
   1024             date.set(mYear, mMonth, day);
   1025             mOnDayClickListener.onDayClick(this, date);
   1026         }
   1027 
   1028         // This is a no-op if accessibility is turned off.
   1029         mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
   1030         return true;
   1031     }
   1032 
   1033     @Override
   1034     public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
   1035         if (!isEnabled()) {
   1036             return null;
   1037         }
   1038         // Add 0.5f to event coordinates to match the logic in onTouchEvent.
   1039         final int x = (int) (event.getX() + 0.5f);
   1040         final int y = (int) (event.getY() + 0.5f);
   1041         final int dayUnderPointer = getDayAtLocation(x, y);
   1042         if (dayUnderPointer >= 0) {
   1043             return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
   1044         }
   1045         return super.onResolvePointerIcon(event, pointerIndex);
   1046     }
   1047 
   1048     /**
   1049      * Provides a virtual view hierarchy for interfacing with an accessibility
   1050      * service.
   1051      */
   1052     private class MonthViewTouchHelper extends ExploreByTouchHelper {
   1053         private static final String DATE_FORMAT = "dd MMMM yyyy";
   1054 
   1055         private final Rect mTempRect = new Rect();
   1056         private final Calendar mTempCalendar = Calendar.getInstance();
   1057 
   1058         public MonthViewTouchHelper(View host) {
   1059             super(host);
   1060         }
   1061 
   1062         @Override
   1063         protected int getVirtualViewAt(float x, float y) {
   1064             final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f));
   1065             if (day != -1) {
   1066                 return day;
   1067             }
   1068             return ExploreByTouchHelper.INVALID_ID;
   1069         }
   1070 
   1071         @Override
   1072         protected void getVisibleVirtualViews(IntArray virtualViewIds) {
   1073             for (int day = 1; day <= mDaysInMonth; day++) {
   1074                 virtualViewIds.add(day);
   1075             }
   1076         }
   1077 
   1078         @Override
   1079         protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
   1080             event.setContentDescription(getDayDescription(virtualViewId));
   1081         }
   1082 
   1083         @Override
   1084         protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
   1085             final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect);
   1086 
   1087             if (!hasBounds) {
   1088                 // The day is invalid, kill the node.
   1089                 mTempRect.setEmpty();
   1090                 node.setContentDescription("");
   1091                 node.setBoundsInParent(mTempRect);
   1092                 node.setVisibleToUser(false);
   1093                 return;
   1094             }
   1095 
   1096             node.setText(getDayText(virtualViewId));
   1097             node.setContentDescription(getDayDescription(virtualViewId));
   1098             node.setBoundsInParent(mTempRect);
   1099 
   1100             final boolean isDayEnabled = isDayEnabled(virtualViewId);
   1101             if (isDayEnabled) {
   1102                 node.addAction(AccessibilityAction.ACTION_CLICK);
   1103             }
   1104 
   1105             node.setEnabled(isDayEnabled);
   1106 
   1107             if (virtualViewId == mActivatedDay) {
   1108                 // TODO: This should use activated once that's supported.
   1109                 node.setChecked(true);
   1110             }
   1111 
   1112         }
   1113 
   1114         @Override
   1115         protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
   1116                 Bundle arguments) {
   1117             switch (action) {
   1118                 case AccessibilityNodeInfo.ACTION_CLICK:
   1119                     return onDayClicked(virtualViewId);
   1120             }
   1121 
   1122             return false;
   1123         }
   1124 
   1125         /**
   1126          * Generates a description for a given virtual view.
   1127          *
   1128          * @param id the day to generate a description for
   1129          * @return a description of the virtual view
   1130          */
   1131         private CharSequence getDayDescription(int id) {
   1132             if (isValidDayOfMonth(id)) {
   1133                 mTempCalendar.set(mYear, mMonth, id);
   1134                 return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
   1135             }
   1136 
   1137             return "";
   1138         }
   1139 
   1140         /**
   1141          * Generates displayed text for a given virtual view.
   1142          *
   1143          * @param id the day to generate text for
   1144          * @return the visible text of the virtual view
   1145          */
   1146         private CharSequence getDayText(int id) {
   1147             if (isValidDayOfMonth(id)) {
   1148                 return mDayFormatter.format(id);
   1149             }
   1150 
   1151             return null;
   1152         }
   1153     }
   1154 
   1155     /**
   1156      * Handles callbacks when the user clicks on a time object.
   1157      */
   1158     public interface OnDayClickListener {
   1159         void onDayClick(SimpleMonthView view, Calendar day);
   1160     }
   1161 }
   1162