blob: d2a37ac111efc0da611ce479ebcddeed35944a63 [file] [log] [blame]
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001/*
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
17package android.widget;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
Alan Viverettefd2dd2082014-08-19 18:11:54 -070021import android.content.res.Configuration;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070022import android.content.res.Resources;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Paint.Align;
26import android.graphics.Paint.Style;
27import android.graphics.Rect;
28import android.graphics.Typeface;
29import android.os.Bundle;
30import android.text.format.DateFormat;
31import android.text.format.DateUtils;
32import android.text.format.Time;
33import android.util.AttributeSet;
Alan Viveretteffb46bf2014-10-24 12:06:11 -070034import android.util.IntArray;
Alan Viverette58780762014-09-10 17:09:13 -070035import android.util.MathUtils;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070036import android.view.MotionEvent;
37import android.view.View;
38import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.AccessibilityNodeInfo;
40
41import com.android.internal.R;
42import com.android.internal.widget.ExploreByTouchHelper;
43
Alan Viverettefd2dd2082014-08-19 18:11:54 -070044import java.text.SimpleDateFormat;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070045import java.util.Calendar;
46import java.util.Formatter;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070047import java.util.List;
48import 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 */
54class SimpleMonthView extends View {
Alan Viverette518ff0d2014-08-15 14:20:35 -070055 private static final int DEFAULT_HEIGHT = 32;
56 private static final int MIN_HEIGHT = 10;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070057
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
Alan Viverette518ff0d2014-08-15 14:20:35 -070066 private static final int DAY_SEPARATOR_WIDTH = 1;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070067
Alan Viverette50eb0252014-10-24 14:34:26 -070068 private final Formatter mFormatter;
69 private final StringBuilder mStringBuilder;
70
Alan Viverette518ff0d2014-08-15 14:20:35 -070071 private final int mMiniDayNumberTextSize;
72 private final int mMonthLabelTextSize;
73 private final int mMonthDayLabelTextSize;
74 private final int mMonthHeaderSize;
75 private final int mDaySelectedCircleSize;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070076
Alan Viverettefd2dd2082014-08-19 18:11:54 -070077 /** Single-letter (when available) formatter for the day of week label. */
78 private SimpleDateFormat mDayFormatter = new SimpleDateFormat("EEEEE", Locale.getDefault());
79
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070080 // 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
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070093 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
Alan Viverette50eb0252014-10-24 14:34:26 -0700153 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);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700159
160 final Resources res = context.getResources();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700161 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
Alan Viverettefd2dd2082014-08-19 18:11:54 -0700189 @Override
190 protected void onConfigurationChanged(Configuration newConfig) {
191 super.onConfigurationChanged(newConfig);
192
193 mDayFormatter = new SimpleDateFormat("EEEEE", newConfig.locale);
194 }
195
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700196 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
Alan Viverette518ff0d2014-08-15 14:20:35 -0700302 private static boolean isValidDayOfWeek(int day) {
Alan Viverette58780762014-09-10 17:09:13 -0700303 return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
304 }
305
306 private static boolean isValidMonth(int month) {
307 return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700308 }
309
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700310 /**
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700311 * 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.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700315 *
Alan Viverette518ff0d2014-08-15 14:20:35 -0700316 * @param selectedDay the selected day of the month, or -1 for no selection.
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700317 * @param month the month.
318 * @param year the year.
Alan Viverette58780762014-09-10 17:09:13 -0700319 * @param weekStart which day the week should start on. {@link Calendar#SUNDAY} through
320 * {@link Calendar#SATURDAY}.
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700321 * @param enabledDayStart the first enabled day.
322 * @param enabledDayEnd the last enabled day.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700323 */
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700324 void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
325 int enabledDayEnd) {
326 if (mRowHeight < MIN_HEIGHT) {
327 mRowHeight = MIN_HEIGHT;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700328 }
329
Alan Viverette518ff0d2014-08-15 14:20:35 -0700330 mSelectedDay = selectedDay;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700331
Alan Viverette58780762014-09-10 17:09:13 -0700332 if (isValidMonth(month)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700333 mMonth = month;
334 }
335 mYear = year;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700336
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
Alan Viverette518ff0d2014-08-15 14:20:35 -0700348 if (isValidDayOfWeek(weekStart)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700349 mWeekStart = weekStart;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700350 } else {
351 mWeekStart = mCalendar.getFirstDayOfWeek();
352 }
353
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700354 if (enabledDayStart > 0 && enabledDayEnd < 32) {
355 mEnabledDayStart = enabledDayStart;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700356 }
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700357 if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) {
358 mEnabledDayEnd = enabledDayEnd;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700359 }
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) {
Alan Viverette518ff0d2014-08-15 14:20:35 -0700439 final float x = (mWidth + 2 * mPadding) / 2f;
440 final float y = (mMonthHeaderSize - mMonthDayLabelTextSize) / 2f;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700441 canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
442 }
443
444 private void drawWeekDayLabels(Canvas canvas) {
Alan Viverettefd2dd2082014-08-19 18:11:54 -0700445 final int y = mMonthHeaderSize - (mMonthDayLabelTextSize / 2);
446 final int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700447
448 for (int i = 0; i < mNumDays; i++) {
Alan Viverettefd2dd2082014-08-19 18:11:54 -0700449 final int calendarDay = (i + mWeekStart) % mNumDays;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700450 mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
Alan Viverettefd2dd2082014-08-19 18:11:54 -0700451
452 final String dayLabel = mDayFormatter.format(mDayLabelCalendar.getTime());
453 final int x = (2 * i + 1) * dayWidthHalf + mPadding;
454 canvas.drawText(dayLabel, x, y, mMonthDayLabelPaint);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700455 }
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
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700612 protected void getVisibleVirtualViews(IntArray virtualViewIds) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700613 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}