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