blob: 0249c222d9693c51f44733f204bb61d99336ce6c [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;
29import android.os.Bundle;
Alan Viverette5dc973c2015-01-08 11:12:39 -080030import android.text.TextPaint;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070031import android.text.format.DateFormat;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070032import android.util.AttributeSet;
Alan Viveretteffb46bf2014-10-24 12:06:11 -070033import android.util.IntArray;
Alan Viverettec5b95c22015-01-07 13:57:12 -080034import android.util.StateSet;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070035import android.view.MotionEvent;
36import android.view.View;
37import android.view.accessibility.AccessibilityEvent;
38import android.view.accessibility.AccessibilityNodeInfo;
Alan Viverette0ef59ac2015-03-23 13:13:25 -070039import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070040
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;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070046import java.util.Locale;
47
48/**
49 * A calendar-like view displaying a specified month and the appropriate selectable day numbers
50 * within the specified month.
51 */
52class SimpleMonthView extends View {
Alan Viverette0ef59ac2015-03-23 13:13:25 -070053 private static final int DAYS_IN_WEEK = 7;
54 private static final int MAX_WEEKS_IN_MONTH = 6;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070055
56 private static final int DEFAULT_SELECTED_DAY = -1;
57 private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070058
Alan Viverette0ef59ac2015-03-23 13:13:25 -070059 private static final String DEFAULT_TITLE_FORMAT = "MMMMy";
60 private static final String DAY_OF_WEEK_FORMAT = "EEEEE";
Alan Viverettec5b95c22015-01-07 13:57:12 -080061
Alan Viverette5dc973c2015-01-08 11:12:39 -080062 private final TextPaint mMonthPaint = new TextPaint();
63 private final TextPaint mDayOfWeekPaint = new TextPaint();
64 private final TextPaint mDayPaint = new TextPaint();
Alan Viverette0ef59ac2015-03-23 13:13:25 -070065 private final Paint mDaySelectorPaint = new Paint();
66 private final Paint mDayHighlightPaint = new Paint();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070067
68 private final Calendar mCalendar = Calendar.getInstance();
Alan Viverette60b674e2015-03-25 13:00:42 -070069 private final Calendar mDayOfWeekLabelCalendar = Calendar.getInstance();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070070
71 private final MonthViewTouchHelper mTouchHelper;
72
Alan Viverette0ef59ac2015-03-23 13:13:25 -070073 private final SimpleDateFormat mTitleFormatter;
74 private final SimpleDateFormat mDayOfWeekFormatter;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070075
Alan Viverettef63757b2015-04-01 17:14:45 -070076 // Desired dimensions.
77 private final int mDesiredMonthHeight;
78 private final int mDesiredDayOfWeekHeight;
79 private final int mDesiredDayHeight;
80 private final int mDesiredCellWidth;
81 private final int mDesiredDaySelectorRadius;
Alan Viverette60b674e2015-03-25 13:00:42 -070082
Alan Viverette0ef59ac2015-03-23 13:13:25 -070083 private CharSequence mTitle;
84
85 private int mMonth;
86 private int mYear;
87
Alan Viverettef63757b2015-04-01 17:14:45 -070088 // Dimensions as laid out.
89 private int mMonthHeight;
90 private int mDayOfWeekHeight;
91 private int mDayHeight;
92 private int mCellWidth;
93 private int mDaySelectorRadius;
94
Alan Viverette0ef59ac2015-03-23 13:13:25 -070095 private int mPaddedWidth;
96 private int mPaddedHeight;
97
Alan Viverette0ef59ac2015-03-23 13:13:25 -070098 /** The day of month for the selected day, or -1 if no day is selected. */
99 private int mActivatedDay = -1;
100
101 /**
102 * The day of month for today, or -1 if the today is not in the current
103 * month.
104 */
105 private int mToday = DEFAULT_SELECTED_DAY;
106
107 /** The first day of the week (ex. Calendar.SUNDAY). */
108 private int mWeekStart = DEFAULT_WEEK_START;
109
110 /** The number of days (ex. 28) in the current month. */
111 private int mDaysInMonth;
112
113 /**
114 * The day of week (ex. Calendar.SUNDAY) for the first day of the current
115 * month.
116 */
117 private int mDayOfWeekStart;
118
119 /** The day of month for the first (inclusive) enabled day. */
120 private int mEnabledDayStart = 1;
121
122 /** The day of month for the last (inclusive) enabled day. */
123 private int mEnabledDayEnd = 31;
124
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700125 /** Optional listener for handling day click actions. */
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700126 private OnDayClickListener mOnDayClickListener;
127
Alan Viverettec5b95c22015-01-07 13:57:12 -0800128 private ColorStateList mDayTextColor;
129
Alan Viverette60b674e2015-03-25 13:00:42 -0700130 private int mTouchedItem = -1;
131
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700132 public SimpleMonthView(Context context) {
133 this(context, null);
134 }
135
136 public SimpleMonthView(Context context, AttributeSet attrs) {
137 this(context, attrs, R.attr.datePickerStyle);
138 }
139
Alan Viverette50eb0252014-10-24 14:34:26 -0700140 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
141 this(context, attrs, defStyleAttr, 0);
142 }
143
144 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
145 super(context, attrs, defStyleAttr, defStyleRes);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700146
147 final Resources res = context.getResources();
Alan Viverettef63757b2015-04-01 17:14:45 -0700148 mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
149 mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
150 mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
151 mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
Alan Viverette78bf1d32015-04-17 10:39:22 -0700152 mDesiredDaySelectorRadius = res.getDimensionPixelSize(
153 R.dimen.date_picker_day_selector_radius);
Alan Viverette60b674e2015-03-25 13:00:42 -0700154
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700155 // Set up accessibility components.
156 mTouchHelper = new MonthViewTouchHelper(this);
157 setAccessibilityDelegate(mTouchHelper);
158 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700159
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700160 final Locale locale = res.getConfiguration().locale;
161 final String titleFormat = DateFormat.getBestDateTimePattern(locale, DEFAULT_TITLE_FORMAT);
162 mTitleFormatter = new SimpleDateFormat(titleFormat, locale);
163 mDayOfWeekFormatter = new SimpleDateFormat(DAY_OF_WEEK_FORMAT, locale);
164
165 setClickable(true);
166 initPaints(res);
167 }
168
169 /**
170 * Applies the specified text appearance resource to a paint, returning the
171 * text color if one is set in the text appearance.
172 *
173 * @param p the paint to modify
174 * @param resId the resource ID of the text appearance
175 * @return the text color, if available
176 */
177 private ColorStateList applyTextAppearance(Paint p, int resId) {
178 final TypedArray ta = mContext.obtainStyledAttributes(null,
179 R.styleable.TextAppearance, 0, resId);
180
181 final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
182 if (fontFamily != null) {
183 p.setTypeface(Typeface.create(fontFamily, 0));
184 }
185
186 p.setTextSize(ta.getDimensionPixelSize(
187 R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
188
189 final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
190 if (textColor != null) {
191 final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
192 p.setColor(enabledColor);
193 }
194
195 ta.recycle();
196
197 return textColor;
198 }
199
Alan Viverette78bf1d32015-04-17 10:39:22 -0700200 public int getMonthHeight() {
201 return mMonthHeight;
202 }
203
204 public int getCellWidth() {
205 return mCellWidth;
206 }
207
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700208 public void setMonthTextAppearance(int resId) {
Alan Viverette78bf1d32015-04-17 10:39:22 -0700209 applyTextAppearance(mMonthPaint, resId);
Alan Viverette60b674e2015-03-25 13:00:42 -0700210
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700211 invalidate();
212 }
213
214 public void setDayOfWeekTextAppearance(int resId) {
215 applyTextAppearance(mDayOfWeekPaint, resId);
216 invalidate();
217 }
218
219 public void setDayTextAppearance(int resId) {
220 final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
221 if (textColor != null) {
222 mDayTextColor = textColor;
223 }
224
225 invalidate();
226 }
227
228 public CharSequence getTitle() {
229 if (mTitle == null) {
230 mTitle = mTitleFormatter.format(mCalendar.getTime());
231 }
232 return mTitle;
Alan Viverettec5b95c22015-01-07 13:57:12 -0800233 }
234
235 /**
236 * Sets up the text and style properties for painting.
237 */
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700238 private void initPaints(Resources res) {
239 final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
240 final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
241 final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
242
243 final int monthTextSize = res.getDimensionPixelSize(
244 R.dimen.date_picker_month_text_size);
245 final int dayOfWeekTextSize = res.getDimensionPixelSize(
246 R.dimen.date_picker_day_of_week_text_size);
247 final int dayTextSize = res.getDimensionPixelSize(
248 R.dimen.date_picker_day_text_size);
249
Alan Viverettec5b95c22015-01-07 13:57:12 -0800250 mMonthPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700251 mMonthPaint.setTextSize(monthTextSize);
252 mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800253 mMonthPaint.setTextAlign(Align.CENTER);
254 mMonthPaint.setStyle(Style.FILL);
255
256 mDayOfWeekPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700257 mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
258 mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800259 mDayOfWeekPaint.setTextAlign(Align.CENTER);
260 mDayOfWeekPaint.setStyle(Style.FILL);
261
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700262 mDaySelectorPaint.setAntiAlias(true);
263 mDaySelectorPaint.setStyle(Style.FILL);
264
265 mDayHighlightPaint.setAntiAlias(true);
266 mDayHighlightPaint.setStyle(Style.FILL);
Alan Viverettec5b95c22015-01-07 13:57:12 -0800267
268 mDayPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700269 mDayPaint.setTextSize(dayTextSize);
270 mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800271 mDayPaint.setTextAlign(Align.CENTER);
272 mDayPaint.setStyle(Style.FILL);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700273 }
274
Alan Viverettec5b95c22015-01-07 13:57:12 -0800275 void setMonthTextColor(ColorStateList monthTextColor) {
276 final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
277 mMonthPaint.setColor(enabledColor);
278 invalidate();
279 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700280
Alan Viverettec5b95c22015-01-07 13:57:12 -0800281 void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
282 final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
283 mDayOfWeekPaint.setColor(enabledColor);
284 invalidate();
285 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700286
Alan Viverettec5b95c22015-01-07 13:57:12 -0800287 void setDayTextColor(ColorStateList dayTextColor) {
288 mDayTextColor = dayTextColor;
289 invalidate();
290 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700291
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700292 void setDaySelectorColor(ColorStateList dayBackgroundColor) {
Alan Viverette5dc973c2015-01-08 11:12:39 -0800293 final int activatedColor = dayBackgroundColor.getColorForState(
Alan Viverettec5b95c22015-01-07 13:57:12 -0800294 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700295 mDaySelectorPaint.setColor(activatedColor);
Alan Viverettec5b95c22015-01-07 13:57:12 -0800296 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700297 }
298
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700299 void setDayHighlightColor(ColorStateList dayHighlightColor) {
300 final int pressedColor = dayHighlightColor.getColorForState(
301 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
302 mDayHighlightPaint.setColor(pressedColor);
303 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700304 }
305
306 public void setOnDayClickListener(OnDayClickListener listener) {
307 mOnDayClickListener = listener;
308 }
309
310 @Override
311 public boolean dispatchHoverEvent(MotionEvent event) {
312 // First right-of-refusal goes the touch exploration helper.
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700313 return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700314 }
315
316 @Override
317 public boolean onTouchEvent(MotionEvent event) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700318 final int x = (int) (event.getX() + 0.5f);
319 final int y = (int) (event.getY() + 0.5f);
320
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700321 switch (event.getAction()) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700322 case MotionEvent.ACTION_DOWN:
323 case MotionEvent.ACTION_MOVE:
Alan Viverette78bf1d32015-04-17 10:39:22 -0700324 final int touchedItem = getDayAtLocation(x, y);
Alan Viverette60b674e2015-03-25 13:00:42 -0700325 if (mTouchedItem != touchedItem) {
326 mTouchedItem = touchedItem;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700327 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700328 }
329 break;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700330
331 case MotionEvent.ACTION_UP:
Alan Viverette78bf1d32015-04-17 10:39:22 -0700332 final int clickedDay = getDayAtLocation(x, y);
333 onDayClicked(clickedDay);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700334 // Fall through.
335 case MotionEvent.ACTION_CANCEL:
336 // Reset touched day on stream end.
Alan Viverette60b674e2015-03-25 13:00:42 -0700337 mTouchedItem = -1;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700338 invalidate();
339 break;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700340 }
341 return true;
342 }
343
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700344 @Override
345 protected void onDraw(Canvas canvas) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700346 final int paddingLeft = getPaddingLeft();
347 final int paddingTop = getPaddingTop();
348 canvas.translate(paddingLeft, paddingTop);
349
350 drawMonth(canvas);
351 drawDaysOfWeek(canvas);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700352 drawDays(canvas);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700353
354 canvas.translate(-paddingLeft, -paddingTop);
355 }
356
357 private void drawMonth(Canvas canvas) {
358 final float x = mPaddedWidth / 2f;
359
360 // Vertically centered within the month header height.
361 final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
362 final float y = (mMonthHeight - lineHeight) / 2f;
363
364 canvas.drawText(getTitle().toString(), x, y, mMonthPaint);
365 }
366
367 private void drawDaysOfWeek(Canvas canvas) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700368 final TextPaint p = mDayOfWeekPaint;
369 final int headerHeight = mMonthHeight;
370 final int rowHeight = mDayOfWeekHeight;
Alan Viverettef63757b2015-04-01 17:14:45 -0700371 final int colWidth = mCellWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700372
Alan Viverette60b674e2015-03-25 13:00:42 -0700373 // Text is vertically centered within the day of week height.
374 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
375 final int rowCenter = headerHeight + rowHeight / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700376
Alan Viverette60b674e2015-03-25 13:00:42 -0700377 for (int col = 0; col < DAYS_IN_WEEK; col++) {
378 final int colCenter = colWidth * col + colWidth / 2;
379 final int dayOfWeek = (col + mWeekStart) % DAYS_IN_WEEK;
380 final String label = getDayOfWeekLabel(dayOfWeek);
381 canvas.drawText(label, colCenter, rowCenter - halfLineHeight, p);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700382 }
383 }
384
Alan Viverette60b674e2015-03-25 13:00:42 -0700385 private String getDayOfWeekLabel(int dayOfWeek) {
386 mDayOfWeekLabelCalendar.set(Calendar.DAY_OF_WEEK, dayOfWeek);
387 return mDayOfWeekFormatter.format(mDayOfWeekLabelCalendar.getTime());
388 }
389
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700390 /**
391 * Draws the month days.
392 */
393 private void drawDays(Canvas canvas) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700394 final TextPaint p = mDayPaint;
395 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
396 final int rowHeight = mDayHeight;
Alan Viverettef63757b2015-04-01 17:14:45 -0700397 final int colWidth = mCellWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700398
Alan Viverette60b674e2015-03-25 13:00:42 -0700399 // Text is vertically centered within the row height.
400 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
401 int rowCenter = headerHeight + rowHeight / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700402
Alan Viverette60b674e2015-03-25 13:00:42 -0700403 for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) {
404 final int colCenter = colWidth * col + colWidth / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700405 int stateMask = 0;
406
407 if (day >= mEnabledDayStart && day <= mEnabledDayEnd) {
408 stateMask |= StateSet.VIEW_STATE_ENABLED;
409 }
410
411 final boolean isDayActivated = mActivatedDay == day;
412 if (isDayActivated) {
413 stateMask |= StateSet.VIEW_STATE_ACTIVATED;
414
415 // Adjust the circle to be centered on the row.
Alan Viverette60b674e2015-03-25 13:00:42 -0700416 canvas.drawCircle(colCenter, rowCenter, mDaySelectorRadius, mDaySelectorPaint);
417 } else if (mTouchedItem == day) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700418 stateMask |= StateSet.VIEW_STATE_PRESSED;
419
420 // Adjust the circle to be centered on the row.
Alan Viverette60b674e2015-03-25 13:00:42 -0700421 canvas.drawCircle(colCenter, rowCenter, mDaySelectorRadius, mDayHighlightPaint);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700422 }
423
424 final boolean isDayToday = mToday == day;
425 final int dayTextColor;
426 if (isDayToday && !isDayActivated) {
427 dayTextColor = mDaySelectorPaint.getColor();
428 } else {
429 final int[] stateSet = StateSet.get(stateMask);
430 dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
431 }
Alan Viverette60b674e2015-03-25 13:00:42 -0700432 p.setColor(dayTextColor);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700433
Alan Viverette60b674e2015-03-25 13:00:42 -0700434 canvas.drawText(Integer.toString(day), colCenter, rowCenter - halfLineHeight, p);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700435
Alan Viverette60b674e2015-03-25 13:00:42 -0700436 col++;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700437
Alan Viverette60b674e2015-03-25 13:00:42 -0700438 if (col == DAYS_IN_WEEK) {
439 col = 0;
440 rowCenter += rowHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700441 }
442 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700443 }
444
Alan Viverette518ff0d2014-08-15 14:20:35 -0700445 private static boolean isValidDayOfWeek(int day) {
Alan Viverette58780762014-09-10 17:09:13 -0700446 return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
447 }
448
449 private static boolean isValidMonth(int month) {
450 return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700451 }
452
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700453 /**
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700454 * Sets the selected day.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700455 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700456 * @param dayOfMonth the selected day of the month, or {@code -1} to clear
457 * the selection
458 */
459 public void setSelectedDay(int dayOfMonth) {
460 mActivatedDay = dayOfMonth;
461
462 // Invalidate cached accessibility information.
463 mTouchHelper.invalidateRoot();
464 invalidate();
465 }
466
467 /**
468 * Sets the first day of the week.
469 *
470 * @param weekStart which day the week should start on, valid values are
471 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
472 */
473 public void setFirstDayOfWeek(int weekStart) {
474 if (isValidDayOfWeek(weekStart)) {
475 mWeekStart = weekStart;
476 } else {
477 mWeekStart = mCalendar.getFirstDayOfWeek();
478 }
479
480 // Invalidate cached accessibility information.
481 mTouchHelper.invalidateRoot();
482 invalidate();
483 }
484
485 /**
486 * Sets all the parameters for displaying this week.
487 * <p>
488 * Parameters have a default value and will only update if a new value is
489 * included, except for focus month, which will always default to no focus
490 * month if no value is passed in. The only required parameter is the week
491 * start.
492 *
493 * @param selectedDay the selected day of the month, or -1 for no selection
494 * @param month the month
495 * @param year the year
496 * @param weekStart which day the week should start on, valid values are
497 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
498 * @param enabledDayStart the first enabled day
499 * @param enabledDayEnd the last enabled day
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700500 */
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700501 void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
502 int enabledDayEnd) {
Alan Viverettec5b95c22015-01-07 13:57:12 -0800503 mActivatedDay = selectedDay;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700504
Alan Viverette58780762014-09-10 17:09:13 -0700505 if (isValidMonth(month)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700506 mMonth = month;
507 }
508 mYear = year;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700509
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700510 mCalendar.set(Calendar.MONTH, mMonth);
511 mCalendar.set(Calendar.YEAR, mYear);
512 mCalendar.set(Calendar.DAY_OF_MONTH, 1);
513 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
514
Alan Viverette518ff0d2014-08-15 14:20:35 -0700515 if (isValidDayOfWeek(weekStart)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700516 mWeekStart = weekStart;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700517 } else {
518 mWeekStart = mCalendar.getFirstDayOfWeek();
519 }
520
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700521 if (enabledDayStart > 0 && enabledDayEnd < 32) {
522 mEnabledDayStart = enabledDayStart;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700523 }
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700524 if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) {
525 mEnabledDayEnd = enabledDayEnd;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700526 }
527
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700528 // Figure out what day today is.
529 final Calendar today = Calendar.getInstance();
530 mToday = -1;
531 mDaysInMonth = getDaysInMonth(mMonth, mYear);
532 for (int i = 0; i < mDaysInMonth; i++) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700533 final int day = i + 1;
534 if (sameDay(day, today)) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700535 mToday = day;
536 }
537 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700538
539 // Invalidate the old title.
540 mTitle = null;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700541
542 // Invalidate cached accessibility information.
543 mTouchHelper.invalidateRoot();
544 }
545
546 private static int getDaysInMonth(int month, int year) {
547 switch (month) {
548 case Calendar.JANUARY:
549 case Calendar.MARCH:
550 case Calendar.MAY:
551 case Calendar.JULY:
552 case Calendar.AUGUST:
553 case Calendar.OCTOBER:
554 case Calendar.DECEMBER:
555 return 31;
556 case Calendar.APRIL:
557 case Calendar.JUNE:
558 case Calendar.SEPTEMBER:
559 case Calendar.NOVEMBER:
560 return 30;
561 case Calendar.FEBRUARY:
562 return (year % 4 == 0) ? 29 : 28;
563 default:
564 throw new IllegalArgumentException("Invalid Month");
565 }
566 }
567
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700568 private boolean sameDay(int day, Calendar today) {
569 return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
570 && day == today.get(Calendar.DAY_OF_MONTH);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700571 }
572
573 @Override
574 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette816aa142015-04-10 15:41:10 -0700575 final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH
576 + mDesiredDayOfWeekHeight + mDesiredMonthHeight
577 + getPaddingTop() + getPaddingBottom();
Alan Viverettef63757b2015-04-01 17:14:45 -0700578 final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700579 + getPaddingStart() + getPaddingEnd();
580 final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
581 final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
582 setMeasuredDimension(resolvedWidth, resolvedHeight);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700583 }
584
585 @Override
Alan Viverettef63757b2015-04-01 17:14:45 -0700586 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
587 if (!changed) {
588 return;
589 }
Alan Viverette60b674e2015-03-25 13:00:42 -0700590
Alan Viverettef63757b2015-04-01 17:14:45 -0700591 // Let's initialize a completely reasonable number of variables.
592 final int w = right - left;
593 final int h = bottom - top;
594 final int paddingLeft = getPaddingLeft();
595 final int paddingTop = getPaddingTop();
596 final int paddingRight = getPaddingRight();
597 final int paddingBottom = getPaddingBottom();
598 final int paddedRight = w - paddingRight;
599 final int paddedBottom = h - paddingBottom;
600 final int paddedWidth = paddedRight - paddingLeft;
601 final int paddedHeight = paddedBottom - paddingTop;
602 if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) {
603 return;
604 }
605
606 mPaddedWidth = paddedWidth;
607 mPaddedHeight = paddedHeight;
608
609 // We may have been laid out smaller than our preferred size. If so,
610 // scale all dimensions to fit.
611 final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom;
612 final float scaleH = paddedHeight / (float) measuredPaddedHeight;
613 final int monthHeight = (int) (mDesiredMonthHeight * scaleH);
Alan Viverette60b674e2015-03-25 13:00:42 -0700614 final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
Alan Viverettef63757b2015-04-01 17:14:45 -0700615 mMonthHeight = monthHeight;
616 mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH);
617 mDayHeight = (int) (mDesiredDayHeight * scaleH);
618 mCellWidth = cellWidth;
619
620 // Compute the largest day selector radius that's still within the clip
621 // bounds and desired selector radius.
622 final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
623 final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
624 mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
625 Math.min(maxSelectorWidth, maxSelectorHeight));
Alan Viverette60b674e2015-03-25 13:00:42 -0700626
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700627 // Invalidate cached accessibility information.
628 mTouchHelper.invalidateRoot();
629 }
630
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700631 private int findDayOffset() {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700632 final int offset = mDayOfWeekStart - mWeekStart;
633 if (mDayOfWeekStart < mWeekStart) {
634 return offset + DAYS_IN_WEEK;
635 }
636 return offset;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700637 }
638
639 /**
Alan Viverette78bf1d32015-04-17 10:39:22 -0700640 * Calculates the day of the month at the specified touch position. Returns
641 * the day of the month or -1 if the position wasn't in a valid day.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700642 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700643 * @param x the x position of the touch event
644 * @param y the y position of the touch event
Alan Viverette78bf1d32015-04-17 10:39:22 -0700645 * @return the day of the month at (x, y), or -1 if the position wasn't in
646 * a valid day
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700647 */
Alan Viverette78bf1d32015-04-17 10:39:22 -0700648 private int getDayAtLocation(int x, int y) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700649 final int paddedX = x - getPaddingLeft();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700650 if (paddedX < 0 || paddedX >= mPaddedWidth) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700651 return -1;
652 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700653
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700654 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
Alan Viverette60b674e2015-03-25 13:00:42 -0700655 final int paddedY = y - getPaddingTop();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700656 if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700657 return -1;
658 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700659
660 final int row = (paddedY - headerHeight) / mDayHeight;
661 final int col = (paddedX * DAYS_IN_WEEK) / mPaddedWidth;
662 final int index = col + row * DAYS_IN_WEEK;
663 final int day = index + 1 - findDayOffset();
664 if (day < 1 || day > mDaysInMonth) {
665 return -1;
666 }
667
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700668 return day;
669 }
670
671 /**
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700672 * Calculates the bounds of the specified day.
673 *
Alan Viverette78bf1d32015-04-17 10:39:22 -0700674 * @param id the day of the month
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700675 * @param outBounds the rect to populate with bounds
676 */
Alan Viverette78bf1d32015-04-17 10:39:22 -0700677 private boolean getBoundsForDay(int id, Rect outBounds) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700678 if (id < 1 || id > mDaysInMonth) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700679 return false;
680 }
681
Alan Viverette60b674e2015-03-25 13:00:42 -0700682 final int index = id - 1 + findDayOffset();
683
684 // Compute left edge.
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700685 final int col = index % DAYS_IN_WEEK;
Alan Viverettef63757b2015-04-01 17:14:45 -0700686 final int colWidth = mCellWidth;
Alan Viverette60b674e2015-03-25 13:00:42 -0700687 final int left = getPaddingLeft() + col * colWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700688
Alan Viverette60b674e2015-03-25 13:00:42 -0700689 // Compute top edge.
690 final int row = index / DAYS_IN_WEEK;
691 final int rowHeight = mDayHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700692 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
Alan Viverette60b674e2015-03-25 13:00:42 -0700693 final int top = getPaddingTop() + headerHeight + row * rowHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700694
Alan Viverette60b674e2015-03-25 13:00:42 -0700695 outBounds.set(left, top, left + colWidth, top + rowHeight);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700696
Alan Viverette78bf1d32015-04-17 10:39:22 -0700697 return true;
Alan Viverette60b674e2015-03-25 13:00:42 -0700698 }
699
700 /**
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700701 * Called when the user clicks on a day. Handles callbacks to the
702 * {@link OnDayClickListener} if one is set.
703 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700704 * @param day the day that was clicked
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700705 */
Alan Viverette60b674e2015-03-25 13:00:42 -0700706 private boolean onDayClicked(int day) {
707 if (day < 0 || day > mDaysInMonth) {
708 return false;
709 }
710
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700711 if (mOnDayClickListener != null) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700712 final Calendar date = Calendar.getInstance();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700713 date.set(mYear, mMonth, day);
714 mOnDayClickListener.onDayClick(this, date);
715 }
716
717 // This is a no-op if accessibility is turned off.
718 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
Alan Viverette60b674e2015-03-25 13:00:42 -0700719 return true;
720 }
721
722 /**
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700723 * Provides a virtual view hierarchy for interfacing with an accessibility
724 * service.
725 */
726 private class MonthViewTouchHelper extends ExploreByTouchHelper {
727 private static final String DATE_FORMAT = "dd MMMM yyyy";
728
729 private final Rect mTempRect = new Rect();
730 private final Calendar mTempCalendar = Calendar.getInstance();
731
732 public MonthViewTouchHelper(View host) {
733 super(host);
734 }
735
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700736 @Override
737 protected int getVirtualViewAt(float x, float y) {
Alan Viverette78bf1d32015-04-17 10:39:22 -0700738 final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f));
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700739 if (day >= 0) {
740 return day;
741 }
742 return ExploreByTouchHelper.INVALID_ID;
743 }
744
745 @Override
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700746 protected void getVisibleVirtualViews(IntArray virtualViewIds) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700747 for (int day = 1; day <= mDaysInMonth; day++) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700748 virtualViewIds.add(day);
749 }
750 }
751
752 @Override
753 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
Alan Viverette78bf1d32015-04-17 10:39:22 -0700754 event.setContentDescription(getDayDescription(virtualViewId));
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700755 }
756
757 @Override
758 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
Alan Viverette78bf1d32015-04-17 10:39:22 -0700759 final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700760
761 if (!hasBounds) {
762 // The day is invalid, kill the node.
763 mTempRect.setEmpty();
764 node.setContentDescription("");
765 node.setBoundsInParent(mTempRect);
766 node.setVisibleToUser(false);
767 return;
768 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700769
Alan Viverette78bf1d32015-04-17 10:39:22 -0700770 node.setText(getDayText(virtualViewId));
771 node.setContentDescription(getDayDescription(virtualViewId));
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700772 node.setBoundsInParent(mTempRect);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700773 node.addAction(AccessibilityAction.ACTION_CLICK);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700774
Alan Viverettec5b95c22015-01-07 13:57:12 -0800775 if (virtualViewId == mActivatedDay) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700776 // TODO: This should use activated once that's supported.
777 node.setChecked(true);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700778 }
779
780 }
781
782 @Override
783 protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
784 Bundle arguments) {
785 switch (action) {
786 case AccessibilityNodeInfo.ACTION_CLICK:
Alan Viverette78bf1d32015-04-17 10:39:22 -0700787 return onDayClicked(virtualViewId);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700788 }
789
790 return false;
791 }
792
793 /**
Alan Viverette60b674e2015-03-25 13:00:42 -0700794 * Generates a description for a given virtual view.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700795 *
Alan Viverette78bf1d32015-04-17 10:39:22 -0700796 * @param id the day to generate a description for
Alan Viverette60b674e2015-03-25 13:00:42 -0700797 * @return a description of the virtual view
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700798 */
Alan Viverette78bf1d32015-04-17 10:39:22 -0700799 private CharSequence getDayDescription(int id) {
800 if (id >= 1 && id <= mDaysInMonth) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700801 mTempCalendar.set(mYear, mMonth, id);
802 return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700803 }
804
Alan Viverette60b674e2015-03-25 13:00:42 -0700805 return "";
806 }
807
808 /**
809 * Generates displayed text for a given virtual view.
810 *
Alan Viverette78bf1d32015-04-17 10:39:22 -0700811 * @param id the day to generate text for
Alan Viverette60b674e2015-03-25 13:00:42 -0700812 * @return the visible text of the virtual view
813 */
Alan Viverette78bf1d32015-04-17 10:39:22 -0700814 private CharSequence getDayText(int id) {
815 if (id >= 1 && id <= mDaysInMonth) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700816 return Integer.toString(id);
817 }
818
819 return null;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700820 }
821 }
822
823 /**
824 * Handles callbacks when the user clicks on a time object.
825 */
826 public interface OnDayClickListener {
Alan Viverette78bf1d32015-04-17 10:39:22 -0700827 void onDayClick(SimpleMonthView view, Calendar day);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700828 }
829}