blob: 217693eed686b4d84daddc318f10bf3731d0e71c [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
George Mounte998c3f2015-10-27 08:46:44 -070019import android.annotation.Nullable;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070020import android.content.Context;
21import android.content.res.ColorStateList;
22import android.content.res.Resources;
Alan Viverette0ef59ac2015-03-23 13:13:25 -070023import android.content.res.TypedArray;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070024import android.graphics.Canvas;
25import android.graphics.Paint;
26import android.graphics.Paint.Align;
27import android.graphics.Paint.Style;
28import android.graphics.Rect;
29import android.graphics.Typeface;
Alan Viverettec3e5a822016-05-16 13:35:56 -040030import android.icu.text.DisplayContext;
yingleiw7eef9a12019-10-18 16:31:09 -070031import android.icu.text.RelativeDateTimeFormatter;
Deepanshu Guptaa7f827b2015-08-04 14:15:00 -070032import android.icu.text.SimpleDateFormat;
Alan Viverette68763be2016-05-25 11:42:42 -040033import android.icu.util.Calendar;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070034import android.os.Bundle;
Alan Viverette5dc973c2015-01-08 11:12:39 -080035import android.text.TextPaint;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070036import android.text.format.DateFormat;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070037import android.util.AttributeSet;
Alan Viveretteffb46bf2014-10-24 12:06:11 -070038import android.util.IntArray;
Alan Viverette5c339492015-04-28 14:07:36 -070039import android.util.MathUtils;
Alan Viverettec5b95c22015-01-07 13:57:12 -080040import android.util.StateSet;
George Mounte998c3f2015-10-27 08:46:44 -070041import android.view.KeyEvent;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070042import android.view.MotionEvent;
Aurimas Liutikas99441c52016-10-11 16:48:32 -070043import android.view.PointerIcon;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070044import android.view.View;
George Mounte998c3f2015-10-27 08:46:44 -070045import android.view.ViewParent;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070046import android.view.accessibility.AccessibilityEvent;
47import android.view.accessibility.AccessibilityNodeInfo;
Alan Viverette0ef59ac2015-03-23 13:13:25 -070048import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070049
Aurimas Liutikas99441c52016-10-11 16:48:32 -070050import com.android.internal.R;
51import com.android.internal.widget.ExploreByTouchHelper;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070052
Alan Viveretted5c85c82016-03-29 11:44:32 -040053import libcore.icu.LocaleData;
54
Aurimas Liutikas99441c52016-10-11 16:48:32 -070055import java.text.NumberFormat;
56import java.util.Locale;
57
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070058/**
59 * A calendar-like view displaying a specified month and the appropriate selectable day numbers
60 * within the specified month.
61 */
62class SimpleMonthView extends View {
Alan Viverette0ef59ac2015-03-23 13:13:25 -070063 private static final int DAYS_IN_WEEK = 7;
64 private static final int MAX_WEEKS_IN_MONTH = 6;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070065
66 private static final int DEFAULT_SELECTED_DAY = -1;
67 private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070068
Alan Viverettee264f952016-03-04 13:18:48 -050069 private static final String MONTH_YEAR_FORMAT = "MMMMy";
Alan Viverettec5b95c22015-01-07 13:57:12 -080070
George Mounte998c3f2015-10-27 08:46:44 -070071 private static final int SELECTED_HIGHLIGHT_ALPHA = 0xB0;
72
Alan Viverette5dc973c2015-01-08 11:12:39 -080073 private final TextPaint mMonthPaint = new TextPaint();
74 private final TextPaint mDayOfWeekPaint = new TextPaint();
75 private final TextPaint mDayPaint = new TextPaint();
Alan Viverette0ef59ac2015-03-23 13:13:25 -070076 private final Paint mDaySelectorPaint = new Paint();
77 private final Paint mDayHighlightPaint = new Paint();
George Mounte998c3f2015-10-27 08:46:44 -070078 private final Paint mDayHighlightSelectorPaint = new Paint();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070079
Alan Viveretted5c85c82016-03-29 11:44:32 -040080 /** Array of single-character weekday labels ordered by column index. */
Alan Viverettee264f952016-03-04 13:18:48 -050081 private final String[] mDayOfWeekLabels = new String[7];
82
83 private final Calendar mCalendar;
84 private final Locale mLocale;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070085
86 private final MonthViewTouchHelper mTouchHelper;
87
Chet Haase1ea47152015-07-17 12:42:08 -070088 private final NumberFormat mDayFormatter;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070089
Alan Viverettef63757b2015-04-01 17:14:45 -070090 // Desired dimensions.
91 private final int mDesiredMonthHeight;
92 private final int mDesiredDayOfWeekHeight;
93 private final int mDesiredDayHeight;
94 private final int mDesiredCellWidth;
95 private final int mDesiredDaySelectorRadius;
Alan Viverette60b674e2015-03-25 13:00:42 -070096
Alan Viverettee264f952016-03-04 13:18:48 -050097 private String mMonthYearLabel;
Alan Viverette0ef59ac2015-03-23 13:13:25 -070098
99 private int mMonth;
100 private int mYear;
101
Alan Viverettef63757b2015-04-01 17:14:45 -0700102 // Dimensions as laid out.
103 private int mMonthHeight;
104 private int mDayOfWeekHeight;
105 private int mDayHeight;
106 private int mCellWidth;
107 private int mDaySelectorRadius;
108
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700109 private int mPaddedWidth;
110 private int mPaddedHeight;
111
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700112 /** The day of month for the selected day, or -1 if no day is selected. */
113 private int mActivatedDay = -1;
114
115 /**
116 * The day of month for today, or -1 if the today is not in the current
117 * month.
118 */
119 private int mToday = DEFAULT_SELECTED_DAY;
120
Alan Viveretted5c85c82016-03-29 11:44:32 -0400121 /** The first day of the week (ex. Calendar.SUNDAY) indexed from one. */
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700122 private int mWeekStart = DEFAULT_WEEK_START;
123
124 /** The number of days (ex. 28) in the current month. */
125 private int mDaysInMonth;
126
127 /**
128 * The day of week (ex. Calendar.SUNDAY) for the first day of the current
129 * month.
130 */
131 private int mDayOfWeekStart;
132
133 /** The day of month for the first (inclusive) enabled day. */
134 private int mEnabledDayStart = 1;
135
136 /** The day of month for the last (inclusive) enabled day. */
137 private int mEnabledDayEnd = 31;
138
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700139 /** Optional listener for handling day click actions. */
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700140 private OnDayClickListener mOnDayClickListener;
141
Alan Viverettec5b95c22015-01-07 13:57:12 -0800142 private ColorStateList mDayTextColor;
143
George Mounte998c3f2015-10-27 08:46:44 -0700144 private int mHighlightedDay = -1;
145 private int mPreviouslyHighlightedDay = -1;
146 private boolean mIsTouchHighlighted = false;
Alan Viverette60b674e2015-03-25 13:00:42 -0700147
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700148 public SimpleMonthView(Context context) {
149 this(context, null);
150 }
151
152 public SimpleMonthView(Context context, AttributeSet attrs) {
153 this(context, attrs, R.attr.datePickerStyle);
154 }
155
Alan Viverette50eb0252014-10-24 14:34:26 -0700156 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
157 this(context, attrs, defStyleAttr, 0);
158 }
159
160 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
161 super(context, attrs, defStyleAttr, defStyleRes);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700162
163 final Resources res = context.getResources();
Alan Viverettef63757b2015-04-01 17:14:45 -0700164 mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
165 mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
166 mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
167 mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
Alan Viverette78bf1d32015-04-17 10:39:22 -0700168 mDesiredDaySelectorRadius = res.getDimensionPixelSize(
169 R.dimen.date_picker_day_selector_radius);
Alan Viverette60b674e2015-03-25 13:00:42 -0700170
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700171 // Set up accessibility components.
172 mTouchHelper = new MonthViewTouchHelper(this);
173 setAccessibilityDelegate(mTouchHelper);
174 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700175
Alan Viverettee264f952016-03-04 13:18:48 -0500176 mLocale = res.getConfiguration().locale;
177 mCalendar = Calendar.getInstance(mLocale);
178
179 mDayFormatter = NumberFormat.getIntegerInstance(mLocale);
180
181 updateMonthYearLabel();
182 updateDayOfWeekLabels();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700183
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700184 initPaints(res);
185 }
186
Alan Viverettee264f952016-03-04 13:18:48 -0500187 private void updateMonthYearLabel() {
188 final String format = DateFormat.getBestDateTimePattern(mLocale, MONTH_YEAR_FORMAT);
189 final SimpleDateFormat formatter = new SimpleDateFormat(format, mLocale);
Alan Viverettec3e5a822016-05-16 13:35:56 -0400190 formatter.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
Alan Viverettee264f952016-03-04 13:18:48 -0500191 mMonthYearLabel = formatter.format(mCalendar.getTime());
192 }
193
194 private void updateDayOfWeekLabels() {
Alan Viveretted5c85c82016-03-29 11:44:32 -0400195 // Use tiny (e.g. single-character) weekday names from ICU. The indices
196 // for this list correspond to Calendar days, e.g. SUNDAY is index 1.
197 final String[] tinyWeekdayNames = LocaleData.get(mLocale).tinyWeekdayNames;
198 for (int i = 0; i < DAYS_IN_WEEK; i++) {
199 mDayOfWeekLabels[i] = tinyWeekdayNames[(mWeekStart + i - 1) % DAYS_IN_WEEK + 1];
Alan Viverettee264f952016-03-04 13:18:48 -0500200 }
201 }
202
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700203 /**
204 * Applies the specified text appearance resource to a paint, returning the
205 * text color if one is set in the text appearance.
206 *
207 * @param p the paint to modify
208 * @param resId the resource ID of the text appearance
209 * @return the text color, if available
210 */
211 private ColorStateList applyTextAppearance(Paint p, int resId) {
212 final TypedArray ta = mContext.obtainStyledAttributes(null,
213 R.styleable.TextAppearance, 0, resId);
214
215 final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
216 if (fontFamily != null) {
217 p.setTypeface(Typeface.create(fontFamily, 0));
218 }
219
220 p.setTextSize(ta.getDimensionPixelSize(
221 R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
222
223 final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
224 if (textColor != null) {
225 final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
226 p.setColor(enabledColor);
227 }
228
229 ta.recycle();
230
231 return textColor;
232 }
233
Alan Viverette78bf1d32015-04-17 10:39:22 -0700234 public int getMonthHeight() {
235 return mMonthHeight;
236 }
237
238 public int getCellWidth() {
239 return mCellWidth;
240 }
241
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700242 public void setMonthTextAppearance(int resId) {
Alan Viverette78bf1d32015-04-17 10:39:22 -0700243 applyTextAppearance(mMonthPaint, resId);
Alan Viverette60b674e2015-03-25 13:00:42 -0700244
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700245 invalidate();
246 }
247
248 public void setDayOfWeekTextAppearance(int resId) {
249 applyTextAppearance(mDayOfWeekPaint, resId);
250 invalidate();
251 }
252
253 public void setDayTextAppearance(int resId) {
254 final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
255 if (textColor != null) {
256 mDayTextColor = textColor;
257 }
258
259 invalidate();
260 }
261
Alan Viverettec5b95c22015-01-07 13:57:12 -0800262 /**
263 * Sets up the text and style properties for painting.
264 */
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700265 private void initPaints(Resources res) {
266 final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
267 final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
268 final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
269
270 final int monthTextSize = res.getDimensionPixelSize(
271 R.dimen.date_picker_month_text_size);
272 final int dayOfWeekTextSize = res.getDimensionPixelSize(
273 R.dimen.date_picker_day_of_week_text_size);
274 final int dayTextSize = res.getDimensionPixelSize(
275 R.dimen.date_picker_day_text_size);
276
Alan Viverettec5b95c22015-01-07 13:57:12 -0800277 mMonthPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700278 mMonthPaint.setTextSize(monthTextSize);
279 mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800280 mMonthPaint.setTextAlign(Align.CENTER);
281 mMonthPaint.setStyle(Style.FILL);
282
283 mDayOfWeekPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700284 mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
285 mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800286 mDayOfWeekPaint.setTextAlign(Align.CENTER);
287 mDayOfWeekPaint.setStyle(Style.FILL);
288
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700289 mDaySelectorPaint.setAntiAlias(true);
290 mDaySelectorPaint.setStyle(Style.FILL);
291
292 mDayHighlightPaint.setAntiAlias(true);
293 mDayHighlightPaint.setStyle(Style.FILL);
Alan Viverettec5b95c22015-01-07 13:57:12 -0800294
George Mounte998c3f2015-10-27 08:46:44 -0700295 mDayHighlightSelectorPaint.setAntiAlias(true);
296 mDayHighlightSelectorPaint.setStyle(Style.FILL);
297
Alan Viverettec5b95c22015-01-07 13:57:12 -0800298 mDayPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700299 mDayPaint.setTextSize(dayTextSize);
300 mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800301 mDayPaint.setTextAlign(Align.CENTER);
302 mDayPaint.setStyle(Style.FILL);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700303 }
304
Alan Viverettec5b95c22015-01-07 13:57:12 -0800305 void setMonthTextColor(ColorStateList monthTextColor) {
306 final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
307 mMonthPaint.setColor(enabledColor);
308 invalidate();
309 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700310
Alan Viverettec5b95c22015-01-07 13:57:12 -0800311 void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
312 final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
313 mDayOfWeekPaint.setColor(enabledColor);
314 invalidate();
315 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700316
Alan Viverettec5b95c22015-01-07 13:57:12 -0800317 void setDayTextColor(ColorStateList dayTextColor) {
318 mDayTextColor = dayTextColor;
319 invalidate();
320 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700321
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700322 void setDaySelectorColor(ColorStateList dayBackgroundColor) {
Alan Viverette5dc973c2015-01-08 11:12:39 -0800323 final int activatedColor = dayBackgroundColor.getColorForState(
Alan Viverettec5b95c22015-01-07 13:57:12 -0800324 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700325 mDaySelectorPaint.setColor(activatedColor);
George Mounte998c3f2015-10-27 08:46:44 -0700326 mDayHighlightSelectorPaint.setColor(activatedColor);
327 mDayHighlightSelectorPaint.setAlpha(SELECTED_HIGHLIGHT_ALPHA);
Alan Viverettec5b95c22015-01-07 13:57:12 -0800328 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700329 }
330
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700331 void setDayHighlightColor(ColorStateList dayHighlightColor) {
332 final int pressedColor = dayHighlightColor.getColorForState(
333 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
334 mDayHighlightPaint.setColor(pressedColor);
335 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700336 }
337
338 public void setOnDayClickListener(OnDayClickListener listener) {
339 mOnDayClickListener = listener;
340 }
341
342 @Override
343 public boolean dispatchHoverEvent(MotionEvent event) {
344 // First right-of-refusal goes the touch exploration helper.
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700345 return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700346 }
347
348 @Override
349 public boolean onTouchEvent(MotionEvent event) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700350 final int x = (int) (event.getX() + 0.5f);
351 final int y = (int) (event.getY() + 0.5f);
352
Alan Viveretteddf655c2015-04-22 13:43:31 -0700353 final int action = event.getAction();
354 switch (action) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700355 case MotionEvent.ACTION_DOWN:
356 case MotionEvent.ACTION_MOVE:
Alan Viverette78bf1d32015-04-17 10:39:22 -0700357 final int touchedItem = getDayAtLocation(x, y);
George Mounte998c3f2015-10-27 08:46:44 -0700358 mIsTouchHighlighted = true;
359 if (mHighlightedDay != touchedItem) {
360 mHighlightedDay = touchedItem;
361 mPreviouslyHighlightedDay = touchedItem;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700362 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700363 }
Alan Viveretteddf655c2015-04-22 13:43:31 -0700364 if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) {
365 // Touch something that's not an item, reject event.
366 return false;
367 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700368 break;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700369
370 case MotionEvent.ACTION_UP:
Alan Viverette78bf1d32015-04-17 10:39:22 -0700371 final int clickedDay = getDayAtLocation(x, y);
372 onDayClicked(clickedDay);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700373 // Fall through.
374 case MotionEvent.ACTION_CANCEL:
375 // Reset touched day on stream end.
George Mounte998c3f2015-10-27 08:46:44 -0700376 mHighlightedDay = -1;
377 mIsTouchHighlighted = false;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700378 invalidate();
379 break;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700380 }
381 return true;
382 }
383
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700384 @Override
George Mounte998c3f2015-10-27 08:46:44 -0700385 public boolean onKeyDown(int keyCode, KeyEvent event) {
386 // We need to handle focus change within the SimpleMonthView because we are simulating
387 // multiple Views. The arrow keys will move between days until there is no space (no
388 // day to the left, top, right, or bottom). Focus forward and back jumps out of the
389 // SimpleMonthView, skipping over other SimpleMonthViews in the parent ViewPager
390 // to the next focusable View in the hierarchy.
391 boolean focusChanged = false;
392 switch (event.getKeyCode()) {
393 case KeyEvent.KEYCODE_DPAD_LEFT:
394 if (event.hasNoModifiers()) {
395 focusChanged = moveOneDay(isLayoutRtl());
396 }
397 break;
398 case KeyEvent.KEYCODE_DPAD_RIGHT:
399 if (event.hasNoModifiers()) {
400 focusChanged = moveOneDay(!isLayoutRtl());
401 }
402 break;
403 case KeyEvent.KEYCODE_DPAD_UP:
404 if (event.hasNoModifiers()) {
405 ensureFocusedDay();
406 if (mHighlightedDay > 7) {
407 mHighlightedDay -= 7;
408 focusChanged = true;
409 }
410 }
411 break;
412 case KeyEvent.KEYCODE_DPAD_DOWN:
413 if (event.hasNoModifiers()) {
414 ensureFocusedDay();
415 if (mHighlightedDay <= mDaysInMonth - 7) {
416 mHighlightedDay += 7;
417 focusChanged = true;
418 }
419 }
420 break;
421 case KeyEvent.KEYCODE_DPAD_CENTER:
422 case KeyEvent.KEYCODE_ENTER:
423 if (mHighlightedDay != -1) {
424 onDayClicked(mHighlightedDay);
425 return true;
426 }
427 break;
428 case KeyEvent.KEYCODE_TAB: {
429 int focusChangeDirection = 0;
430 if (event.hasNoModifiers()) {
431 focusChangeDirection = View.FOCUS_FORWARD;
432 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
433 focusChangeDirection = View.FOCUS_BACKWARD;
434 }
435 if (focusChangeDirection != 0) {
436 final ViewParent parent = getParent();
437 // move out of the ViewPager next/previous
438 View nextFocus = this;
439 do {
440 nextFocus = nextFocus.focusSearch(focusChangeDirection);
441 } while (nextFocus != null && nextFocus != this &&
442 nextFocus.getParent() == parent);
443 if (nextFocus != null) {
444 nextFocus.requestFocus();
445 return true;
446 }
447 }
448 break;
449 }
450 }
451 if (focusChanged) {
452 invalidate();
453 return true;
454 } else {
455 return super.onKeyDown(keyCode, event);
456 }
457 }
458
459 private boolean moveOneDay(boolean positive) {
460 ensureFocusedDay();
461 boolean focusChanged = false;
462 if (positive) {
463 if (!isLastDayOfWeek(mHighlightedDay) && mHighlightedDay < mDaysInMonth) {
464 mHighlightedDay++;
465 focusChanged = true;
466 }
467 } else {
468 if (!isFirstDayOfWeek(mHighlightedDay) && mHighlightedDay > 1) {
469 mHighlightedDay--;
470 focusChanged = true;
471 }
472 }
473 return focusChanged;
474 }
475
476 @Override
477 protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
478 @Nullable Rect previouslyFocusedRect) {
479 if (gainFocus) {
480 // If we've gained focus through arrow keys, we should find the day closest
481 // to the focus rect. If we've gained focus through forward/back, we should
482 // focus on the selected day if there is one.
483 final int offset = findDayOffset();
484 switch(direction) {
485 case View.FOCUS_RIGHT: {
486 int row = findClosestRow(previouslyFocusedRect);
487 mHighlightedDay = row == 0 ? 1 : (row * DAYS_IN_WEEK) - offset + 1;
488 break;
489 }
490 case View.FOCUS_LEFT: {
491 int row = findClosestRow(previouslyFocusedRect) + 1;
492 mHighlightedDay = Math.min(mDaysInMonth, (row * DAYS_IN_WEEK) - offset);
493 break;
494 }
495 case View.FOCUS_DOWN: {
496 final int col = findClosestColumn(previouslyFocusedRect);
497 final int day = col - offset + 1;
498 mHighlightedDay = day < 1 ? day + DAYS_IN_WEEK : day;
499 break;
500 }
501 case View.FOCUS_UP: {
502 final int col = findClosestColumn(previouslyFocusedRect);
503 final int maxWeeks = (offset + mDaysInMonth) / DAYS_IN_WEEK;
504 final int day = col - offset + (DAYS_IN_WEEK * maxWeeks) + 1;
505 mHighlightedDay = day > mDaysInMonth ? day - DAYS_IN_WEEK : day;
506 break;
507 }
508 }
509 ensureFocusedDay();
510 invalidate();
511 }
512 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
513 }
514
515 /**
516 * Returns the row (0 indexed) closest to previouslyFocusedRect or center if null.
517 */
518 private int findClosestRow(@Nullable Rect previouslyFocusedRect) {
519 if (previouslyFocusedRect == null) {
520 return 3;
George Mount45362a62017-08-04 10:17:36 -0700521 } else if (mDayHeight == 0) {
522 return 0; // There hasn't been a layout, so just choose the first row
George Mounte998c3f2015-10-27 08:46:44 -0700523 } else {
524 int centerY = previouslyFocusedRect.centerY();
525
526 final TextPaint p = mDayPaint;
527 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
528 final int rowHeight = mDayHeight;
529
530 // Text is vertically centered within the row height.
531 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
532 final int rowCenter = headerHeight + rowHeight / 2;
533
534 centerY -= rowCenter - halfLineHeight;
535 int row = Math.round(centerY / (float) rowHeight);
536 final int maxDay = findDayOffset() + mDaysInMonth;
537 final int maxRows = (maxDay / DAYS_IN_WEEK) - ((maxDay % DAYS_IN_WEEK == 0) ? 1 : 0);
538
539 row = MathUtils.constrain(row, 0, maxRows);
540 return row;
541 }
542 }
543
544 /**
545 * Returns the column (0 indexed) closest to the previouslyFocusedRect or center if null.
546 * The 0 index is related to the first day of the week.
547 */
548 private int findClosestColumn(@Nullable Rect previouslyFocusedRect) {
549 if (previouslyFocusedRect == null) {
550 return DAYS_IN_WEEK / 2;
George Mount45362a62017-08-04 10:17:36 -0700551 } else if (mCellWidth == 0) {
552 return 0; // There hasn't been a layout, so we can just choose the first column
George Mounte998c3f2015-10-27 08:46:44 -0700553 } else {
554 int centerX = previouslyFocusedRect.centerX() - mPaddingLeft;
555 final int columnFromLeft =
556 MathUtils.constrain(centerX / mCellWidth, 0, DAYS_IN_WEEK - 1);
557 return isLayoutRtl() ? DAYS_IN_WEEK - columnFromLeft - 1: columnFromLeft;
558 }
559 }
560
561 @Override
562 public void getFocusedRect(Rect r) {
563 if (mHighlightedDay > 0) {
564 getBoundsForDay(mHighlightedDay, r);
565 } else {
566 super.getFocusedRect(r);
567 }
568 }
569
570 @Override
571 protected void onFocusLost() {
572 if (!mIsTouchHighlighted) {
573 // Unhighlight a day.
574 mPreviouslyHighlightedDay = mHighlightedDay;
575 mHighlightedDay = -1;
576 invalidate();
577 }
578 super.onFocusLost();
579 }
580
581 /**
582 * Ensure some day is highlighted. If a day isn't highlighted, it chooses the selected day,
583 * if possible, or the first day of the month if not.
584 */
585 private void ensureFocusedDay() {
586 if (mHighlightedDay != -1) {
587 return;
588 }
589 if (mPreviouslyHighlightedDay != -1) {
590 mHighlightedDay = mPreviouslyHighlightedDay;
591 return;
592 }
593 if (mActivatedDay != -1) {
594 mHighlightedDay = mActivatedDay;
595 return;
596 }
597 mHighlightedDay = 1;
598 }
599
600 private boolean isFirstDayOfWeek(int day) {
601 final int offset = findDayOffset();
602 return (offset + day - 1) % DAYS_IN_WEEK == 0;
603 }
604
605 private boolean isLastDayOfWeek(int day) {
606 final int offset = findDayOffset();
607 return (offset + day) % DAYS_IN_WEEK == 0;
608 }
609
610 @Override
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700611 protected void onDraw(Canvas canvas) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700612 final int paddingLeft = getPaddingLeft();
613 final int paddingTop = getPaddingTop();
614 canvas.translate(paddingLeft, paddingTop);
615
616 drawMonth(canvas);
617 drawDaysOfWeek(canvas);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700618 drawDays(canvas);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700619
620 canvas.translate(-paddingLeft, -paddingTop);
621 }
622
623 private void drawMonth(Canvas canvas) {
624 final float x = mPaddedWidth / 2f;
625
626 // Vertically centered within the month header height.
627 final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
628 final float y = (mMonthHeight - lineHeight) / 2f;
629
Alan Viverettee264f952016-03-04 13:18:48 -0500630 canvas.drawText(mMonthYearLabel, x, y, mMonthPaint);
631 }
632
633 public String getMonthYearLabel() {
634 return mMonthYearLabel;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700635 }
636
637 private void drawDaysOfWeek(Canvas canvas) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700638 final TextPaint p = mDayOfWeekPaint;
639 final int headerHeight = mMonthHeight;
640 final int rowHeight = mDayOfWeekHeight;
Alan Viverettef63757b2015-04-01 17:14:45 -0700641 final int colWidth = mCellWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700642
Alan Viverette60b674e2015-03-25 13:00:42 -0700643 // Text is vertically centered within the day of week height.
644 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
645 final int rowCenter = headerHeight + rowHeight / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700646
Alan Viverette60b674e2015-03-25 13:00:42 -0700647 for (int col = 0; col < DAYS_IN_WEEK; col++) {
648 final int colCenter = colWidth * col + colWidth / 2;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700649 final int colCenterRtl;
650 if (isLayoutRtl()) {
651 colCenterRtl = mPaddedWidth - colCenter;
652 } else {
653 colCenterRtl = colCenter;
654 }
655
Alan Viveretted5c85c82016-03-29 11:44:32 -0400656 final String label = mDayOfWeekLabels[col];
Alan Viveretteddf655c2015-04-22 13:43:31 -0700657 canvas.drawText(label, colCenterRtl, rowCenter - halfLineHeight, p);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700658 }
659 }
660
661 /**
662 * Draws the month days.
663 */
664 private void drawDays(Canvas canvas) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700665 final TextPaint p = mDayPaint;
666 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
667 final int rowHeight = mDayHeight;
Alan Viverettef63757b2015-04-01 17:14:45 -0700668 final int colWidth = mCellWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700669
Alan Viverette60b674e2015-03-25 13:00:42 -0700670 // Text is vertically centered within the row height.
671 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
672 int rowCenter = headerHeight + rowHeight / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700673
Alan Viverette60b674e2015-03-25 13:00:42 -0700674 for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) {
675 final int colCenter = colWidth * col + colWidth / 2;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700676 final int colCenterRtl;
677 if (isLayoutRtl()) {
678 colCenterRtl = mPaddedWidth - colCenter;
679 } else {
680 colCenterRtl = colCenter;
681 }
682
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700683 int stateMask = 0;
684
Alan Viverette5c339492015-04-28 14:07:36 -0700685 final boolean isDayEnabled = isDayEnabled(day);
686 if (isDayEnabled) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700687 stateMask |= StateSet.VIEW_STATE_ENABLED;
688 }
689
690 final boolean isDayActivated = mActivatedDay == day;
George Mounte998c3f2015-10-27 08:46:44 -0700691 final boolean isDayHighlighted = mHighlightedDay == day;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700692 if (isDayActivated) {
693 stateMask |= StateSet.VIEW_STATE_ACTIVATED;
694
695 // Adjust the circle to be centered on the row.
George Mounte998c3f2015-10-27 08:46:44 -0700696 final Paint paint = isDayHighlighted ? mDayHighlightSelectorPaint :
697 mDaySelectorPaint;
698 canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, paint);
699 } else if (isDayHighlighted) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700700 stateMask |= StateSet.VIEW_STATE_PRESSED;
701
Alan Viverette5c339492015-04-28 14:07:36 -0700702 if (isDayEnabled) {
703 // Adjust the circle to be centered on the row.
704 canvas.drawCircle(colCenterRtl, rowCenter,
705 mDaySelectorRadius, mDayHighlightPaint);
706 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700707 }
708
709 final boolean isDayToday = mToday == day;
710 final int dayTextColor;
711 if (isDayToday && !isDayActivated) {
712 dayTextColor = mDaySelectorPaint.getColor();
713 } else {
714 final int[] stateSet = StateSet.get(stateMask);
715 dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
716 }
Alan Viverette60b674e2015-03-25 13:00:42 -0700717 p.setColor(dayTextColor);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700718
Chet Haase1ea47152015-07-17 12:42:08 -0700719 canvas.drawText(mDayFormatter.format(day), colCenterRtl, rowCenter - halfLineHeight, p);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700720
Alan Viverette60b674e2015-03-25 13:00:42 -0700721 col++;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700722
Alan Viverette60b674e2015-03-25 13:00:42 -0700723 if (col == DAYS_IN_WEEK) {
724 col = 0;
725 rowCenter += rowHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700726 }
727 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700728 }
729
Alan Viverette5c339492015-04-28 14:07:36 -0700730 private boolean isDayEnabled(int day) {
731 return day >= mEnabledDayStart && day <= mEnabledDayEnd;
732 }
733
734 private boolean isValidDayOfMonth(int day) {
735 return day >= 1 && day <= mDaysInMonth;
736 }
737
Alan Viverette518ff0d2014-08-15 14:20:35 -0700738 private static boolean isValidDayOfWeek(int day) {
Alan Viverette58780762014-09-10 17:09:13 -0700739 return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
740 }
741
742 private static boolean isValidMonth(int month) {
743 return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700744 }
745
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700746 /**
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700747 * Sets the selected day.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700748 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700749 * @param dayOfMonth the selected day of the month, or {@code -1} to clear
750 * the selection
751 */
752 public void setSelectedDay(int dayOfMonth) {
753 mActivatedDay = dayOfMonth;
754
755 // Invalidate cached accessibility information.
756 mTouchHelper.invalidateRoot();
757 invalidate();
758 }
759
760 /**
761 * Sets the first day of the week.
762 *
763 * @param weekStart which day the week should start on, valid values are
764 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
765 */
766 public void setFirstDayOfWeek(int weekStart) {
767 if (isValidDayOfWeek(weekStart)) {
768 mWeekStart = weekStart;
769 } else {
770 mWeekStart = mCalendar.getFirstDayOfWeek();
771 }
772
Alan Viverettee264f952016-03-04 13:18:48 -0500773 updateDayOfWeekLabels();
774
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700775 // Invalidate cached accessibility information.
776 mTouchHelper.invalidateRoot();
777 invalidate();
778 }
779
780 /**
781 * Sets all the parameters for displaying this week.
782 * <p>
783 * Parameters have a default value and will only update if a new value is
784 * included, except for focus month, which will always default to no focus
785 * month if no value is passed in. The only required parameter is the week
786 * start.
787 *
788 * @param selectedDay the selected day of the month, or -1 for no selection
789 * @param month the month
790 * @param year the year
791 * @param weekStart which day the week should start on, valid values are
792 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
793 * @param enabledDayStart the first enabled day
794 * @param enabledDayEnd the last enabled day
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700795 */
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700796 void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
797 int enabledDayEnd) {
Alan Viverettec5b95c22015-01-07 13:57:12 -0800798 mActivatedDay = selectedDay;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700799
Alan Viverette58780762014-09-10 17:09:13 -0700800 if (isValidMonth(month)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700801 mMonth = month;
802 }
803 mYear = year;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700804
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700805 mCalendar.set(Calendar.MONTH, mMonth);
806 mCalendar.set(Calendar.YEAR, mYear);
807 mCalendar.set(Calendar.DAY_OF_MONTH, 1);
808 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
809
Alan Viverette518ff0d2014-08-15 14:20:35 -0700810 if (isValidDayOfWeek(weekStart)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700811 mWeekStart = weekStart;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700812 } else {
813 mWeekStart = mCalendar.getFirstDayOfWeek();
814 }
815
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700816 // Figure out what day today is.
817 final Calendar today = Calendar.getInstance();
818 mToday = -1;
819 mDaysInMonth = getDaysInMonth(mMonth, mYear);
820 for (int i = 0; i < mDaysInMonth; i++) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700821 final int day = i + 1;
822 if (sameDay(day, today)) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700823 mToday = day;
824 }
825 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700826
Alan Viverette5c339492015-04-28 14:07:36 -0700827 mEnabledDayStart = MathUtils.constrain(enabledDayStart, 1, mDaysInMonth);
828 mEnabledDayEnd = MathUtils.constrain(enabledDayEnd, mEnabledDayStart, mDaysInMonth);
829
Alan Viverette08dbbcd2016-04-14 10:15:04 -0400830 updateMonthYearLabel();
831 updateDayOfWeekLabels();
832
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700833 // Invalidate cached accessibility information.
834 mTouchHelper.invalidateRoot();
Alan Viverette08dbbcd2016-04-14 10:15:04 -0400835 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700836 }
837
838 private static int getDaysInMonth(int month, int year) {
839 switch (month) {
840 case Calendar.JANUARY:
841 case Calendar.MARCH:
842 case Calendar.MAY:
843 case Calendar.JULY:
844 case Calendar.AUGUST:
845 case Calendar.OCTOBER:
846 case Calendar.DECEMBER:
847 return 31;
848 case Calendar.APRIL:
849 case Calendar.JUNE:
850 case Calendar.SEPTEMBER:
851 case Calendar.NOVEMBER:
852 return 30;
853 case Calendar.FEBRUARY:
Neil Fuller6c0984a2019-04-17 15:54:22 +0100854 return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? 29 : 28;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700855 default:
856 throw new IllegalArgumentException("Invalid Month");
857 }
858 }
859
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700860 private boolean sameDay(int day, Calendar today) {
861 return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
862 && day == today.get(Calendar.DAY_OF_MONTH);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700863 }
864
865 @Override
866 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette816aa142015-04-10 15:41:10 -0700867 final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH
868 + mDesiredDayOfWeekHeight + mDesiredMonthHeight
869 + getPaddingTop() + getPaddingBottom();
Alan Viverettef63757b2015-04-01 17:14:45 -0700870 final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700871 + getPaddingStart() + getPaddingEnd();
872 final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
873 final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
874 setMeasuredDimension(resolvedWidth, resolvedHeight);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700875 }
876
877 @Override
Alan Viveretteddf655c2015-04-22 13:43:31 -0700878 public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
879 super.onRtlPropertiesChanged(layoutDirection);
880
881 requestLayout();
882 }
883
884 @Override
Alan Viverettef63757b2015-04-01 17:14:45 -0700885 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
886 if (!changed) {
887 return;
888 }
Alan Viverette60b674e2015-03-25 13:00:42 -0700889
Alan Viverettef63757b2015-04-01 17:14:45 -0700890 // Let's initialize a completely reasonable number of variables.
891 final int w = right - left;
892 final int h = bottom - top;
893 final int paddingLeft = getPaddingLeft();
894 final int paddingTop = getPaddingTop();
895 final int paddingRight = getPaddingRight();
896 final int paddingBottom = getPaddingBottom();
897 final int paddedRight = w - paddingRight;
898 final int paddedBottom = h - paddingBottom;
899 final int paddedWidth = paddedRight - paddingLeft;
900 final int paddedHeight = paddedBottom - paddingTop;
901 if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) {
902 return;
903 }
904
905 mPaddedWidth = paddedWidth;
906 mPaddedHeight = paddedHeight;
907
908 // We may have been laid out smaller than our preferred size. If so,
909 // scale all dimensions to fit.
910 final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom;
911 final float scaleH = paddedHeight / (float) measuredPaddedHeight;
912 final int monthHeight = (int) (mDesiredMonthHeight * scaleH);
Alan Viverette60b674e2015-03-25 13:00:42 -0700913 final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
Alan Viverettef63757b2015-04-01 17:14:45 -0700914 mMonthHeight = monthHeight;
915 mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH);
916 mDayHeight = (int) (mDesiredDayHeight * scaleH);
917 mCellWidth = cellWidth;
918
919 // Compute the largest day selector radius that's still within the clip
920 // bounds and desired selector radius.
921 final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
922 final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
923 mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
924 Math.min(maxSelectorWidth, maxSelectorHeight));
Alan Viverette60b674e2015-03-25 13:00:42 -0700925
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700926 // Invalidate cached accessibility information.
927 mTouchHelper.invalidateRoot();
928 }
929
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700930 private int findDayOffset() {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700931 final int offset = mDayOfWeekStart - mWeekStart;
932 if (mDayOfWeekStart < mWeekStart) {
933 return offset + DAYS_IN_WEEK;
934 }
935 return offset;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700936 }
937
938 /**
Alan Viverette78bf1d32015-04-17 10:39:22 -0700939 * Calculates the day of the month at the specified touch position. Returns
940 * the day of the month or -1 if the position wasn't in a valid day.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700941 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700942 * @param x the x position of the touch event
943 * @param y the y position of the touch event
Alan Viverette78bf1d32015-04-17 10:39:22 -0700944 * @return the day of the month at (x, y), or -1 if the position wasn't in
945 * a valid day
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700946 */
Alan Viverette78bf1d32015-04-17 10:39:22 -0700947 private int getDayAtLocation(int x, int y) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700948 final int paddedX = x - getPaddingLeft();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700949 if (paddedX < 0 || paddedX >= mPaddedWidth) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700950 return -1;
951 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700952
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700953 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
Alan Viverette60b674e2015-03-25 13:00:42 -0700954 final int paddedY = y - getPaddingTop();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700955 if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700956 return -1;
957 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700958
Alan Viveretteddf655c2015-04-22 13:43:31 -0700959 // Adjust for RTL after applying padding.
960 final int paddedXRtl;
961 if (isLayoutRtl()) {
962 paddedXRtl = mPaddedWidth - paddedX;
963 } else {
964 paddedXRtl = paddedX;
965 }
966
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700967 final int row = (paddedY - headerHeight) / mDayHeight;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700968 final int col = (paddedXRtl * DAYS_IN_WEEK) / mPaddedWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700969 final int index = col + row * DAYS_IN_WEEK;
970 final int day = index + 1 - findDayOffset();
Alan Viverette5c339492015-04-28 14:07:36 -0700971 if (!isValidDayOfMonth(day)) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700972 return -1;
973 }
974
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700975 return day;
976 }
977
978 /**
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700979 * Calculates the bounds of the specified day.
980 *
Alan Viverette78bf1d32015-04-17 10:39:22 -0700981 * @param id the day of the month
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700982 * @param outBounds the rect to populate with bounds
983 */
Kirill Grouchnikov698b7512016-04-11 17:16:52 -0400984 public boolean getBoundsForDay(int id, Rect outBounds) {
Alan Viverette5c339492015-04-28 14:07:36 -0700985 if (!isValidDayOfMonth(id)) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700986 return false;
987 }
988
Alan Viverette60b674e2015-03-25 13:00:42 -0700989 final int index = id - 1 + findDayOffset();
990
Alan Viveretteddf655c2015-04-22 13:43:31 -0700991 // Compute left edge, taking into account RTL.
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700992 final int col = index % DAYS_IN_WEEK;
Alan Viverettef63757b2015-04-01 17:14:45 -0700993 final int colWidth = mCellWidth;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700994 final int left;
995 if (isLayoutRtl()) {
996 left = getWidth() - getPaddingRight() - (col + 1) * colWidth;
997 } else {
998 left = getPaddingLeft() + col * colWidth;
999 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001000
Alan Viverette60b674e2015-03-25 13:00:42 -07001001 // Compute top edge.
1002 final int row = index / DAYS_IN_WEEK;
1003 final int rowHeight = mDayHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001004 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
Alan Viverette60b674e2015-03-25 13:00:42 -07001005 final int top = getPaddingTop() + headerHeight + row * rowHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001006
Alan Viverette60b674e2015-03-25 13:00:42 -07001007 outBounds.set(left, top, left + colWidth, top + rowHeight);
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001008
Alan Viverette78bf1d32015-04-17 10:39:22 -07001009 return true;
Alan Viverette60b674e2015-03-25 13:00:42 -07001010 }
1011
1012 /**
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001013 * Called when the user clicks on a day. Handles callbacks to the
1014 * {@link OnDayClickListener} if one is set.
1015 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001016 * @param day the day that was clicked
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001017 */
Alan Viverette60b674e2015-03-25 13:00:42 -07001018 private boolean onDayClicked(int day) {
Alan Viverette5c339492015-04-28 14:07:36 -07001019 if (!isValidDayOfMonth(day) || !isDayEnabled(day)) {
Alan Viverette60b674e2015-03-25 13:00:42 -07001020 return false;
1021 }
1022
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001023 if (mOnDayClickListener != null) {
Alan Viverette60b674e2015-03-25 13:00:42 -07001024 final Calendar date = Calendar.getInstance();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001025 date.set(mYear, mMonth, day);
1026 mOnDayClickListener.onDayClick(this, date);
1027 }
1028
1029 // This is a no-op if accessibility is turned off.
1030 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
Alan Viverette60b674e2015-03-25 13:00:42 -07001031 return true;
1032 }
1033
Vladislav Kaznacheev47f333a2016-09-21 11:37:08 -07001034 @Override
1035 public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
1036 if (!isEnabled()) {
1037 return null;
1038 }
1039 // Add 0.5f to event coordinates to match the logic in onTouchEvent.
1040 final int x = (int) (event.getX() + 0.5f);
1041 final int y = (int) (event.getY() + 0.5f);
1042 final int dayUnderPointer = getDayAtLocation(x, y);
1043 if (dayUnderPointer >= 0) {
1044 return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
1045 }
1046 return super.onResolvePointerIcon(event, pointerIndex);
1047 }
1048
Alan Viverette60b674e2015-03-25 13:00:42 -07001049 /**
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001050 * Provides a virtual view hierarchy for interfacing with an accessibility
1051 * service.
1052 */
1053 private class MonthViewTouchHelper extends ExploreByTouchHelper {
1054 private static final String DATE_FORMAT = "dd MMMM yyyy";
1055
1056 private final Rect mTempRect = new Rect();
1057 private final Calendar mTempCalendar = Calendar.getInstance();
1058
1059 public MonthViewTouchHelper(View host) {
1060 super(host);
1061 }
1062
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001063 @Override
1064 protected int getVirtualViewAt(float x, float y) {
Alan Viverette78bf1d32015-04-17 10:39:22 -07001065 final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f));
Alan Viverette5c339492015-04-28 14:07:36 -07001066 if (day != -1) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001067 return day;
1068 }
1069 return ExploreByTouchHelper.INVALID_ID;
1070 }
1071
1072 @Override
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001073 protected void getVisibleVirtualViews(IntArray virtualViewIds) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001074 for (int day = 1; day <= mDaysInMonth; day++) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001075 virtualViewIds.add(day);
1076 }
1077 }
1078
1079 @Override
1080 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
Alan Viverette78bf1d32015-04-17 10:39:22 -07001081 event.setContentDescription(getDayDescription(virtualViewId));
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001082 }
1083
1084 @Override
1085 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
Alan Viverette78bf1d32015-04-17 10:39:22 -07001086 final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect);
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001087
1088 if (!hasBounds) {
1089 // The day is invalid, kill the node.
1090 mTempRect.setEmpty();
1091 node.setContentDescription("");
1092 node.setBoundsInParent(mTempRect);
1093 node.setVisibleToUser(false);
1094 return;
1095 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001096
Alan Viverette78bf1d32015-04-17 10:39:22 -07001097 node.setText(getDayText(virtualViewId));
1098 node.setContentDescription(getDayDescription(virtualViewId));
yingleiw7eef9a12019-10-18 16:31:09 -07001099 if (virtualViewId == mToday) {
1100 RelativeDateTimeFormatter fmt = RelativeDateTimeFormatter.getInstance();
1101 node.setStateDescription(fmt.format(RelativeDateTimeFormatter.Direction.THIS,
1102 RelativeDateTimeFormatter.AbsoluteUnit.DAY));
1103 }
1104 if (virtualViewId == mActivatedDay) {
1105 node.setSelected(true);
1106 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001107 node.setBoundsInParent(mTempRect);
Alan Viverette5c339492015-04-28 14:07:36 -07001108
1109 final boolean isDayEnabled = isDayEnabled(virtualViewId);
1110 if (isDayEnabled) {
1111 node.addAction(AccessibilityAction.ACTION_CLICK);
1112 }
1113
1114 node.setEnabled(isDayEnabled);
yingleiwa26a37c2019-09-20 16:23:39 -07001115 node.setClickable(true);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001116
Alan Viverettec5b95c22015-01-07 13:57:12 -08001117 if (virtualViewId == mActivatedDay) {
Alan Viverette60b674e2015-03-25 13:00:42 -07001118 // TODO: This should use activated once that's supported.
1119 node.setChecked(true);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001120 }
1121
1122 }
1123
1124 @Override
1125 protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
1126 Bundle arguments) {
1127 switch (action) {
1128 case AccessibilityNodeInfo.ACTION_CLICK:
Alan Viverette78bf1d32015-04-17 10:39:22 -07001129 return onDayClicked(virtualViewId);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001130 }
1131
1132 return false;
1133 }
1134
1135 /**
Alan Viverette60b674e2015-03-25 13:00:42 -07001136 * Generates a description for a given virtual view.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001137 *
Alan Viverette78bf1d32015-04-17 10:39:22 -07001138 * @param id the day to generate a description for
Alan Viverette60b674e2015-03-25 13:00:42 -07001139 * @return a description of the virtual view
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001140 */
Alan Viverette78bf1d32015-04-17 10:39:22 -07001141 private CharSequence getDayDescription(int id) {
Alan Viverette5c339492015-04-28 14:07:36 -07001142 if (isValidDayOfMonth(id)) {
Alan Viverette60b674e2015-03-25 13:00:42 -07001143 mTempCalendar.set(mYear, mMonth, id);
1144 return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001145 }
1146
Alan Viverette60b674e2015-03-25 13:00:42 -07001147 return "";
1148 }
1149
1150 /**
1151 * Generates displayed text for a given virtual view.
1152 *
Alan Viverette78bf1d32015-04-17 10:39:22 -07001153 * @param id the day to generate text for
Alan Viverette60b674e2015-03-25 13:00:42 -07001154 * @return the visible text of the virtual view
1155 */
Alan Viverette78bf1d32015-04-17 10:39:22 -07001156 private CharSequence getDayText(int id) {
Alan Viverette5c339492015-04-28 14:07:36 -07001157 if (isValidDayOfMonth(id)) {
Chet Haase1ea47152015-07-17 12:42:08 -07001158 return mDayFormatter.format(id);
Alan Viverette60b674e2015-03-25 13:00:42 -07001159 }
1160
1161 return null;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001162 }
1163 }
1164
1165 /**
1166 * Handles callbacks when the user clicks on a time object.
1167 */
1168 public interface OnDayClickListener {
Alan Viverette78bf1d32015-04-17 10:39:22 -07001169 void onDayClick(SimpleMonthView view, Calendar day);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001170 }
1171}