Home | History | Annotate | Download | only in date
      1 /*
      2  * Copyright (C) 2013 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 com.android.datetimepicker.date;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.graphics.Canvas;
     22 import android.graphics.Paint;
     23 import android.graphics.Paint.Align;
     24 import android.graphics.Paint.Style;
     25 import android.graphics.Rect;
     26 import android.graphics.Typeface;
     27 import android.os.Bundle;
     28 import android.support.v4.view.ViewCompat;
     29 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     30 import android.support.v4.widget.ExploreByTouchHelper;
     31 import android.text.format.DateFormat;
     32 import android.text.format.DateUtils;
     33 import android.text.format.Time;
     34 import android.util.AttributeSet;
     35 import android.view.MotionEvent;
     36 import android.view.View;
     37 import android.view.accessibility.AccessibilityEvent;
     38 import android.view.accessibility.AccessibilityNodeInfo;
     39 
     40 import com.android.datetimepicker.R;
     41 import com.android.datetimepicker.Utils;
     42 import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
     43 
     44 import java.security.InvalidParameterException;
     45 import java.util.Calendar;
     46 import java.util.Formatter;
     47 import java.util.HashMap;
     48 import java.util.List;
     49 import java.util.Locale;
     50 
     51 /**
     52  * A calendar-like view displaying a specified month and the appropriate selectable day numbers
     53  * within the specified month.
     54  */
     55 abstract class MonthView extends View {
     56     private static final String TAG = "MonthView";
     57 
     58     /**
     59      * These params can be passed into the view to control how it appears.
     60      * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default
     61      * values are unlikely to fit most layouts correctly.
     62      */
     63     /**
     64      * This sets the height of this week in pixels
     65      */
     66     public static final String VIEW_PARAMS_HEIGHT = "height";
     67     /**
     68      * This specifies the position (or weeks since the epoch) of this week,
     69      * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
     70      */
     71     public static final String VIEW_PARAMS_MONTH = "month";
     72     /**
     73      * This specifies the position (or weeks since the epoch) of this week,
     74      * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
     75      */
     76     public static final String VIEW_PARAMS_YEAR = "year";
     77     /**
     78      * This sets one of the days in this view as selected {@link Time#SUNDAY}
     79      * through {@link Time#SATURDAY}.
     80      */
     81     public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day";
     82     /**
     83      * Which day the week should start on. {@link Time#SUNDAY} through
     84      * {@link Time#SATURDAY}.
     85      */
     86     public static final String VIEW_PARAMS_WEEK_START = "week_start";
     87     /**
     88      * How many days to display at a time. Days will be displayed starting with
     89      * {@link #mWeekStart}.
     90      */
     91     public static final String VIEW_PARAMS_NUM_DAYS = "num_days";
     92     /**
     93      * Which month is currently in focus, as defined by {@link Time#month}
     94      * [0-11].
     95      */
     96     public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month";
     97     /**
     98      * If this month should display week numbers. false if 0, true otherwise.
     99      */
    100     public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num";
    101 
    102     protected static int DEFAULT_HEIGHT = 32;
    103     protected static int MIN_HEIGHT = 10;
    104     protected static final int DEFAULT_SELECTED_DAY = -1;
    105     protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
    106     protected static final int DEFAULT_NUM_DAYS = 7;
    107     protected static final int DEFAULT_SHOW_WK_NUM = 0;
    108     protected static final int DEFAULT_FOCUS_MONTH = -1;
    109     protected static final int DEFAULT_NUM_ROWS = 6;
    110     protected static final int MAX_NUM_ROWS = 6;
    111 
    112     private static final int SELECTED_CIRCLE_ALPHA = 60;
    113 
    114     protected static int DAY_SEPARATOR_WIDTH = 1;
    115     protected static int MINI_DAY_NUMBER_TEXT_SIZE;
    116     protected static int MONTH_LABEL_TEXT_SIZE;
    117     protected static int MONTH_DAY_LABEL_TEXT_SIZE;
    118     protected static int MONTH_HEADER_SIZE;
    119     protected static int DAY_SELECTED_CIRCLE_SIZE;
    120 
    121     // used for scaling to the device density
    122     protected static float mScale = 0;
    123 
    124     protected DatePickerController mController;
    125 
    126     // affects the padding on the sides of this view
    127     protected int mEdgePadding = 0;
    128 
    129     private String mDayOfWeekTypeface;
    130     private String mMonthTitleTypeface;
    131 
    132     protected Paint mMonthNumPaint;
    133     protected Paint mMonthTitlePaint;
    134     protected Paint mMonthTitleBGPaint;
    135     protected Paint mSelectedCirclePaint;
    136     protected Paint mMonthDayLabelPaint;
    137 
    138     private final Formatter mFormatter;
    139     private final StringBuilder mStringBuilder;
    140 
    141     // The Julian day of the first day displayed by this item
    142     protected int mFirstJulianDay = -1;
    143     // The month of the first day in this week
    144     protected int mFirstMonth = -1;
    145     // The month of the last day in this week
    146     protected int mLastMonth = -1;
    147 
    148     protected int mMonth;
    149 
    150     protected int mYear;
    151     // Quick reference to the width of this view, matches parent
    152     protected int mWidth;
    153     // The height this view should draw at in pixels, set by height param
    154     protected int mRowHeight = DEFAULT_HEIGHT;
    155     // If this view contains the today
    156     protected boolean mHasToday = false;
    157     // Which day is selected [0-6] or -1 if no day is selected
    158     protected int mSelectedDay = -1;
    159     // Which day is today [0-6] or -1 if no day is today
    160     protected int mToday = DEFAULT_SELECTED_DAY;
    161     // Which day of the week to start on [0-6]
    162     protected int mWeekStart = DEFAULT_WEEK_START;
    163     // How many days to display
    164     protected int mNumDays = DEFAULT_NUM_DAYS;
    165     // The number of days + a spot for week number if it is displayed
    166     protected int mNumCells = mNumDays;
    167     // The left edge of the selected day
    168     protected int mSelectedLeft = -1;
    169     // The right edge of the selected day
    170     protected int mSelectedRight = -1;
    171 
    172     private final Calendar mCalendar;
    173     protected final Calendar mDayLabelCalendar;
    174     private final MonthViewTouchHelper mTouchHelper;
    175 
    176     protected int mNumRows = DEFAULT_NUM_ROWS;
    177 
    178     // Optional listener for handling day click actions
    179     protected OnDayClickListener mOnDayClickListener;
    180 
    181     // Whether to prevent setting the accessibility delegate
    182     private boolean mLockAccessibilityDelegate;
    183 
    184     protected int mDayTextColor;
    185     protected int mTodayNumberColor;
    186     protected int mDisabledDayTextColor;
    187     protected int mMonthTitleColor;
    188     protected int mMonthTitleBGColor;
    189 
    190     public MonthView(Context context) {
    191         this(context, null);
    192     }
    193 
    194     public MonthView(Context context, AttributeSet attr) {
    195         super(context, attr);
    196         Resources res = context.getResources();
    197 
    198         mDayLabelCalendar = Calendar.getInstance();
    199         mCalendar = Calendar.getInstance();
    200 
    201         mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
    202         mMonthTitleTypeface = res.getString(R.string.sans_serif);
    203 
    204         mDayTextColor = res.getColor(R.color.date_picker_text_normal);
    205         mTodayNumberColor = res.getColor(R.color.blue);
    206         mDisabledDayTextColor = res.getColor(R.color.date_picker_text_disabled);
    207         mMonthTitleColor = res.getColor(android.R.color.white);
    208         mMonthTitleBGColor = res.getColor(R.color.circle_background);
    209 
    210         mStringBuilder = new StringBuilder(50);
    211         mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
    212 
    213         MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size);
    214         MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size);
    215         MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size);
    216         MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height);
    217         DAY_SELECTED_CIRCLE_SIZE = res
    218                 .getDimensionPixelSize(R.dimen.day_number_select_circle_radius);
    219 
    220         mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height)
    221                 - getMonthHeaderSize()) / MAX_NUM_ROWS;
    222 
    223         // Set up accessibility components.
    224         mTouchHelper = getMonthViewTouchHelper();
    225         ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
    226         ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
    227         mLockAccessibilityDelegate = true;
    228 
    229         // Sets up any standard paints that will be used
    230         initView();
    231     }
    232 
    233     public void setDatePickerController(DatePickerController controller) {
    234         mController = controller;
    235     }
    236 
    237     protected MonthViewTouchHelper getMonthViewTouchHelper() {
    238         return new MonthViewTouchHelper(this);
    239     }
    240 
    241     @Override
    242     public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
    243         // Workaround for a JB MR1 issue where accessibility delegates on
    244         // top-level ListView items are overwritten.
    245         if (!mLockAccessibilityDelegate) {
    246             super.setAccessibilityDelegate(delegate);
    247         }
    248     }
    249 
    250     public void setOnDayClickListener(OnDayClickListener listener) {
    251         mOnDayClickListener = listener;
    252     }
    253 
    254     @Override
    255     public boolean dispatchHoverEvent(MotionEvent event) {
    256         // First right-of-refusal goes the touch exploration helper.
    257         if (mTouchHelper.dispatchHoverEvent(event)) {
    258             return true;
    259         }
    260         return super.dispatchHoverEvent(event);
    261     }
    262 
    263     @Override
    264     public boolean onTouchEvent(MotionEvent event) {
    265         switch (event.getAction()) {
    266             case MotionEvent.ACTION_UP:
    267                 final int day = getDayFromLocation(event.getX(), event.getY());
    268                 if (day >= 0) {
    269                     onDayClick(day);
    270                 }
    271                 break;
    272         }
    273         return true;
    274     }
    275 
    276     /**
    277      * Sets up the text and style properties for painting. Override this if you
    278      * want to use a different paint.
    279      */
    280     protected void initView() {
    281         mMonthTitlePaint = new Paint();
    282         mMonthTitlePaint.setFakeBoldText(true);
    283         mMonthTitlePaint.setAntiAlias(true);
    284         mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
    285         mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
    286         mMonthTitlePaint.setColor(mDayTextColor);
    287         mMonthTitlePaint.setTextAlign(Align.CENTER);
    288         mMonthTitlePaint.setStyle(Style.FILL);
    289 
    290         mMonthTitleBGPaint = new Paint();
    291         mMonthTitleBGPaint.setFakeBoldText(true);
    292         mMonthTitleBGPaint.setAntiAlias(true);
    293         mMonthTitleBGPaint.setColor(mMonthTitleBGColor);
    294         mMonthTitleBGPaint.setTextAlign(Align.CENTER);
    295         mMonthTitleBGPaint.setStyle(Style.FILL);
    296 
    297         mSelectedCirclePaint = new Paint();
    298         mSelectedCirclePaint.setFakeBoldText(true);
    299         mSelectedCirclePaint.setAntiAlias(true);
    300         mSelectedCirclePaint.setColor(mTodayNumberColor);
    301         mSelectedCirclePaint.setTextAlign(Align.CENTER);
    302         mSelectedCirclePaint.setStyle(Style.FILL);
    303         mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
    304 
    305         mMonthDayLabelPaint = new Paint();
    306         mMonthDayLabelPaint.setAntiAlias(true);
    307         mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
    308         mMonthDayLabelPaint.setColor(mDayTextColor);
    309         mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
    310         mMonthDayLabelPaint.setStyle(Style.FILL);
    311         mMonthDayLabelPaint.setTextAlign(Align.CENTER);
    312         mMonthDayLabelPaint.setFakeBoldText(true);
    313 
    314         mMonthNumPaint = new Paint();
    315         mMonthNumPaint.setAntiAlias(true);
    316         mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
    317         mMonthNumPaint.setStyle(Style.FILL);
    318         mMonthNumPaint.setTextAlign(Align.CENTER);
    319         mMonthNumPaint.setFakeBoldText(false);
    320     }
    321 
    322     @Override
    323     protected void onDraw(Canvas canvas) {
    324         drawMonthTitle(canvas);
    325         drawMonthDayLabels(canvas);
    326         drawMonthNums(canvas);
    327     }
    328 
    329     private int mDayOfWeekStart = 0;
    330 
    331     /**
    332      * Sets all the parameters for displaying this week. The only required
    333      * parameter is the week number. Other parameters have a default value and
    334      * will only update if a new value is included, except for focus month,
    335      * which will always default to no focus month if no value is passed in. See
    336      * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters.
    337      *
    338      * @param params A map of the new parameters, see
    339      *            {@link #VIEW_PARAMS_HEIGHT}
    340      */
    341     public void setMonthParams(HashMap<String, Integer> params) {
    342         if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) {
    343             throw new InvalidParameterException("You must specify month and year for this view");
    344         }
    345         setTag(params);
    346         // We keep the current value for any params not present
    347         if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
    348             mRowHeight = params.get(VIEW_PARAMS_HEIGHT);
    349             if (mRowHeight < MIN_HEIGHT) {
    350                 mRowHeight = MIN_HEIGHT;
    351             }
    352         }
    353         if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
    354             mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
    355         }
    356 
    357         // Allocate space for caching the day numbers and focus values
    358         mMonth = params.get(VIEW_PARAMS_MONTH);
    359         mYear = params.get(VIEW_PARAMS_YEAR);
    360 
    361         // Figure out what day today is
    362         final Time today = new Time(Time.getCurrentTimezone());
    363         today.setToNow();
    364         mHasToday = false;
    365         mToday = -1;
    366 
    367         mCalendar.set(Calendar.MONTH, mMonth);
    368         mCalendar.set(Calendar.YEAR, mYear);
    369         mCalendar.set(Calendar.DAY_OF_MONTH, 1);
    370         mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
    371 
    372         if (params.containsKey(VIEW_PARAMS_WEEK_START)) {
    373             mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
    374         } else {
    375             mWeekStart = mCalendar.getFirstDayOfWeek();
    376         }
    377 
    378         mNumCells = Utils.getDaysInMonth(mMonth, mYear);
    379         for (int i = 0; i < mNumCells; i++) {
    380             final int day = i + 1;
    381             if (sameDay(day, today)) {
    382                 mHasToday = true;
    383                 mToday = day;
    384             }
    385         }
    386         mNumRows = calculateNumRows();
    387 
    388         // Invalidate cached accessibility information.
    389         mTouchHelper.invalidateRoot();
    390     }
    391 
    392     public void setSelectedDay(int day) {
    393         mSelectedDay = day;
    394     }
    395 
    396     public void reuse() {
    397         mNumRows = DEFAULT_NUM_ROWS;
    398         requestLayout();
    399     }
    400 
    401     private int calculateNumRows() {
    402         int offset = findDayOffset();
    403         int dividend = (offset + mNumCells) / mNumDays;
    404         int remainder = (offset + mNumCells) % mNumDays;
    405         return (dividend + (remainder > 0 ? 1 : 0));
    406     }
    407 
    408     private boolean sameDay(int day, Time today) {
    409         return mYear == today.year &&
    410                 mMonth == today.month &&
    411                 day == today.monthDay;
    412     }
    413 
    414     @Override
    415     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    416         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
    417                 + getMonthHeaderSize());
    418     }
    419 
    420     @Override
    421     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    422         mWidth = w;
    423 
    424         // Invalidate cached accessibility information.
    425         mTouchHelper.invalidateRoot();
    426     }
    427 
    428     public int getMonth() {
    429         return mMonth;
    430     }
    431 
    432     public int getYear() {
    433         return mYear;
    434     }
    435 
    436     /**
    437      * A wrapper to the MonthHeaderSize to allow override it in children
    438      */
    439     protected int getMonthHeaderSize() {
    440         return MONTH_HEADER_SIZE;
    441     }
    442 
    443     private String getMonthAndYearString() {
    444         int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
    445                 | DateUtils.FORMAT_NO_MONTH_DAY;
    446         mStringBuilder.setLength(0);
    447         long millis = mCalendar.getTimeInMillis();
    448         return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
    449                 Time.getCurrentTimezone()).toString();
    450     }
    451 
    452     protected void drawMonthTitle(Canvas canvas) {
    453         int x = (mWidth + 2 * mEdgePadding) / 2;
    454         int y = (getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3);
    455         canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
    456     }
    457 
    458     protected void drawMonthDayLabels(Canvas canvas) {
    459         int y = getMonthHeaderSize() - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
    460         int dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2);
    461 
    462         for (int i = 0; i < mNumDays; i++) {
    463             int calendarDay = (i + mWeekStart) % mNumDays;
    464             int x = (2 * i + 1) * dayWidthHalf + mEdgePadding;
    465             mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
    466             canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
    467                     Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
    468                     mMonthDayLabelPaint);
    469         }
    470     }
    471 
    472     /**
    473      * Draws the week and month day numbers for this week. Override this method
    474      * if you need different placement.
    475      *
    476      * @param canvas The canvas to draw on
    477      */
    478     protected void drawMonthNums(Canvas canvas) {
    479         int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
    480                 + getMonthHeaderSize();
    481         final float dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2.0f);
    482         int j = findDayOffset();
    483         for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
    484             final int x = (int)((2 * j + 1) * dayWidthHalf + mEdgePadding);
    485 
    486             int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH;
    487 
    488             final int startX = (int)(x - dayWidthHalf);
    489             final int stopX = (int)(x + dayWidthHalf);
    490             final int startY = (int)(y - yRelativeToDay);
    491             final int stopY = (int)(startY + mRowHeight);
    492 
    493             drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY);
    494 
    495             j++;
    496             if (j == mNumDays) {
    497                 j = 0;
    498                 y += mRowHeight;
    499             }
    500         }
    501     }
    502 
    503     /**
    504      * This method should draw the month day.  Implemented by sub-classes to allow customization.
    505      *
    506      * @param canvas  The canvas to draw on
    507      * @param year  The year of this month day
    508      * @param month  The month of this month day
    509      * @param day  The day number of this month day
    510      * @param x  The default x position to draw the day number
    511      * @param y  The default y position to draw the day number
    512      * @param startX  The left boundary of the day number rect
    513      * @param stopX  The right boundary of the day number rect
    514      * @param startY  The top boundary of the day number rect
    515      * @param stopY  The bottom boundary of the day number rect
    516      */
    517     public abstract void drawMonthDay(Canvas canvas, int year, int month, int day,
    518             int x, int y, int startX, int stopX, int startY, int stopY);
    519 
    520     protected int findDayOffset() {
    521         return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
    522                 - mWeekStart;
    523     }
    524 
    525 
    526     /**
    527      * Calculates the day that the given x position is in, accounting for week
    528      * number. Returns the day or -1 if the position wasn't in a day.
    529      *
    530      * @param x The x position of the touch event
    531      * @return The day number, or -1 if the position wasn't in a day
    532      */
    533     public int getDayFromLocation(float x, float y) {
    534         final int day = getInternalDayFromLocation(x, y);
    535         if (day < 1 || day > mNumCells) {
    536             return -1;
    537         }
    538         return day;
    539     }
    540 
    541     /**
    542      * Calculates the day that the given x position is in, accounting for week
    543      * number.
    544      *
    545      * @param x The x position of the touch event
    546      * @return The day number
    547      */
    548     protected int getInternalDayFromLocation(float x, float y) {
    549         int dayStart = mEdgePadding;
    550         if (x < dayStart || x > mWidth - mEdgePadding) {
    551             return -1;
    552         }
    553         // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
    554         int row = (int) (y - getMonthHeaderSize()) / mRowHeight;
    555         int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mEdgePadding));
    556 
    557         int day = column - findDayOffset() + 1;
    558         day += row * mNumDays;
    559         return day;
    560     }
    561 
    562     /**
    563      * Called when the user clicks on a day. Handles callbacks to the
    564      * {@link OnDayClickListener} if one is set.
    565      * <p/>
    566      * If the day is out of the range set by minDate and/or maxDate, this is a no-op.
    567      *
    568      * @param day The day that was clicked
    569      */
    570     private void onDayClick(int day) {
    571         // If the min / max date are set, only process the click if it's a valid selection.
    572         if (isOutOfRange(mYear, mMonth, day)) {
    573             return;
    574         }
    575 
    576 
    577         if (mOnDayClickListener != null) {
    578             mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day));
    579         }
    580 
    581         // This is a no-op if accessibility is turned off.
    582         mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
    583     }
    584 
    585     /**
    586      * @return true if the specified year/month/day are within the range set by minDate and maxDate.
    587      * If one or either have not been set, they are considered as Integer.MIN_VALUE and
    588      * Integer.MAX_VALUE.
    589      */
    590     protected boolean isOutOfRange(int year, int month, int day) {
    591         if (isBeforeMin(year, month, day)) {
    592             return true;
    593         } else if (isAfterMax(year, month, day)) {
    594             return true;
    595         }
    596 
    597         return false;
    598     }
    599 
    600     private boolean isBeforeMin(int year, int month, int day) {
    601         if (mController == null) {
    602             return false;
    603         }
    604         Calendar minDate = mController.getMinDate();
    605         if (minDate == null) {
    606             return false;
    607         }
    608 
    609         if (year < minDate.get(Calendar.YEAR)) {
    610             return true;
    611         } else if (year > minDate.get(Calendar.YEAR)) {
    612             return false;
    613         }
    614 
    615         if (month < minDate.get(Calendar.MONTH)) {
    616             return true;
    617         } else if (month > minDate.get(Calendar.MONTH)) {
    618             return false;
    619         }
    620 
    621         if (day < minDate.get(Calendar.DAY_OF_MONTH)) {
    622             return true;
    623         } else {
    624             return false;
    625         }
    626     }
    627 
    628     private boolean isAfterMax(int year, int month, int day) {
    629         if (mController == null) {
    630             return false;
    631         }
    632         Calendar maxDate = mController.getMaxDate();
    633         if (maxDate == null) {
    634             return false;
    635         }
    636 
    637         if (year > maxDate.get(Calendar.YEAR)) {
    638             return true;
    639         } else if (year < maxDate.get(Calendar.YEAR)) {
    640             return false;
    641         }
    642 
    643         if (month > maxDate.get(Calendar.MONTH)) {
    644             return true;
    645         } else if (month < maxDate.get(Calendar.MONTH)) {
    646             return false;
    647         }
    648 
    649         if (day > maxDate.get(Calendar.DAY_OF_MONTH)) {
    650             return true;
    651         } else {
    652             return false;
    653         }
    654     }
    655 
    656     /**
    657      * @return The date that has accessibility focus, or {@code null} if no date
    658      *         has focus
    659      */
    660     public CalendarDay getAccessibilityFocus() {
    661         final int day = mTouchHelper.getFocusedVirtualView();
    662         if (day >= 0) {
    663             return new CalendarDay(mYear, mMonth, day);
    664         }
    665         return null;
    666     }
    667 
    668     /**
    669      * Clears accessibility focus within the view. No-op if the view does not
    670      * contain accessibility focus.
    671      */
    672     public void clearAccessibilityFocus() {
    673         mTouchHelper.clearFocusedVirtualView();
    674     }
    675 
    676     /**
    677      * Attempts to restore accessibility focus to the specified date.
    678      *
    679      * @param day The date which should receive focus
    680      * @return {@code false} if the date is not valid for this month view, or
    681      *         {@code true} if the date received focus
    682      */
    683     public boolean restoreAccessibilityFocus(CalendarDay day) {
    684         if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) {
    685             return false;
    686         }
    687         mTouchHelper.setFocusedVirtualView(day.day);
    688         return true;
    689     }
    690 
    691     /**
    692      * Provides a virtual view hierarchy for interfacing with an accessibility
    693      * service.
    694      */
    695     protected class MonthViewTouchHelper extends ExploreByTouchHelper {
    696         private static final String DATE_FORMAT = "dd MMMM yyyy";
    697 
    698         private final Rect mTempRect = new Rect();
    699         private final Calendar mTempCalendar = Calendar.getInstance();
    700 
    701         public MonthViewTouchHelper(View host) {
    702             super(host);
    703         }
    704 
    705         public void setFocusedVirtualView(int virtualViewId) {
    706             getAccessibilityNodeProvider(MonthView.this).performAction(
    707                     virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
    708         }
    709 
    710         public void clearFocusedVirtualView() {
    711             final int focusedVirtualView = getFocusedVirtualView();
    712             if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
    713                 getAccessibilityNodeProvider(MonthView.this).performAction(
    714                         focusedVirtualView,
    715                         AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
    716                         null);
    717             }
    718         }
    719 
    720         @Override
    721         protected int getVirtualViewAt(float x, float y) {
    722             final int day = getDayFromLocation(x, y);
    723             if (day >= 0) {
    724                 return day;
    725             }
    726             return ExploreByTouchHelper.INVALID_ID;
    727         }
    728 
    729         @Override
    730         protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
    731             for (int day = 1; day <= mNumCells; day++) {
    732                 virtualViewIds.add(day);
    733             }
    734         }
    735 
    736         @Override
    737         protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
    738             event.setContentDescription(getItemDescription(virtualViewId));
    739         }
    740 
    741         @Override
    742         protected void onPopulateNodeForVirtualView(int virtualViewId,
    743                 AccessibilityNodeInfoCompat node) {
    744             getItemBounds(virtualViewId, mTempRect);
    745 
    746             node.setContentDescription(getItemDescription(virtualViewId));
    747             node.setBoundsInParent(mTempRect);
    748             node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
    749 
    750             if (virtualViewId == mSelectedDay) {
    751                 node.setSelected(true);
    752             }
    753 
    754         }
    755 
    756         @Override
    757         protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
    758                 Bundle arguments) {
    759             switch (action) {
    760                 case AccessibilityNodeInfo.ACTION_CLICK:
    761                     onDayClick(virtualViewId);
    762                     return true;
    763             }
    764 
    765             return false;
    766         }
    767 
    768         /**
    769          * Calculates the bounding rectangle of a given time object.
    770          *
    771          * @param day The day to calculate bounds for
    772          * @param rect The rectangle in which to store the bounds
    773          */
    774         protected void getItemBounds(int day, Rect rect) {
    775             final int offsetX = mEdgePadding;
    776             final int offsetY = getMonthHeaderSize();
    777             final int cellHeight = mRowHeight;
    778             final int cellWidth = ((mWidth - (2 * mEdgePadding)) / mNumDays);
    779             final int index = ((day - 1) + findDayOffset());
    780             final int row = (index / mNumDays);
    781             final int column = (index % mNumDays);
    782             final int x = (offsetX + (column * cellWidth));
    783             final int y = (offsetY + (row * cellHeight));
    784 
    785             rect.set(x, y, (x + cellWidth), (y + cellHeight));
    786         }
    787 
    788         /**
    789          * Generates a description for a given time object. Since this
    790          * description will be spoken, the components are ordered by descending
    791          * specificity as DAY MONTH YEAR.
    792          *
    793          * @param day The day to generate a description for
    794          * @return A description of the time object
    795          */
    796         protected CharSequence getItemDescription(int day) {
    797             mTempCalendar.set(mYear, mMonth, day);
    798             final CharSequence date = DateFormat.format(DATE_FORMAT,
    799                     mTempCalendar.getTimeInMillis());
    800 
    801             if (day == mSelectedDay) {
    802                 return getContext().getString(R.string.item_is_selected, date);
    803             }
    804 
    805             return date;
    806         }
    807     }
    808 
    809     /**
    810      * Handles callbacks when the user clicks on a time object.
    811      */
    812     public interface OnDayClickListener {
    813         public void onDayClick(MonthView view, CalendarDay day);
    814     }
    815 }
    816