blob: aa7f0b689065195d659963c6730f322d00eb2a98 [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;
21import android.content.res.Resources;
Alan Viverette0ef59ac2015-03-23 13:13:25 -070022import android.content.res.TypedArray;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070023import 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;
Alan Viverette60b674e2015-03-25 13:00:42 -070029import android.graphics.drawable.Drawable;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070030import android.os.Bundle;
Alan Viverette5dc973c2015-01-08 11:12:39 -080031import android.text.TextPaint;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070032import android.text.format.DateFormat;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070033import android.util.AttributeSet;
Alan Viveretteffb46bf2014-10-24 12:06:11 -070034import android.util.IntArray;
Alan Viverettec5b95c22015-01-07 13:57:12 -080035import android.util.StateSet;
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;
Alan Viverette0ef59ac2015-03-23 13:13:25 -070040import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070041
42import com.android.internal.R;
43import com.android.internal.widget.ExploreByTouchHelper;
44
Alan Viverettefd2dd2082014-08-19 18:11:54 -070045import java.text.SimpleDateFormat;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070046import java.util.Calendar;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070047import 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 */
53class SimpleMonthView extends View {
Alan Viverette0ef59ac2015-03-23 13:13:25 -070054 private static final int DAYS_IN_WEEK = 7;
55 private static final int MAX_WEEKS_IN_MONTH = 6;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070056
57 private static final int DEFAULT_SELECTED_DAY = -1;
58 private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070059
Alan Viverette0ef59ac2015-03-23 13:13:25 -070060 private static final String DEFAULT_TITLE_FORMAT = "MMMMy";
61 private static final String DAY_OF_WEEK_FORMAT = "EEEEE";
Alan Viverettec5b95c22015-01-07 13:57:12 -080062
Alan Viverette60b674e2015-03-25 13:00:42 -070063 /** Virtual view ID for previous button. */
64 private static final int ITEM_ID_PREV = 0x101;
65
66 /** Virtual view ID for next button. */
67 private static final int ITEM_ID_NEXT = 0x100;
68
Alan Viverette5dc973c2015-01-08 11:12:39 -080069 private final TextPaint mMonthPaint = new TextPaint();
70 private final TextPaint mDayOfWeekPaint = new TextPaint();
71 private final TextPaint mDayPaint = new TextPaint();
Alan Viverette0ef59ac2015-03-23 13:13:25 -070072 private final Paint mDaySelectorPaint = new Paint();
73 private final Paint mDayHighlightPaint = new Paint();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070074
75 private final Calendar mCalendar = Calendar.getInstance();
Alan Viverette60b674e2015-03-25 13:00:42 -070076 private final Calendar mDayOfWeekLabelCalendar = Calendar.getInstance();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070077
78 private final MonthViewTouchHelper mTouchHelper;
79
Alan Viverette0ef59ac2015-03-23 13:13:25 -070080 private final SimpleDateFormat mTitleFormatter;
81 private final SimpleDateFormat mDayOfWeekFormatter;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070082
Alan Viverettef63757b2015-04-01 17:14:45 -070083 // Desired dimensions.
84 private final int mDesiredMonthHeight;
85 private final int mDesiredDayOfWeekHeight;
86 private final int mDesiredDayHeight;
87 private final int mDesiredCellWidth;
88 private final int mDesiredDaySelectorRadius;
Alan Viverette60b674e2015-03-25 13:00:42 -070089
90 // Next/previous drawables.
91 private final Drawable mPrevDrawable;
92 private final Drawable mNextDrawable;
93 private final Rect mPrevHitArea;
94 private final Rect mNextHitArea;
95 private final CharSequence mPrevContentDesc;
96 private final CharSequence mNextContentDesc;
97
Alan Viverette0ef59ac2015-03-23 13:13:25 -070098 private CharSequence mTitle;
99
100 private int mMonth;
101 private int mYear;
102
Alan Viverettef63757b2015-04-01 17:14:45 -0700103 // Dimensions as laid out.
104 private int mMonthHeight;
105 private int mDayOfWeekHeight;
106 private int mDayHeight;
107 private int mCellWidth;
108 private int mDaySelectorRadius;
109
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700110 private int mPaddedWidth;
111 private int mPaddedHeight;
112
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700113 /** The day of month for the selected day, or -1 if no day is selected. */
114 private int mActivatedDay = -1;
115
116 /**
117 * The day of month for today, or -1 if the today is not in the current
118 * month.
119 */
120 private int mToday = DEFAULT_SELECTED_DAY;
121
122 /** The first day of the week (ex. Calendar.SUNDAY). */
123 private int mWeekStart = DEFAULT_WEEK_START;
124
125 /** The number of days (ex. 28) in the current month. */
126 private int mDaysInMonth;
127
128 /**
129 * The day of week (ex. Calendar.SUNDAY) for the first day of the current
130 * month.
131 */
132 private int mDayOfWeekStart;
133
134 /** The day of month for the first (inclusive) enabled day. */
135 private int mEnabledDayStart = 1;
136
137 /** The day of month for the last (inclusive) enabled day. */
138 private int mEnabledDayEnd = 31;
139
140 /** The number of week rows needed to display the current month. */
141 private int mNumWeeks = MAX_WEEKS_IN_MONTH;
142
143 /** Optional listener for handling day click actions. */
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700144 private OnDayClickListener mOnDayClickListener;
145
Alan Viverettec5b95c22015-01-07 13:57:12 -0800146 private ColorStateList mDayTextColor;
147
Alan Viverette60b674e2015-03-25 13:00:42 -0700148 private int mTouchedItem = -1;
149
150 private boolean mPrevEnabled;
151 private boolean mNextEnabled;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700152
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700153 public SimpleMonthView(Context context) {
154 this(context, null);
155 }
156
157 public SimpleMonthView(Context context, AttributeSet attrs) {
158 this(context, attrs, R.attr.datePickerStyle);
159 }
160
Alan Viverette50eb0252014-10-24 14:34:26 -0700161 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
162 this(context, attrs, defStyleAttr, 0);
163 }
164
165 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
166 super(context, attrs, defStyleAttr, defStyleRes);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700167
168 final Resources res = context.getResources();
Alan Viverettef63757b2015-04-01 17:14:45 -0700169 mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
170 mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
171 mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
172 mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
173 mDesiredDaySelectorRadius = res.getDimensionPixelSize(R.dimen.date_picker_day_selector_radius);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700174
Alan Viverette60b674e2015-03-25 13:00:42 -0700175 mPrevDrawable = context.getDrawable(R.drawable.ic_chevron_left);
176 mNextDrawable = context.getDrawable(R.drawable.ic_chevron_right);
177 mPrevHitArea = mPrevDrawable != null ? new Rect() : null;
178 mNextHitArea = mNextDrawable != null ? new Rect() : null;
179 mPrevContentDesc = res.getText(R.string.date_picker_prev_month_button);
180 mNextContentDesc = res.getText(R.string.date_picker_next_month_button);
181
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700182 // Set up accessibility components.
183 mTouchHelper = new MonthViewTouchHelper(this);
184 setAccessibilityDelegate(mTouchHelper);
185 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700186
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700187 final Locale locale = res.getConfiguration().locale;
188 final String titleFormat = DateFormat.getBestDateTimePattern(locale, DEFAULT_TITLE_FORMAT);
189 mTitleFormatter = new SimpleDateFormat(titleFormat, locale);
190 mDayOfWeekFormatter = new SimpleDateFormat(DAY_OF_WEEK_FORMAT, locale);
191
192 setClickable(true);
193 initPaints(res);
194 }
195
Alan Viverette60b674e2015-03-25 13:00:42 -0700196 public void setNextEnabled(boolean enabled) {
197 mNextEnabled = enabled;
198 mTouchHelper.invalidateRoot();
199 invalidate();
200 }
201
202 public void setPrevEnabled(boolean enabled) {
203 mPrevEnabled = enabled;
204 mTouchHelper.invalidateRoot();
205 invalidate();
206 }
207
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700208 /**
209 * Applies the specified text appearance resource to a paint, returning the
210 * text color if one is set in the text appearance.
211 *
212 * @param p the paint to modify
213 * @param resId the resource ID of the text appearance
214 * @return the text color, if available
215 */
216 private ColorStateList applyTextAppearance(Paint p, int resId) {
217 final TypedArray ta = mContext.obtainStyledAttributes(null,
218 R.styleable.TextAppearance, 0, resId);
219
220 final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
221 if (fontFamily != null) {
222 p.setTypeface(Typeface.create(fontFamily, 0));
223 }
224
225 p.setTextSize(ta.getDimensionPixelSize(
226 R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
227
228 final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
229 if (textColor != null) {
230 final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
231 p.setColor(enabledColor);
232 }
233
234 ta.recycle();
235
236 return textColor;
237 }
238
239 public void setMonthTextAppearance(int resId) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700240 final ColorStateList monthColor = applyTextAppearance(mMonthPaint, resId);
241 if (monthColor != null) {
242 if (mPrevDrawable != null) {
243 mPrevDrawable.setTintList(monthColor);
244 }
245 if (mNextDrawable != null) {
246 mNextDrawable.setTintList(monthColor);
247 }
248 }
249
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700250 invalidate();
251 }
252
253 public void setDayOfWeekTextAppearance(int resId) {
254 applyTextAppearance(mDayOfWeekPaint, resId);
255 invalidate();
256 }
257
258 public void setDayTextAppearance(int resId) {
259 final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
260 if (textColor != null) {
261 mDayTextColor = textColor;
262 }
263
264 invalidate();
265 }
266
267 public CharSequence getTitle() {
268 if (mTitle == null) {
269 mTitle = mTitleFormatter.format(mCalendar.getTime());
270 }
271 return mTitle;
Alan Viverettec5b95c22015-01-07 13:57:12 -0800272 }
273
274 /**
275 * Sets up the text and style properties for painting.
276 */
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700277 private void initPaints(Resources res) {
278 final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
279 final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
280 final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
281
282 final int monthTextSize = res.getDimensionPixelSize(
283 R.dimen.date_picker_month_text_size);
284 final int dayOfWeekTextSize = res.getDimensionPixelSize(
285 R.dimen.date_picker_day_of_week_text_size);
286 final int dayTextSize = res.getDimensionPixelSize(
287 R.dimen.date_picker_day_text_size);
288
Alan Viverettec5b95c22015-01-07 13:57:12 -0800289 mMonthPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700290 mMonthPaint.setTextSize(monthTextSize);
291 mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800292 mMonthPaint.setTextAlign(Align.CENTER);
293 mMonthPaint.setStyle(Style.FILL);
294
295 mDayOfWeekPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700296 mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
297 mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800298 mDayOfWeekPaint.setTextAlign(Align.CENTER);
299 mDayOfWeekPaint.setStyle(Style.FILL);
300
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700301 mDaySelectorPaint.setAntiAlias(true);
302 mDaySelectorPaint.setStyle(Style.FILL);
303
304 mDayHighlightPaint.setAntiAlias(true);
305 mDayHighlightPaint.setStyle(Style.FILL);
Alan Viverettec5b95c22015-01-07 13:57:12 -0800306
307 mDayPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700308 mDayPaint.setTextSize(dayTextSize);
309 mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800310 mDayPaint.setTextAlign(Align.CENTER);
311 mDayPaint.setStyle(Style.FILL);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700312 }
313
Alan Viverettec5b95c22015-01-07 13:57:12 -0800314 void setMonthTextColor(ColorStateList monthTextColor) {
315 final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
316 mMonthPaint.setColor(enabledColor);
317 invalidate();
318 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700319
Alan Viverettec5b95c22015-01-07 13:57:12 -0800320 void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
321 final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
322 mDayOfWeekPaint.setColor(enabledColor);
323 invalidate();
324 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700325
Alan Viverettec5b95c22015-01-07 13:57:12 -0800326 void setDayTextColor(ColorStateList dayTextColor) {
327 mDayTextColor = dayTextColor;
328 invalidate();
329 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700330
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700331 void setDaySelectorColor(ColorStateList dayBackgroundColor) {
Alan Viverette5dc973c2015-01-08 11:12:39 -0800332 final int activatedColor = dayBackgroundColor.getColorForState(
Alan Viverettec5b95c22015-01-07 13:57:12 -0800333 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700334 mDaySelectorPaint.setColor(activatedColor);
Alan Viverettec5b95c22015-01-07 13:57:12 -0800335 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700336 }
337
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700338 void setDayHighlightColor(ColorStateList dayHighlightColor) {
339 final int pressedColor = dayHighlightColor.getColorForState(
340 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
341 mDayHighlightPaint.setColor(pressedColor);
342 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700343 }
344
345 public void setOnDayClickListener(OnDayClickListener listener) {
346 mOnDayClickListener = listener;
347 }
348
349 @Override
350 public boolean dispatchHoverEvent(MotionEvent event) {
351 // First right-of-refusal goes the touch exploration helper.
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700352 return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700353 }
354
355 @Override
356 public boolean onTouchEvent(MotionEvent event) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700357 final int x = (int) (event.getX() + 0.5f);
358 final int y = (int) (event.getY() + 0.5f);
359
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700360 switch (event.getAction()) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700361 case MotionEvent.ACTION_DOWN:
362 case MotionEvent.ACTION_MOVE:
Alan Viverette60b674e2015-03-25 13:00:42 -0700363 final int touchedItem = getItemAtLocation(x, y);
364 if (mTouchedItem != touchedItem) {
365 mTouchedItem = touchedItem;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700366 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700367 }
368 break;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700369
370 case MotionEvent.ACTION_UP:
Alan Viverette60b674e2015-03-25 13:00:42 -0700371 final int clickedItem = getItemAtLocation(x, y);
372 onItemClicked(clickedItem, true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700373 // Fall through.
374 case MotionEvent.ACTION_CANCEL:
375 // Reset touched day on stream end.
Alan Viverette60b674e2015-03-25 13:00:42 -0700376 mTouchedItem = -1;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700377 invalidate();
378 break;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700379 }
380 return true;
381 }
382
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700383 @Override
384 protected void onDraw(Canvas canvas) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700385 final int paddingLeft = getPaddingLeft();
386 final int paddingTop = getPaddingTop();
387 canvas.translate(paddingLeft, paddingTop);
388
389 drawMonth(canvas);
390 drawDaysOfWeek(canvas);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700391 drawDays(canvas);
Alan Viverette60b674e2015-03-25 13:00:42 -0700392 drawButtons(canvas);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700393
394 canvas.translate(-paddingLeft, -paddingTop);
395 }
396
397 private void drawMonth(Canvas canvas) {
398 final float x = mPaddedWidth / 2f;
399
400 // Vertically centered within the month header height.
401 final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
402 final float y = (mMonthHeight - lineHeight) / 2f;
403
404 canvas.drawText(getTitle().toString(), x, y, mMonthPaint);
405 }
406
407 private void drawDaysOfWeek(Canvas canvas) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700408 final TextPaint p = mDayOfWeekPaint;
409 final int headerHeight = mMonthHeight;
410 final int rowHeight = mDayOfWeekHeight;
Alan Viverettef63757b2015-04-01 17:14:45 -0700411 final int colWidth = mCellWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700412
Alan Viverette60b674e2015-03-25 13:00:42 -0700413 // Text is vertically centered within the day of week height.
414 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
415 final int rowCenter = headerHeight + rowHeight / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700416
Alan Viverette60b674e2015-03-25 13:00:42 -0700417 for (int col = 0; col < DAYS_IN_WEEK; col++) {
418 final int colCenter = colWidth * col + colWidth / 2;
419 final int dayOfWeek = (col + mWeekStart) % DAYS_IN_WEEK;
420 final String label = getDayOfWeekLabel(dayOfWeek);
421 canvas.drawText(label, colCenter, rowCenter - halfLineHeight, p);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700422 }
423 }
424
Alan Viverette60b674e2015-03-25 13:00:42 -0700425 private String getDayOfWeekLabel(int dayOfWeek) {
426 mDayOfWeekLabelCalendar.set(Calendar.DAY_OF_WEEK, dayOfWeek);
427 return mDayOfWeekFormatter.format(mDayOfWeekLabelCalendar.getTime());
428 }
429
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700430 /**
431 * Draws the month days.
432 */
433 private void drawDays(Canvas canvas) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700434 final TextPaint p = mDayPaint;
435 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
436 final int rowHeight = mDayHeight;
Alan Viverettef63757b2015-04-01 17:14:45 -0700437 final int colWidth = mCellWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700438
Alan Viverette60b674e2015-03-25 13:00:42 -0700439 // Text is vertically centered within the row height.
440 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
441 int rowCenter = headerHeight + rowHeight / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700442
Alan Viverette60b674e2015-03-25 13:00:42 -0700443 for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) {
444 final int colCenter = colWidth * col + colWidth / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700445 int stateMask = 0;
446
447 if (day >= mEnabledDayStart && day <= mEnabledDayEnd) {
448 stateMask |= StateSet.VIEW_STATE_ENABLED;
449 }
450
451 final boolean isDayActivated = mActivatedDay == day;
452 if (isDayActivated) {
453 stateMask |= StateSet.VIEW_STATE_ACTIVATED;
454
455 // Adjust the circle to be centered on the row.
Alan Viverette60b674e2015-03-25 13:00:42 -0700456 canvas.drawCircle(colCenter, rowCenter, mDaySelectorRadius, mDaySelectorPaint);
457 } else if (mTouchedItem == day) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700458 stateMask |= StateSet.VIEW_STATE_PRESSED;
459
460 // Adjust the circle to be centered on the row.
Alan Viverette60b674e2015-03-25 13:00:42 -0700461 canvas.drawCircle(colCenter, rowCenter, mDaySelectorRadius, mDayHighlightPaint);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700462 }
463
464 final boolean isDayToday = mToday == day;
465 final int dayTextColor;
466 if (isDayToday && !isDayActivated) {
467 dayTextColor = mDaySelectorPaint.getColor();
468 } else {
469 final int[] stateSet = StateSet.get(stateMask);
470 dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
471 }
Alan Viverette60b674e2015-03-25 13:00:42 -0700472 p.setColor(dayTextColor);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700473
Alan Viverette60b674e2015-03-25 13:00:42 -0700474 canvas.drawText(Integer.toString(day), colCenter, rowCenter - halfLineHeight, p);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700475
Alan Viverette60b674e2015-03-25 13:00:42 -0700476 col++;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700477
Alan Viverette60b674e2015-03-25 13:00:42 -0700478 if (col == DAYS_IN_WEEK) {
479 col = 0;
480 rowCenter += rowHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700481 }
482 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700483 }
484
Alan Viverette60b674e2015-03-25 13:00:42 -0700485 private void drawButtons(Canvas canvas) {
486 if (mPrevEnabled && mPrevDrawable != null) {
487 mPrevDrawable.draw(canvas);
488 }
489
490 if (mNextEnabled && mNextDrawable != null) {
491 mNextDrawable.draw(canvas);
492 }
493 }
494
Alan Viverette518ff0d2014-08-15 14:20:35 -0700495 private static boolean isValidDayOfWeek(int day) {
Alan Viverette58780762014-09-10 17:09:13 -0700496 return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
497 }
498
499 private static boolean isValidMonth(int month) {
500 return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700501 }
502
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700503 /**
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700504 * Sets the selected day.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700505 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700506 * @param dayOfMonth the selected day of the month, or {@code -1} to clear
507 * the selection
508 */
509 public void setSelectedDay(int dayOfMonth) {
510 mActivatedDay = dayOfMonth;
511
512 // Invalidate cached accessibility information.
513 mTouchHelper.invalidateRoot();
514 invalidate();
515 }
516
517 /**
518 * Sets the first day of the week.
519 *
520 * @param weekStart which day the week should start on, valid values are
521 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
522 */
523 public void setFirstDayOfWeek(int weekStart) {
524 if (isValidDayOfWeek(weekStart)) {
525 mWeekStart = weekStart;
526 } else {
527 mWeekStart = mCalendar.getFirstDayOfWeek();
528 }
529
530 // Invalidate cached accessibility information.
531 mTouchHelper.invalidateRoot();
532 invalidate();
533 }
534
535 /**
536 * Sets all the parameters for displaying this week.
537 * <p>
538 * Parameters have a default value and will only update if a new value is
539 * included, except for focus month, which will always default to no focus
540 * month if no value is passed in. The only required parameter is the week
541 * start.
542 *
543 * @param selectedDay the selected day of the month, or -1 for no selection
544 * @param month the month
545 * @param year the year
546 * @param weekStart which day the week should start on, valid values are
547 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
548 * @param enabledDayStart the first enabled day
549 * @param enabledDayEnd the last enabled day
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700550 */
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700551 void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
552 int enabledDayEnd) {
Alan Viverettec5b95c22015-01-07 13:57:12 -0800553 mActivatedDay = selectedDay;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700554
Alan Viverette58780762014-09-10 17:09:13 -0700555 if (isValidMonth(month)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700556 mMonth = month;
557 }
558 mYear = year;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700559
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700560 mCalendar.set(Calendar.MONTH, mMonth);
561 mCalendar.set(Calendar.YEAR, mYear);
562 mCalendar.set(Calendar.DAY_OF_MONTH, 1);
563 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
564
Alan Viverette518ff0d2014-08-15 14:20:35 -0700565 if (isValidDayOfWeek(weekStart)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700566 mWeekStart = weekStart;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700567 } else {
568 mWeekStart = mCalendar.getFirstDayOfWeek();
569 }
570
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700571 if (enabledDayStart > 0 && enabledDayEnd < 32) {
572 mEnabledDayStart = enabledDayStart;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700573 }
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700574 if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) {
575 mEnabledDayEnd = enabledDayEnd;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700576 }
577
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700578 // Figure out what day today is.
579 final Calendar today = Calendar.getInstance();
580 mToday = -1;
581 mDaysInMonth = getDaysInMonth(mMonth, mYear);
582 for (int i = 0; i < mDaysInMonth; i++) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700583 final int day = i + 1;
584 if (sameDay(day, today)) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700585 mToday = day;
586 }
587 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700588
589 // Invalidate the old title.
590 mTitle = null;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700591
592 // Invalidate cached accessibility information.
593 mTouchHelper.invalidateRoot();
594 }
595
596 private static int getDaysInMonth(int month, int year) {
597 switch (month) {
598 case Calendar.JANUARY:
599 case Calendar.MARCH:
600 case Calendar.MAY:
601 case Calendar.JULY:
602 case Calendar.AUGUST:
603 case Calendar.OCTOBER:
604 case Calendar.DECEMBER:
605 return 31;
606 case Calendar.APRIL:
607 case Calendar.JUNE:
608 case Calendar.SEPTEMBER:
609 case Calendar.NOVEMBER:
610 return 30;
611 case Calendar.FEBRUARY:
612 return (year % 4 == 0) ? 29 : 28;
613 default:
614 throw new IllegalArgumentException("Invalid Month");
615 }
616 }
617
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700618 private boolean sameDay(int day, Calendar today) {
619 return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
620 && day == today.get(Calendar.DAY_OF_MONTH);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700621 }
622
623 @Override
624 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette816aa142015-04-10 15:41:10 -0700625 final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH
626 + mDesiredDayOfWeekHeight + mDesiredMonthHeight
627 + getPaddingTop() + getPaddingBottom();
Alan Viverettef63757b2015-04-01 17:14:45 -0700628 final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700629 + getPaddingStart() + getPaddingEnd();
630 final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
631 final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
632 setMeasuredDimension(resolvedWidth, resolvedHeight);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700633 }
634
635 @Override
Alan Viverettef63757b2015-04-01 17:14:45 -0700636 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
637 if (!changed) {
638 return;
639 }
Alan Viverette60b674e2015-03-25 13:00:42 -0700640
Alan Viverettef63757b2015-04-01 17:14:45 -0700641 // Let's initialize a completely reasonable number of variables.
642 final int w = right - left;
643 final int h = bottom - top;
644 final int paddingLeft = getPaddingLeft();
645 final int paddingTop = getPaddingTop();
646 final int paddingRight = getPaddingRight();
647 final int paddingBottom = getPaddingBottom();
648 final int paddedRight = w - paddingRight;
649 final int paddedBottom = h - paddingBottom;
650 final int paddedWidth = paddedRight - paddingLeft;
651 final int paddedHeight = paddedBottom - paddingTop;
652 if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) {
653 return;
654 }
655
656 mPaddedWidth = paddedWidth;
657 mPaddedHeight = paddedHeight;
658
659 // We may have been laid out smaller than our preferred size. If so,
660 // scale all dimensions to fit.
661 final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom;
662 final float scaleH = paddedHeight / (float) measuredPaddedHeight;
663 final int monthHeight = (int) (mDesiredMonthHeight * scaleH);
Alan Viverette60b674e2015-03-25 13:00:42 -0700664 final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
Alan Viverettef63757b2015-04-01 17:14:45 -0700665 mMonthHeight = monthHeight;
666 mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH);
667 mDayHeight = (int) (mDesiredDayHeight * scaleH);
668 mCellWidth = cellWidth;
669
670 // Compute the largest day selector radius that's still within the clip
671 // bounds and desired selector radius.
672 final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
673 final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
674 mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
675 Math.min(maxSelectorWidth, maxSelectorHeight));
Alan Viverette60b674e2015-03-25 13:00:42 -0700676
677 // Vertically center the previous/next drawables within the month
678 // header, horizontally center within the day cell, then expand the
679 // hit area to ensure it's at least 48x48dp.
680 final Drawable prevDrawable = mPrevDrawable;
681 if (prevDrawable != null) {
682 final int dW = prevDrawable.getIntrinsicWidth();
683 final int dH = prevDrawable.getIntrinsicHeight();
684 final int iconTop = (monthHeight - dH) / 2;
685 final int iconLeft = (cellWidth - dW) / 2;
686
687 // Button bounds don't include padding, but hit area does.
688 prevDrawable.setBounds(iconLeft, iconTop, iconLeft + dW, iconTop + dH);
Alan Viverettef63757b2015-04-01 17:14:45 -0700689 mPrevHitArea.set(0, 0, paddingLeft + cellWidth, paddingTop + monthHeight);
Alan Viverette60b674e2015-03-25 13:00:42 -0700690 }
691
692 final Drawable nextDrawable = mNextDrawable;
693 if (nextDrawable != null) {
694 final int dW = nextDrawable.getIntrinsicWidth();
695 final int dH = nextDrawable.getIntrinsicHeight();
696 final int iconTop = (monthHeight - dH) / 2;
Alan Viverettef63757b2015-04-01 17:14:45 -0700697 final int iconRight = paddedWidth - (cellWidth - dW) / 2;
Alan Viverette60b674e2015-03-25 13:00:42 -0700698
699 // Button bounds don't include padding, but hit area does.
700 nextDrawable.setBounds(iconRight - dW, iconTop, iconRight, iconTop + dH);
Alan Viverettef63757b2015-04-01 17:14:45 -0700701 mNextHitArea.set(paddedRight - cellWidth, 0, w, paddingTop + monthHeight);
Alan Viverette60b674e2015-03-25 13:00:42 -0700702 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700703
704 // Invalidate cached accessibility information.
705 mTouchHelper.invalidateRoot();
706 }
707
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700708 private int findDayOffset() {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700709 final int offset = mDayOfWeekStart - mWeekStart;
710 if (mDayOfWeekStart < mWeekStart) {
711 return offset + DAYS_IN_WEEK;
712 }
713 return offset;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700714 }
715
716 /**
Alan Viverette60b674e2015-03-25 13:00:42 -0700717 * Calculates the day of the month or item identifier at the specified
718 * touch position. Returns the day of the month or -1 if the position
719 * wasn't in a valid day.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700720 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700721 * @param x the x position of the touch event
722 * @param y the y position of the touch event
Alan Viverette60b674e2015-03-25 13:00:42 -0700723 * @return the day of the month at (x, y), an item identifier, or -1 if the
724 * position wasn't in a valid day or item
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700725 */
Alan Viverette60b674e2015-03-25 13:00:42 -0700726 private int getItemAtLocation(int x, int y) {
727 if (mNextEnabled && mNextDrawable != null && mNextHitArea.contains(x, y)) {
728 return ITEM_ID_NEXT;
729 } else if (mPrevEnabled && mPrevDrawable != null && mPrevHitArea.contains(x, y)) {
730 return ITEM_ID_PREV;
731 }
732
733 final int paddedX = x - getPaddingLeft();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700734 if (paddedX < 0 || paddedX >= mPaddedWidth) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700735 return -1;
736 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700737
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700738 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
Alan Viverette60b674e2015-03-25 13:00:42 -0700739 final int paddedY = y - getPaddingTop();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700740 if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700741 return -1;
742 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700743
744 final int row = (paddedY - headerHeight) / mDayHeight;
745 final int col = (paddedX * DAYS_IN_WEEK) / mPaddedWidth;
746 final int index = col + row * DAYS_IN_WEEK;
747 final int day = index + 1 - findDayOffset();
748 if (day < 1 || day > mDaysInMonth) {
749 return -1;
750 }
751
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700752 return day;
753 }
754
755 /**
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700756 * Calculates the bounds of the specified day.
757 *
Alan Viverette60b674e2015-03-25 13:00:42 -0700758 * @param id the day of the month, or an item identifier
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700759 * @param outBounds the rect to populate with bounds
760 */
Alan Viverette60b674e2015-03-25 13:00:42 -0700761 private boolean getBoundsForItem(int id, Rect outBounds) {
762 if (mNextEnabled && id == ITEM_ID_NEXT) {
763 if (mNextDrawable != null) {
764 outBounds.set(mNextHitArea);
765 return true;
766 }
767 } else if (mPrevEnabled && id == ITEM_ID_PREV) {
768 if (mPrevDrawable != null) {
769 outBounds.set(mPrevHitArea);
770 return true;
771 }
772 }
773
774 if (id < 1 || id > mDaysInMonth) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700775 return false;
776 }
777
Alan Viverette60b674e2015-03-25 13:00:42 -0700778 final int index = id - 1 + findDayOffset();
779
780 // Compute left edge.
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700781 final int col = index % DAYS_IN_WEEK;
Alan Viverettef63757b2015-04-01 17:14:45 -0700782 final int colWidth = mCellWidth;
Alan Viverette60b674e2015-03-25 13:00:42 -0700783 final int left = getPaddingLeft() + col * colWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700784
Alan Viverette60b674e2015-03-25 13:00:42 -0700785 // Compute top edge.
786 final int row = index / DAYS_IN_WEEK;
787 final int rowHeight = mDayHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700788 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
Alan Viverette60b674e2015-03-25 13:00:42 -0700789 final int top = getPaddingTop() + headerHeight + row * rowHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700790
Alan Viverette60b674e2015-03-25 13:00:42 -0700791 outBounds.set(left, top, left + colWidth, top + rowHeight);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700792 return true;
793 }
794
795 /**
Alan Viverette60b674e2015-03-25 13:00:42 -0700796 * Called when an item is clicked.
797 *
798 * @param id the day number or item identifier
799 */
800 private boolean onItemClicked(int id, boolean animate) {
801 return onNavigationClicked(id, animate) || onDayClicked(id);
802 }
803
804 /**
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700805 * Called when the user clicks on a day. Handles callbacks to the
806 * {@link OnDayClickListener} if one is set.
807 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700808 * @param day the day that was clicked
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700809 */
Alan Viverette60b674e2015-03-25 13:00:42 -0700810 private boolean onDayClicked(int day) {
811 if (day < 0 || day > mDaysInMonth) {
812 return false;
813 }
814
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700815 if (mOnDayClickListener != null) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700816 final Calendar date = Calendar.getInstance();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700817 date.set(mYear, mMonth, day);
818 mOnDayClickListener.onDayClick(this, date);
819 }
820
821 // This is a no-op if accessibility is turned off.
822 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
Alan Viverette60b674e2015-03-25 13:00:42 -0700823 return true;
824 }
825
826 /**
827 * Called when the user clicks on a navigation button. Handles callbacks to
828 * the {@link OnDayClickListener} if one is set.
829 *
830 * @param id the item identifier
831 */
832 private boolean onNavigationClicked(int id, boolean animate) {
833 final int direction;
834 if (id == ITEM_ID_NEXT) {
835 direction = 1;
836 } else if (id == ITEM_ID_PREV) {
837 direction = -1;
838 } else {
839 return false;
840 }
841
842 if (mOnDayClickListener != null) {
843 mOnDayClickListener.onNavigationClick(this, direction, animate);
844 }
845
846 // This is a no-op if accessibility is turned off.
847 mTouchHelper.sendEventForVirtualView(id, AccessibilityEvent.TYPE_VIEW_CLICKED);
848 return true;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700849 }
850
851 /**
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700852 * Provides a virtual view hierarchy for interfacing with an accessibility
853 * service.
854 */
855 private class MonthViewTouchHelper extends ExploreByTouchHelper {
856 private static final String DATE_FORMAT = "dd MMMM yyyy";
857
858 private final Rect mTempRect = new Rect();
859 private final Calendar mTempCalendar = Calendar.getInstance();
860
861 public MonthViewTouchHelper(View host) {
862 super(host);
863 }
864
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700865 @Override
866 protected int getVirtualViewAt(float x, float y) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700867 final int day = getItemAtLocation((int) (x + 0.5f), (int) (y + 0.5f));
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700868 if (day >= 0) {
869 return day;
870 }
871 return ExploreByTouchHelper.INVALID_ID;
872 }
873
874 @Override
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700875 protected void getVisibleVirtualViews(IntArray virtualViewIds) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700876 if (mNextEnabled && mNextDrawable != null) {
877 virtualViewIds.add(ITEM_ID_PREV);
878 }
879
880 if (mPrevEnabled && mPrevDrawable != null) {
881 virtualViewIds.add(ITEM_ID_NEXT);
882 }
883
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700884 for (int day = 1; day <= mDaysInMonth; day++) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700885 virtualViewIds.add(day);
886 }
887 }
888
889 @Override
890 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
891 event.setContentDescription(getItemDescription(virtualViewId));
892 }
893
894 @Override
895 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700896 final boolean hasBounds = getBoundsForItem(virtualViewId, mTempRect);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700897
898 if (!hasBounds) {
899 // The day is invalid, kill the node.
900 mTempRect.setEmpty();
901 node.setContentDescription("");
902 node.setBoundsInParent(mTempRect);
903 node.setVisibleToUser(false);
904 return;
905 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700906
Alan Viverette60b674e2015-03-25 13:00:42 -0700907 node.setText(getItemText(virtualViewId));
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700908 node.setContentDescription(getItemDescription(virtualViewId));
909 node.setBoundsInParent(mTempRect);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700910 node.addAction(AccessibilityAction.ACTION_CLICK);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700911
Alan Viverettec5b95c22015-01-07 13:57:12 -0800912 if (virtualViewId == mActivatedDay) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700913 // TODO: This should use activated once that's supported.
914 node.setChecked(true);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700915 }
916
917 }
918
919 @Override
920 protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
921 Bundle arguments) {
922 switch (action) {
923 case AccessibilityNodeInfo.ACTION_CLICK:
Alan Viverette60b674e2015-03-25 13:00:42 -0700924 return onItemClicked(virtualViewId, false);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700925 }
926
927 return false;
928 }
929
930 /**
Alan Viverette60b674e2015-03-25 13:00:42 -0700931 * Generates a description for a given virtual view.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700932 *
Alan Viverette60b674e2015-03-25 13:00:42 -0700933 * @param id the day or item identifier to generate a description for
934 * @return a description of the virtual view
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700935 */
Alan Viverette60b674e2015-03-25 13:00:42 -0700936 private CharSequence getItemDescription(int id) {
937 if (id == ITEM_ID_NEXT) {
938 return mNextContentDesc;
939 } else if (id == ITEM_ID_PREV) {
940 return mPrevContentDesc;
941 } else if (id >= 1 && id <= mDaysInMonth) {
942 mTempCalendar.set(mYear, mMonth, id);
943 return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700944 }
945
Alan Viverette60b674e2015-03-25 13:00:42 -0700946 return "";
947 }
948
949 /**
950 * Generates displayed text for a given virtual view.
951 *
952 * @param id the day or item identifier to generate text for
953 * @return the visible text of the virtual view
954 */
955 private CharSequence getItemText(int id) {
956 if (id == ITEM_ID_NEXT || id == ITEM_ID_PREV) {
957 return null;
958 } else if (id >= 1 && id <= mDaysInMonth) {
959 return Integer.toString(id);
960 }
961
962 return null;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700963 }
964 }
965
966 /**
967 * Handles callbacks when the user clicks on a time object.
968 */
969 public interface OnDayClickListener {
970 public void onDayClick(SimpleMonthView view, Calendar day);
Alan Viverette60b674e2015-03-25 13:00:42 -0700971 public void onNavigationClick(SimpleMonthView view, int direction, boolean animate);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700972 }
973}