• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
SimpleMonthView(Context context)149     public SimpleMonthView(Context context) {
150         this(context, null);
151     }
152 
SimpleMonthView(Context context, AttributeSet attrs)153     public SimpleMonthView(Context context, AttributeSet attrs) {
154         this(context, attrs, R.attr.datePickerStyle);
155     }
156 
SimpleMonthView(Context context, AttributeSet attrs, int defStyle)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
onConfigurationChanged(Configuration newConfig)191     protected void onConfigurationChanged(Configuration newConfig) {
192         super.onConfigurationChanged(newConfig);
193 
194         mDayFormatter = new SimpleDateFormat("EEEEE", newConfig.locale);
195     }
196 
setTextColor(ColorStateList colors)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
setAccessibilityDelegate(AccessibilityDelegate delegate)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 
setOnDayClickListener(OnDayClickListener listener)224     public void setOnDayClickListener(OnDayClickListener listener) {
225         mOnDayClickListener = listener;
226     }
227 
228     @Override
dispatchHoverEvent(MotionEvent event)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
onTouchEvent(MotionEvent event)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      */
initView()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
onDraw(Canvas canvas)297     protected void onDraw(Canvas canvas) {
298         drawMonthTitle(canvas);
299         drawWeekDayLabels(canvas);
300         drawDays(canvas);
301     }
302 
isValidDayOfWeek(int day)303     private static boolean isValidDayOfWeek(int day) {
304         return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
305     }
306 
isValidMonth(int month)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      */
setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart, int enabledDayEnd)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 
getDaysInMonth(int month, int year)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 
reuse()398     public void reuse() {
399         mNumRows = DEFAULT_NUM_ROWS;
400         requestLayout();
401     }
402 
calculateNumRows()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 
sameDay(int day, Time today)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)417     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
418         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
419                 + mMonthHeaderSize);
420     }
421 
422     @Override
onSizeChanged(int w, int h, int oldw, int oldh)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 
getMonthAndYearString()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 
drawMonthTitle(Canvas canvas)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 
drawWeekDayLabels(Canvas canvas)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      */
drawDays(Canvas canvas)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 
findDayOffset()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      */
getDayFromLocation(float x, float y)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      */
onDayClick(int day)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      */
getAccessibilityFocus()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      */
clearAccessibilityFocus()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      */
restoreAccessibilityFocus(Calendar day)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 
MonthViewTouchHelper(View host)584         public MonthViewTouchHelper(View host) {
585             super(host);
586         }
587 
setFocusedVirtualView(int virtualViewId)588         public void setFocusedVirtualView(int virtualViewId) {
589             getAccessibilityNodeProvider(SimpleMonthView.this).performAction(
590                     virtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
591         }
592 
clearFocusedVirtualView()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
getVirtualViewAt(float x, float y)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
getVisibleVirtualViews(List<Integer> virtualViewIds)613         protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
614             for (int day = 1; day <= mNumCells; day++) {
615                 virtualViewIds.add(day);
616             }
617         }
618 
619         @Override
onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event)620         protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
621             event.setContentDescription(getItemDescription(virtualViewId));
622         }
623 
624         @Override
onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node)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
onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments)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          */
getItemBounds(int day, Rect rect)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          */
getItemDescription(int day)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 {
onDayClick(SimpleMonthView view, Calendar day)695         public void onDayClick(SimpleMonthView view, Calendar day);
696     }
697 }
698