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