Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.ColorStateList;
     21 import android.content.res.Configuration;
     22 import android.content.res.Resources;
     23 import android.graphics.Canvas;
     24 import android.graphics.Paint;
     25 import android.graphics.Paint.Align;
     26 import android.graphics.Paint.Style;
     27 import android.graphics.Rect;
     28 import android.graphics.Typeface;
     29 import android.os.Bundle;
     30 import android.text.format.DateFormat;
     31 import android.text.format.DateUtils;
     32 import android.text.format.Time;
     33 import android.util.AttributeSet;
     34 import android.util.MathUtils;
     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.internal.R;
     41 import com.android.internal.widget.ExploreByTouchHelper;
     42 
     43 import java.text.SimpleDateFormat;
     44 import java.util.Calendar;
     45 import java.util.Formatter;
     46 import java.util.List;
     47 import java.util.Locale;
     48 
     49 /**
     50  * A calendar-like view displaying a specified month and the appropriate selectable day numbers
     51  * within the specified month.
     52  */
     53 class SimpleMonthView extends View {
     54     private static final String TAG = "SimpleMonthView";
     55 
     56     private static final int DEFAULT_HEIGHT = 32;
     57     private static final int MIN_HEIGHT = 10;
     58 
     59     private static final int DEFAULT_SELECTED_DAY = -1;
     60     private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
     61     private static final int DEFAULT_NUM_DAYS = 7;
     62     private static final int DEFAULT_NUM_ROWS = 6;
     63     private static final int MAX_NUM_ROWS = 6;
     64 
     65     private static final int SELECTED_CIRCLE_ALPHA = 60;
     66 
     67     private static final int DAY_SEPARATOR_WIDTH = 1;
     68 
     69     private final int mMiniDayNumberTextSize;
     70     private final int mMonthLabelTextSize;
     71     private final int mMonthDayLabelTextSize;
     72     private final int mMonthHeaderSize;
     73     private final int mDaySelectedCircleSize;
     74 
     75     // used for scaling to the device density
     76     private static float mScale = 0;
     77 
     78     /** Single-letter (when available) formatter for the day of week label. */
     79     private SimpleDateFormat mDayFormatter = new SimpleDateFormat("EEEEE", Locale.getDefault());
     80 
     81     // affects the padding on the sides of this view
     82     private int mPadding = 0;
     83 
     84     private String mDayOfWeekTypeface;
     85     private String mMonthTitleTypeface;
     86 
     87     private Paint mDayNumberPaint;
     88     private Paint mDayNumberDisabledPaint;
     89     private Paint mDayNumberSelectedPaint;
     90 
     91     private Paint mMonthTitlePaint;
     92     private Paint mMonthDayLabelPaint;
     93 
     94     private final Formatter mFormatter;
     95     private final StringBuilder mStringBuilder;
     96 
     97     private int mMonth;
     98     private int mYear;
     99 
    100     // Quick reference to the width of this view, matches parent
    101     private int mWidth;
    102 
    103     // The height this view should draw at in pixels, set by height param
    104     private int mRowHeight = DEFAULT_HEIGHT;
    105 
    106     // If this view contains the today
    107     private boolean mHasToday = false;
    108 
    109     // Which day is selected [0-6] or -1 if no day is selected
    110     private int mSelectedDay = -1;
    111 
    112     // Which day is today [0-6] or -1 if no day is today
    113     private int mToday = DEFAULT_SELECTED_DAY;
    114 
    115     // Which day of the week to start on [0-6]
    116     private int mWeekStart = DEFAULT_WEEK_START;
    117 
    118     // How many days to display
    119     private int mNumDays = DEFAULT_NUM_DAYS;
    120 
    121     // The number of days + a spot for week number if it is displayed
    122     private int mNumCells = mNumDays;
    123 
    124     private int mDayOfWeekStart = 0;
    125 
    126     // First enabled day
    127     private int mEnabledDayStart = 1;
    128 
    129     // Last enabled day
    130     private int mEnabledDayEnd = 31;
    131 
    132     private final Calendar mCalendar = Calendar.getInstance();
    133     private final Calendar mDayLabelCalendar = Calendar.getInstance();
    134 
    135     private final MonthViewTouchHelper mTouchHelper;
    136 
    137     private int mNumRows = DEFAULT_NUM_ROWS;
    138 
    139     // Optional listener for handling day click actions
    140     private OnDayClickListener mOnDayClickListener;
    141 
    142     // Whether to prevent setting the accessibility delegate
    143     private boolean mLockAccessibilityDelegate;
    144 
    145     private int mNormalTextColor;
    146     private int mDisabledTextColor;
    147     private int mSelectedDayColor;
    148 
    149     public SimpleMonthView(Context context) {
    150         this(context, null);
    151     }
    152 
    153     public SimpleMonthView(Context context, AttributeSet attrs) {
    154         this(context, attrs, R.attr.datePickerStyle);
    155     }
    156 
    157     public SimpleMonthView(Context context, AttributeSet attrs, int defStyle) {
    158         super(context, attrs);
    159 
    160         final Resources res = context.getResources();
    161 
    162         mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
    163         mMonthTitleTypeface = res.getString(R.string.sans_serif);
    164 
    165         mStringBuilder = new StringBuilder(50);
    166         mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
    167 
    168         mMiniDayNumberTextSize = res.getDimensionPixelSize(R.dimen.datepicker_day_number_size);
    169         mMonthLabelTextSize = res.getDimensionPixelSize(R.dimen.datepicker_month_label_size);
    170         mMonthDayLabelTextSize = res.getDimensionPixelSize(
    171                 R.dimen.datepicker_month_day_label_text_size);
    172         mMonthHeaderSize = res.getDimensionPixelOffset(
    173                 R.dimen.datepicker_month_list_item_header_height);
    174         mDaySelectedCircleSize = res.getDimensionPixelSize(
    175                 R.dimen.datepicker_day_number_select_circle_radius);
    176 
    177         mRowHeight = (res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height)
    178                 - mMonthHeaderSize) / MAX_NUM_ROWS;
    179 
    180         // Set up accessibility components.
    181         mTouchHelper = new MonthViewTouchHelper(this);
    182         setAccessibilityDelegate(mTouchHelper);
    183         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    184         mLockAccessibilityDelegate = true;
    185 
    186         // Sets up any standard paints that will be used
    187         initView();
    188     }
    189 
    190     @Override
    191     protected void onConfigurationChanged(Configuration newConfig) {
    192         super.onConfigurationChanged(newConfig);
    193 
    194         mDayFormatter = new SimpleDateFormat("EEEEE", newConfig.locale);
    195     }
    196 
    197     void setTextColor(ColorStateList colors) {
    198         final Resources res = getContext().getResources();
    199 
    200         mNormalTextColor = colors.getColorForState(ENABLED_STATE_SET,
    201                 res.getColor(R.color.datepicker_default_normal_text_color_holo_light));
    202         mMonthTitlePaint.setColor(mNormalTextColor);
    203         mMonthDayLabelPaint.setColor(mNormalTextColor);
    204 
    205         mDisabledTextColor = colors.getColorForState(EMPTY_STATE_SET,
    206                 res.getColor(R.color.datepicker_default_disabled_text_color_holo_light));
    207         mDayNumberDisabledPaint.setColor(mDisabledTextColor);
    208 
    209         mSelectedDayColor = colors.getColorForState(ENABLED_SELECTED_STATE_SET,
    210                 res.getColor(R.color.holo_blue_light));
    211         mDayNumberSelectedPaint.setColor(mSelectedDayColor);
    212         mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA);
    213     }
    214 
    215     @Override
    216     public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
    217         // Workaround for a JB MR1 issue where accessibility delegates on
    218         // top-level ListView items are overwritten.
    219         if (!mLockAccessibilityDelegate) {
    220             super.setAccessibilityDelegate(delegate);
    221         }
    222     }
    223 
    224     public void setOnDayClickListener(OnDayClickListener listener) {
    225         mOnDayClickListener = listener;
    226     }
    227 
    228     @Override
    229     public boolean dispatchHoverEvent(MotionEvent event) {
    230         // First right-of-refusal goes the touch exploration helper.
    231         if (mTouchHelper.dispatchHoverEvent(event)) {
    232             return true;
    233         }
    234         return super.dispatchHoverEvent(event);
    235     }
    236 
    237     @Override
    238     public boolean onTouchEvent(MotionEvent event) {
    239         switch (event.getAction()) {
    240             case MotionEvent.ACTION_UP:
    241                 final int day = getDayFromLocation(event.getX(), event.getY());
    242                 if (day >= 0) {
    243                     onDayClick(day);
    244                 }
    245                 break;
    246         }
    247         return true;
    248     }
    249 
    250     /**
    251      * Sets up the text and style properties for painting.
    252      */
    253     private void initView() {
    254         mMonthTitlePaint = new Paint();
    255         mMonthTitlePaint.setAntiAlias(true);
    256         mMonthTitlePaint.setColor(mNormalTextColor);
    257         mMonthTitlePaint.setTextSize(mMonthLabelTextSize);
    258         mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
    259         mMonthTitlePaint.setTextAlign(Align.CENTER);
    260         mMonthTitlePaint.setStyle(Style.FILL);
    261         mMonthTitlePaint.setFakeBoldText(true);
    262 
    263         mMonthDayLabelPaint = new Paint();
    264         mMonthDayLabelPaint.setAntiAlias(true);
    265         mMonthDayLabelPaint.setColor(mNormalTextColor);
    266         mMonthDayLabelPaint.setTextSize(mMonthDayLabelTextSize);
    267         mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
    268         mMonthDayLabelPaint.setTextAlign(Align.CENTER);
    269         mMonthDayLabelPaint.setStyle(Style.FILL);
    270         mMonthDayLabelPaint.setFakeBoldText(true);
    271 
    272         mDayNumberSelectedPaint = new Paint();
    273         mDayNumberSelectedPaint.setAntiAlias(true);
    274         mDayNumberSelectedPaint.setColor(mSelectedDayColor);
    275         mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA);
    276         mDayNumberSelectedPaint.setTextAlign(Align.CENTER);
    277         mDayNumberSelectedPaint.setStyle(Style.FILL);
    278         mDayNumberSelectedPaint.setFakeBoldText(true);
    279 
    280         mDayNumberPaint = new Paint();
    281         mDayNumberPaint.setAntiAlias(true);
    282         mDayNumberPaint.setTextSize(mMiniDayNumberTextSize);
    283         mDayNumberPaint.setTextAlign(Align.CENTER);
    284         mDayNumberPaint.setStyle(Style.FILL);
    285         mDayNumberPaint.setFakeBoldText(false);
    286 
    287         mDayNumberDisabledPaint = new Paint();
    288         mDayNumberDisabledPaint.setAntiAlias(true);
    289         mDayNumberDisabledPaint.setColor(mDisabledTextColor);
    290         mDayNumberDisabledPaint.setTextSize(mMiniDayNumberTextSize);
    291         mDayNumberDisabledPaint.setTextAlign(Align.CENTER);
    292         mDayNumberDisabledPaint.setStyle(Style.FILL);
    293         mDayNumberDisabledPaint.setFakeBoldText(false);
    294     }
    295 
    296     @Override
    297     protected void onDraw(Canvas canvas) {
    298         drawMonthTitle(canvas);
    299         drawWeekDayLabels(canvas);
    300         drawDays(canvas);
    301     }
    302 
    303     private static boolean isValidDayOfWeek(int day) {
    304         return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
    305     }
    306 
    307     private static boolean isValidMonth(int month) {
    308         return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
    309     }
    310 
    311     /**
    312      * Sets all the parameters for displaying this week. Parameters have a default value and
    313      * will only update if a new value is included, except for focus month, which will always
    314      * default to no focus month if no value is passed in. The only required parameter is the
    315      * week start.
    316      *
    317      * @param selectedDay the selected day of the month, or -1 for no selection.
    318      * @param month the month.
    319      * @param year the year.
    320      * @param weekStart which day the week should start on. {@link Calendar#SUNDAY} through
    321      *        {@link Calendar#SATURDAY}.
    322      * @param enabledDayStart the first enabled day.
    323      * @param enabledDayEnd the last enabled day.
    324      */
    325     void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
    326             int enabledDayEnd) {
    327         if (mRowHeight < MIN_HEIGHT) {
    328             mRowHeight = MIN_HEIGHT;
    329         }
    330 
    331         mSelectedDay = selectedDay;
    332 
    333         if (isValidMonth(month)) {
    334             mMonth = month;
    335         }
    336         mYear = year;
    337 
    338         // Figure out what day today is
    339         final Time today = new Time(Time.getCurrentTimezone());
    340         today.setToNow();
    341         mHasToday = false;
    342         mToday = -1;
    343 
    344         mCalendar.set(Calendar.MONTH, mMonth);
    345         mCalendar.set(Calendar.YEAR, mYear);
    346         mCalendar.set(Calendar.DAY_OF_MONTH, 1);
    347         mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
    348 
    349         if (isValidDayOfWeek(weekStart)) {
    350             mWeekStart = weekStart;
    351         } else {
    352             mWeekStart = mCalendar.getFirstDayOfWeek();
    353         }
    354 
    355         if (enabledDayStart > 0 && enabledDayEnd < 32) {
    356             mEnabledDayStart = enabledDayStart;
    357         }
    358         if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) {
    359             mEnabledDayEnd = enabledDayEnd;
    360         }
    361 
    362         mNumCells = getDaysInMonth(mMonth, mYear);
    363         for (int i = 0; i < mNumCells; i++) {
    364             final int day = i + 1;
    365             if (sameDay(day, today)) {
    366                 mHasToday = true;
    367                 mToday = day;
    368             }
    369         }
    370         mNumRows = calculateNumRows();
    371 
    372         // Invalidate cached accessibility information.
    373         mTouchHelper.invalidateRoot();
    374     }
    375 
    376     private static int getDaysInMonth(int month, int year) {
    377         switch (month) {
    378             case Calendar.JANUARY:
    379             case Calendar.MARCH:
    380             case Calendar.MAY:
    381             case Calendar.JULY:
    382             case Calendar.AUGUST:
    383             case Calendar.OCTOBER:
    384             case Calendar.DECEMBER:
    385                 return 31;
    386             case Calendar.APRIL:
    387             case Calendar.JUNE:
    388             case Calendar.SEPTEMBER:
    389             case Calendar.NOVEMBER:
    390                 return 30;
    391             case Calendar.FEBRUARY:
    392                 return (year % 4 == 0) ? 29 : 28;
    393             default:
    394                 throw new IllegalArgumentException("Invalid Month");
    395         }
    396     }
    397 
    398     public void reuse() {
    399         mNumRows = DEFAULT_NUM_ROWS;
    400         requestLayout();
    401     }
    402 
    403     private int calculateNumRows() {
    404         int offset = findDayOffset();
    405         int dividend = (offset + mNumCells) / mNumDays;
    406         int remainder = (offset + mNumCells) % mNumDays;
    407         return (dividend + (remainder > 0 ? 1 : 0));
    408     }
    409 
    410     private boolean sameDay(int day, Time today) {
    411         return mYear == today.year &&
    412                 mMonth == today.month &&
    413                 day == today.monthDay;
    414     }
    415 
    416     @Override
    417     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    418         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
    419                 + mMonthHeaderSize);
    420     }
    421 
    422     @Override
    423     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    424         mWidth = w;
    425 
    426         // Invalidate cached accessibility information.
    427         mTouchHelper.invalidateRoot();
    428     }
    429 
    430     private String getMonthAndYearString() {
    431         int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
    432                 | DateUtils.FORMAT_NO_MONTH_DAY;
    433         mStringBuilder.setLength(0);
    434         long millis = mCalendar.getTimeInMillis();
    435         return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
    436                 Time.getCurrentTimezone()).toString();
    437     }
    438 
    439     private void drawMonthTitle(Canvas canvas) {
    440         final float x = (mWidth + 2 * mPadding) / 2f;
    441         final float y = (mMonthHeaderSize - mMonthDayLabelTextSize) / 2f;
    442         canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
    443     }
    444 
    445     private void drawWeekDayLabels(Canvas canvas) {
    446         final int y = mMonthHeaderSize - (mMonthDayLabelTextSize / 2);
    447         final int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
    448 
    449         for (int i = 0; i < mNumDays; i++) {
    450             final int calendarDay = (i + mWeekStart) % mNumDays;
    451             mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
    452 
    453             final String dayLabel = mDayFormatter.format(mDayLabelCalendar.getTime());
    454             final int x = (2 * i + 1) * dayWidthHalf + mPadding;
    455             canvas.drawText(dayLabel, x, y, mMonthDayLabelPaint);
    456         }
    457     }
    458 
    459     /**
    460      * Draws the month days.
    461      */
    462     private void drawDays(Canvas canvas) {
    463         int y = (((mRowHeight + mMiniDayNumberTextSize) / 2) - DAY_SEPARATOR_WIDTH)
    464                 + mMonthHeaderSize;
    465         int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
    466         int j = findDayOffset();
    467         for (int day = 1; day <= mNumCells; day++) {
    468             int x = (2 * j + 1) * dayWidthHalf + mPadding;
    469             if (mSelectedDay == day) {
    470                 canvas.drawCircle(x, y - (mMiniDayNumberTextSize / 3), mDaySelectedCircleSize,
    471                         mDayNumberSelectedPaint);
    472             }
    473 
    474             if (mHasToday && mToday == day) {
    475                 mDayNumberPaint.setColor(mSelectedDayColor);
    476             } else {
    477                 mDayNumberPaint.setColor(mNormalTextColor);
    478             }
    479             final Paint paint = (day < mEnabledDayStart || day > mEnabledDayEnd) ?
    480                     mDayNumberDisabledPaint : mDayNumberPaint;
    481             canvas.drawText(String.format("%d", day), x, y, paint);
    482             j++;
    483             if (j == mNumDays) {
    484                 j = 0;
    485                 y += mRowHeight;
    486             }
    487         }
    488     }
    489 
    490     private int findDayOffset() {
    491         return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
    492                 - mWeekStart;
    493     }
    494 
    495     /**
    496      * Calculates the day that the given x position is in, accounting for week
    497      * number. Returns the day or -1 if the position wasn't in a day.
    498      *
    499      * @param x The x position of the touch event
    500      * @return The day number, or -1 if the position wasn't in a day
    501      */
    502     private int getDayFromLocation(float x, float y) {
    503         int dayStart = mPadding;
    504         if (x < dayStart || x > mWidth - mPadding) {
    505             return -1;
    506         }
    507         // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
    508         int row = (int) (y - mMonthHeaderSize) / mRowHeight;
    509         int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
    510 
    511         int day = column - findDayOffset() + 1;
    512         day += row * mNumDays;
    513         if (day < 1 || day > mNumCells) {
    514             return -1;
    515         }
    516         return day;
    517     }
    518 
    519     /**
    520      * Called when the user clicks on a day. Handles callbacks to the
    521      * {@link OnDayClickListener} if one is set.
    522      *
    523      * @param day The day that was clicked
    524      */
    525     private void onDayClick(int day) {
    526         if (mOnDayClickListener != null) {
    527             Calendar date = Calendar.getInstance();
    528             date.set(mYear, mMonth, day);
    529             mOnDayClickListener.onDayClick(this, date);
    530         }
    531 
    532         // This is a no-op if accessibility is turned off.
    533         mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
    534     }
    535 
    536     /**
    537      * @return The date that has accessibility focus, or {@code null} if no date
    538      *         has focus
    539      */
    540     Calendar getAccessibilityFocus() {
    541         final int day = mTouchHelper.getFocusedVirtualView();
    542         Calendar date = null;
    543         if (day >= 0) {
    544             date = Calendar.getInstance();
    545             date.set(mYear, mMonth, day);
    546         }
    547         return date;
    548     }
    549 
    550     /**
    551      * Clears accessibility focus within the view. No-op if the view does not
    552      * contain accessibility focus.
    553      */
    554     public void clearAccessibilityFocus() {
    555         mTouchHelper.clearFocusedVirtualView();
    556     }
    557 
    558     /**
    559      * Attempts to restore accessibility focus to the specified date.
    560      *
    561      * @param day The date which should receive focus
    562      * @return {@code false} if the date is not valid for this month view, or
    563      *         {@code true} if the date received focus
    564      */
    565     boolean restoreAccessibilityFocus(Calendar day) {
    566         if ((day.get(Calendar.YEAR) != mYear) || (day.get(Calendar.MONTH) != mMonth) ||
    567                 (day.get(Calendar.DAY_OF_MONTH) > mNumCells)) {
    568             return false;
    569         }
    570         mTouchHelper.setFocusedVirtualView(day.get(Calendar.DAY_OF_MONTH));
    571         return true;
    572     }
    573 
    574     /**
    575      * Provides a virtual view hierarchy for interfacing with an accessibility
    576      * service.
    577      */
    578     private class MonthViewTouchHelper extends ExploreByTouchHelper {
    579         private static final String DATE_FORMAT = "dd MMMM yyyy";
    580 
    581         private final Rect mTempRect = new Rect();
    582         private final Calendar mTempCalendar = Calendar.getInstance();
    583 
    584         public MonthViewTouchHelper(View host) {
    585             super(host);
    586         }
    587 
    588         public void setFocusedVirtualView(int virtualViewId) {
    589             getAccessibilityNodeProvider(SimpleMonthView.this).performAction(
    590                     virtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
    591         }
    592 
    593         public void clearFocusedVirtualView() {
    594             final int focusedVirtualView = getFocusedVirtualView();
    595             if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
    596                 getAccessibilityNodeProvider(SimpleMonthView.this).performAction(
    597                         focusedVirtualView,
    598                         AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
    599                         null);
    600             }
    601         }
    602 
    603         @Override
    604         protected int getVirtualViewAt(float x, float y) {
    605             final int day = getDayFromLocation(x, y);
    606             if (day >= 0) {
    607                 return day;
    608             }
    609             return ExploreByTouchHelper.INVALID_ID;
    610         }
    611 
    612         @Override
    613         protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
    614             for (int day = 1; day <= mNumCells; day++) {
    615                 virtualViewIds.add(day);
    616             }
    617         }
    618 
    619         @Override
    620         protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
    621             event.setContentDescription(getItemDescription(virtualViewId));
    622         }
    623 
    624         @Override
    625         protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
    626             getItemBounds(virtualViewId, mTempRect);
    627 
    628             node.setContentDescription(getItemDescription(virtualViewId));
    629             node.setBoundsInParent(mTempRect);
    630             node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
    631 
    632             if (virtualViewId == mSelectedDay) {
    633                 node.setSelected(true);
    634             }
    635 
    636         }
    637 
    638         @Override
    639         protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
    640                 Bundle arguments) {
    641             switch (action) {
    642                 case AccessibilityNodeInfo.ACTION_CLICK:
    643                     onDayClick(virtualViewId);
    644                     return true;
    645             }
    646 
    647             return false;
    648         }
    649 
    650         /**
    651          * Calculates the bounding rectangle of a given time object.
    652          *
    653          * @param day The day to calculate bounds for
    654          * @param rect The rectangle in which to store the bounds
    655          */
    656         private void getItemBounds(int day, Rect rect) {
    657             final int offsetX = mPadding;
    658             final int offsetY = mMonthHeaderSize;
    659             final int cellHeight = mRowHeight;
    660             final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays);
    661             final int index = ((day - 1) + findDayOffset());
    662             final int row = (index / mNumDays);
    663             final int column = (index % mNumDays);
    664             final int x = (offsetX + (column * cellWidth));
    665             final int y = (offsetY + (row * cellHeight));
    666 
    667             rect.set(x, y, (x + cellWidth), (y + cellHeight));
    668         }
    669 
    670         /**
    671          * Generates a description for a given time object. Since this
    672          * description will be spoken, the components are ordered by descending
    673          * specificity as DAY MONTH YEAR.
    674          *
    675          * @param day The day to generate a description for
    676          * @return A description of the time object
    677          */
    678         private CharSequence getItemDescription(int day) {
    679             mTempCalendar.set(mYear, mMonth, day);
    680             final CharSequence date = DateFormat.format(DATE_FORMAT,
    681                     mTempCalendar.getTimeInMillis());
    682 
    683             if (day == mSelectedDay) {
    684                 return getContext().getString(R.string.item_is_selected, date);
    685             }
    686 
    687             return date;
    688         }
    689     }
    690 
    691     /**
    692      * Handles callbacks when the user clicks on a time object.
    693      */
    694     public interface OnDayClickListener {
    695         public void onDayClick(SimpleMonthView view, Calendar day);
    696     }
    697 }
    698