blob: 9982732384dada79c098ce61b609452135de2a54 [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;
Deepanshu Guptaa7f827b2015-08-04 14:15:00 -070031import android.icu.text.SimpleDateFormat;
Alan Viverette68763be2016-05-25 11:42:42 -040032import android.icu.util.Calendar;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070033import android.os.Bundle;
Alan Viverette5dc973c2015-01-08 11:12:39 -080034import android.text.TextPaint;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070035import android.text.format.DateFormat;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070036import android.util.AttributeSet;
Alan Viveretteffb46bf2014-10-24 12:06:11 -070037import android.util.IntArray;
Alan Viverette5c339492015-04-28 14:07:36 -070038import android.util.MathUtils;
Alan Viverettec5b95c22015-01-07 13:57:12 -080039import android.util.StateSet;
George Mounte998c3f2015-10-27 08:46:44 -070040import android.view.KeyEvent;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070041import android.view.MotionEvent;
Aurimas Liutikas99441c52016-10-11 16:48:32 -070042import android.view.PointerIcon;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070043import android.view.View;
George Mounte998c3f2015-10-27 08:46:44 -070044import android.view.ViewParent;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070045import android.view.accessibility.AccessibilityEvent;
46import android.view.accessibility.AccessibilityNodeInfo;
Alan Viverette0ef59ac2015-03-23 13:13:25 -070047import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070048
Aurimas Liutikas99441c52016-10-11 16:48:32 -070049import com.android.internal.R;
50import com.android.internal.widget.ExploreByTouchHelper;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070051
Alan Viveretted5c85c82016-03-29 11:44:32 -040052import libcore.icu.LocaleData;
53
Aurimas Liutikas99441c52016-10-11 16:48:32 -070054import java.text.NumberFormat;
55import java.util.Locale;
56
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070057/**
58 * A calendar-like view displaying a specified month and the appropriate selectable day numbers
59 * within the specified month.
60 */
61class SimpleMonthView extends View {
Alan Viverette0ef59ac2015-03-23 13:13:25 -070062 private static final int DAYS_IN_WEEK = 7;
63 private static final int MAX_WEEKS_IN_MONTH = 6;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070064
65 private static final int DEFAULT_SELECTED_DAY = -1;
66 private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070067
Alan Viverettee264f952016-03-04 13:18:48 -050068 private static final String MONTH_YEAR_FORMAT = "MMMMy";
Alan Viverettec5b95c22015-01-07 13:57:12 -080069
George Mounte998c3f2015-10-27 08:46:44 -070070 private static final int SELECTED_HIGHLIGHT_ALPHA = 0xB0;
71
Alan Viverette5dc973c2015-01-08 11:12:39 -080072 private final TextPaint mMonthPaint = new TextPaint();
73 private final TextPaint mDayOfWeekPaint = new TextPaint();
74 private final TextPaint mDayPaint = new TextPaint();
Alan Viverette0ef59ac2015-03-23 13:13:25 -070075 private final Paint mDaySelectorPaint = new Paint();
76 private final Paint mDayHighlightPaint = new Paint();
George Mounte998c3f2015-10-27 08:46:44 -070077 private final Paint mDayHighlightSelectorPaint = new Paint();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070078
Alan Viveretted5c85c82016-03-29 11:44:32 -040079 /** Array of single-character weekday labels ordered by column index. */
Alan Viverettee264f952016-03-04 13:18:48 -050080 private final String[] mDayOfWeekLabels = new String[7];
81
82 private final Calendar mCalendar;
83 private final Locale mLocale;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070084
85 private final MonthViewTouchHelper mTouchHelper;
86
Chet Haase1ea47152015-07-17 12:42:08 -070087 private final NumberFormat mDayFormatter;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -070088
Alan Viverettef63757b2015-04-01 17:14:45 -070089 // Desired dimensions.
90 private final int mDesiredMonthHeight;
91 private final int mDesiredDayOfWeekHeight;
92 private final int mDesiredDayHeight;
93 private final int mDesiredCellWidth;
94 private final int mDesiredDaySelectorRadius;
Alan Viverette60b674e2015-03-25 13:00:42 -070095
Alan Viverettee264f952016-03-04 13:18:48 -050096 private String mMonthYearLabel;
Alan Viverette0ef59ac2015-03-23 13:13:25 -070097
98 private int mMonth;
99 private int mYear;
100
Alan Viverettef63757b2015-04-01 17:14:45 -0700101 // Dimensions as laid out.
102 private int mMonthHeight;
103 private int mDayOfWeekHeight;
104 private int mDayHeight;
105 private int mCellWidth;
106 private int mDaySelectorRadius;
107
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700108 private int mPaddedWidth;
109 private int mPaddedHeight;
110
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700111 /** The day of month for the selected day, or -1 if no day is selected. */
112 private int mActivatedDay = -1;
113
114 /**
115 * The day of month for today, or -1 if the today is not in the current
116 * month.
117 */
118 private int mToday = DEFAULT_SELECTED_DAY;
119
Alan Viveretted5c85c82016-03-29 11:44:32 -0400120 /** The first day of the week (ex. Calendar.SUNDAY) indexed from one. */
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700121 private int mWeekStart = DEFAULT_WEEK_START;
122
123 /** The number of days (ex. 28) in the current month. */
124 private int mDaysInMonth;
125
126 /**
127 * The day of week (ex. Calendar.SUNDAY) for the first day of the current
128 * month.
129 */
130 private int mDayOfWeekStart;
131
132 /** The day of month for the first (inclusive) enabled day. */
133 private int mEnabledDayStart = 1;
134
135 /** The day of month for the last (inclusive) enabled day. */
136 private int mEnabledDayEnd = 31;
137
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700138 /** Optional listener for handling day click actions. */
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700139 private OnDayClickListener mOnDayClickListener;
140
Alan Viverettec5b95c22015-01-07 13:57:12 -0800141 private ColorStateList mDayTextColor;
142
George Mounte998c3f2015-10-27 08:46:44 -0700143 private int mHighlightedDay = -1;
144 private int mPreviouslyHighlightedDay = -1;
145 private boolean mIsTouchHighlighted = false;
Alan Viverette60b674e2015-03-25 13:00:42 -0700146
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700147 public SimpleMonthView(Context context) {
148 this(context, null);
149 }
150
151 public SimpleMonthView(Context context, AttributeSet attrs) {
152 this(context, attrs, R.attr.datePickerStyle);
153 }
154
Alan Viverette50eb0252014-10-24 14:34:26 -0700155 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
156 this(context, attrs, defStyleAttr, 0);
157 }
158
159 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
160 super(context, attrs, defStyleAttr, defStyleRes);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700161
162 final Resources res = context.getResources();
Alan Viverettef63757b2015-04-01 17:14:45 -0700163 mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
164 mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
165 mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
166 mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
Alan Viverette78bf1d32015-04-17 10:39:22 -0700167 mDesiredDaySelectorRadius = res.getDimensionPixelSize(
168 R.dimen.date_picker_day_selector_radius);
Alan Viverette60b674e2015-03-25 13:00:42 -0700169
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700170 // Set up accessibility components.
171 mTouchHelper = new MonthViewTouchHelper(this);
172 setAccessibilityDelegate(mTouchHelper);
173 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700174
Alan Viverettee264f952016-03-04 13:18:48 -0500175 mLocale = res.getConfiguration().locale;
176 mCalendar = Calendar.getInstance(mLocale);
177
178 mDayFormatter = NumberFormat.getIntegerInstance(mLocale);
179
180 updateMonthYearLabel();
181 updateDayOfWeekLabels();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700182
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700183 initPaints(res);
184 }
185
Alan Viverettee264f952016-03-04 13:18:48 -0500186 private void updateMonthYearLabel() {
187 final String format = DateFormat.getBestDateTimePattern(mLocale, MONTH_YEAR_FORMAT);
188 final SimpleDateFormat formatter = new SimpleDateFormat(format, mLocale);
Alan Viverettec3e5a822016-05-16 13:35:56 -0400189 formatter.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
Alan Viverettee264f952016-03-04 13:18:48 -0500190 mMonthYearLabel = formatter.format(mCalendar.getTime());
191 }
192
193 private void updateDayOfWeekLabels() {
Alan Viveretted5c85c82016-03-29 11:44:32 -0400194 // Use tiny (e.g. single-character) weekday names from ICU. The indices
195 // for this list correspond to Calendar days, e.g. SUNDAY is index 1.
196 final String[] tinyWeekdayNames = LocaleData.get(mLocale).tinyWeekdayNames;
197 for (int i = 0; i < DAYS_IN_WEEK; i++) {
198 mDayOfWeekLabels[i] = tinyWeekdayNames[(mWeekStart + i - 1) % DAYS_IN_WEEK + 1];
Alan Viverettee264f952016-03-04 13:18:48 -0500199 }
200 }
201
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700202 /**
203 * Applies the specified text appearance resource to a paint, returning the
204 * text color if one is set in the text appearance.
205 *
206 * @param p the paint to modify
207 * @param resId the resource ID of the text appearance
208 * @return the text color, if available
209 */
210 private ColorStateList applyTextAppearance(Paint p, int resId) {
211 final TypedArray ta = mContext.obtainStyledAttributes(null,
212 R.styleable.TextAppearance, 0, resId);
213
214 final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
215 if (fontFamily != null) {
216 p.setTypeface(Typeface.create(fontFamily, 0));
217 }
218
219 p.setTextSize(ta.getDimensionPixelSize(
220 R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
221
222 final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
223 if (textColor != null) {
224 final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
225 p.setColor(enabledColor);
226 }
227
228 ta.recycle();
229
230 return textColor;
231 }
232
Alan Viverette78bf1d32015-04-17 10:39:22 -0700233 public int getMonthHeight() {
234 return mMonthHeight;
235 }
236
237 public int getCellWidth() {
238 return mCellWidth;
239 }
240
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700241 public void setMonthTextAppearance(int resId) {
Alan Viverette78bf1d32015-04-17 10:39:22 -0700242 applyTextAppearance(mMonthPaint, resId);
Alan Viverette60b674e2015-03-25 13:00:42 -0700243
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700244 invalidate();
245 }
246
247 public void setDayOfWeekTextAppearance(int resId) {
248 applyTextAppearance(mDayOfWeekPaint, resId);
249 invalidate();
250 }
251
252 public void setDayTextAppearance(int resId) {
253 final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
254 if (textColor != null) {
255 mDayTextColor = textColor;
256 }
257
258 invalidate();
259 }
260
Alan Viverettec5b95c22015-01-07 13:57:12 -0800261 /**
262 * Sets up the text and style properties for painting.
263 */
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700264 private void initPaints(Resources res) {
265 final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
266 final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
267 final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
268
269 final int monthTextSize = res.getDimensionPixelSize(
270 R.dimen.date_picker_month_text_size);
271 final int dayOfWeekTextSize = res.getDimensionPixelSize(
272 R.dimen.date_picker_day_of_week_text_size);
273 final int dayTextSize = res.getDimensionPixelSize(
274 R.dimen.date_picker_day_text_size);
275
Alan Viverettec5b95c22015-01-07 13:57:12 -0800276 mMonthPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700277 mMonthPaint.setTextSize(monthTextSize);
278 mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800279 mMonthPaint.setTextAlign(Align.CENTER);
280 mMonthPaint.setStyle(Style.FILL);
281
282 mDayOfWeekPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700283 mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
284 mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800285 mDayOfWeekPaint.setTextAlign(Align.CENTER);
286 mDayOfWeekPaint.setStyle(Style.FILL);
287
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700288 mDaySelectorPaint.setAntiAlias(true);
289 mDaySelectorPaint.setStyle(Style.FILL);
290
291 mDayHighlightPaint.setAntiAlias(true);
292 mDayHighlightPaint.setStyle(Style.FILL);
Alan Viverettec5b95c22015-01-07 13:57:12 -0800293
George Mounte998c3f2015-10-27 08:46:44 -0700294 mDayHighlightSelectorPaint.setAntiAlias(true);
295 mDayHighlightSelectorPaint.setStyle(Style.FILL);
296
Alan Viverettec5b95c22015-01-07 13:57:12 -0800297 mDayPaint.setAntiAlias(true);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700298 mDayPaint.setTextSize(dayTextSize);
299 mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
Alan Viverettec5b95c22015-01-07 13:57:12 -0800300 mDayPaint.setTextAlign(Align.CENTER);
301 mDayPaint.setStyle(Style.FILL);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700302 }
303
Alan Viverettec5b95c22015-01-07 13:57:12 -0800304 void setMonthTextColor(ColorStateList monthTextColor) {
305 final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
306 mMonthPaint.setColor(enabledColor);
307 invalidate();
308 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700309
Alan Viverettec5b95c22015-01-07 13:57:12 -0800310 void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
311 final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
312 mDayOfWeekPaint.setColor(enabledColor);
313 invalidate();
314 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700315
Alan Viverettec5b95c22015-01-07 13:57:12 -0800316 void setDayTextColor(ColorStateList dayTextColor) {
317 mDayTextColor = dayTextColor;
318 invalidate();
319 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700320
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700321 void setDaySelectorColor(ColorStateList dayBackgroundColor) {
Alan Viverette5dc973c2015-01-08 11:12:39 -0800322 final int activatedColor = dayBackgroundColor.getColorForState(
Alan Viverettec5b95c22015-01-07 13:57:12 -0800323 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700324 mDaySelectorPaint.setColor(activatedColor);
George Mounte998c3f2015-10-27 08:46:44 -0700325 mDayHighlightSelectorPaint.setColor(activatedColor);
326 mDayHighlightSelectorPaint.setAlpha(SELECTED_HIGHLIGHT_ALPHA);
Alan Viverettec5b95c22015-01-07 13:57:12 -0800327 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700328 }
329
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700330 void setDayHighlightColor(ColorStateList dayHighlightColor) {
331 final int pressedColor = dayHighlightColor.getColorForState(
332 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
333 mDayHighlightPaint.setColor(pressedColor);
334 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700335 }
336
337 public void setOnDayClickListener(OnDayClickListener listener) {
338 mOnDayClickListener = listener;
339 }
340
341 @Override
342 public boolean dispatchHoverEvent(MotionEvent event) {
343 // First right-of-refusal goes the touch exploration helper.
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700344 return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700345 }
346
347 @Override
348 public boolean onTouchEvent(MotionEvent event) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700349 final int x = (int) (event.getX() + 0.5f);
350 final int y = (int) (event.getY() + 0.5f);
351
Alan Viveretteddf655c2015-04-22 13:43:31 -0700352 final int action = event.getAction();
353 switch (action) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700354 case MotionEvent.ACTION_DOWN:
355 case MotionEvent.ACTION_MOVE:
Alan Viverette78bf1d32015-04-17 10:39:22 -0700356 final int touchedItem = getDayAtLocation(x, y);
George Mounte998c3f2015-10-27 08:46:44 -0700357 mIsTouchHighlighted = true;
358 if (mHighlightedDay != touchedItem) {
359 mHighlightedDay = touchedItem;
360 mPreviouslyHighlightedDay = touchedItem;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700361 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700362 }
Alan Viveretteddf655c2015-04-22 13:43:31 -0700363 if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) {
364 // Touch something that's not an item, reject event.
365 return false;
366 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700367 break;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700368
369 case MotionEvent.ACTION_UP:
Alan Viverette78bf1d32015-04-17 10:39:22 -0700370 final int clickedDay = getDayAtLocation(x, y);
371 onDayClicked(clickedDay);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700372 // Fall through.
373 case MotionEvent.ACTION_CANCEL:
374 // Reset touched day on stream end.
George Mounte998c3f2015-10-27 08:46:44 -0700375 mHighlightedDay = -1;
376 mIsTouchHighlighted = false;
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
George Mounte998c3f2015-10-27 08:46:44 -0700384 public boolean onKeyDown(int keyCode, KeyEvent event) {
385 // We need to handle focus change within the SimpleMonthView because we are simulating
386 // multiple Views. The arrow keys will move between days until there is no space (no
387 // day to the left, top, right, or bottom). Focus forward and back jumps out of the
388 // SimpleMonthView, skipping over other SimpleMonthViews in the parent ViewPager
389 // to the next focusable View in the hierarchy.
390 boolean focusChanged = false;
391 switch (event.getKeyCode()) {
392 case KeyEvent.KEYCODE_DPAD_LEFT:
393 if (event.hasNoModifiers()) {
394 focusChanged = moveOneDay(isLayoutRtl());
395 }
396 break;
397 case KeyEvent.KEYCODE_DPAD_RIGHT:
398 if (event.hasNoModifiers()) {
399 focusChanged = moveOneDay(!isLayoutRtl());
400 }
401 break;
402 case KeyEvent.KEYCODE_DPAD_UP:
403 if (event.hasNoModifiers()) {
404 ensureFocusedDay();
405 if (mHighlightedDay > 7) {
406 mHighlightedDay -= 7;
407 focusChanged = true;
408 }
409 }
410 break;
411 case KeyEvent.KEYCODE_DPAD_DOWN:
412 if (event.hasNoModifiers()) {
413 ensureFocusedDay();
414 if (mHighlightedDay <= mDaysInMonth - 7) {
415 mHighlightedDay += 7;
416 focusChanged = true;
417 }
418 }
419 break;
420 case KeyEvent.KEYCODE_DPAD_CENTER:
421 case KeyEvent.KEYCODE_ENTER:
422 if (mHighlightedDay != -1) {
423 onDayClicked(mHighlightedDay);
424 return true;
425 }
426 break;
427 case KeyEvent.KEYCODE_TAB: {
428 int focusChangeDirection = 0;
429 if (event.hasNoModifiers()) {
430 focusChangeDirection = View.FOCUS_FORWARD;
431 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
432 focusChangeDirection = View.FOCUS_BACKWARD;
433 }
434 if (focusChangeDirection != 0) {
435 final ViewParent parent = getParent();
436 // move out of the ViewPager next/previous
437 View nextFocus = this;
438 do {
439 nextFocus = nextFocus.focusSearch(focusChangeDirection);
440 } while (nextFocus != null && nextFocus != this &&
441 nextFocus.getParent() == parent);
442 if (nextFocus != null) {
443 nextFocus.requestFocus();
444 return true;
445 }
446 }
447 break;
448 }
449 }
450 if (focusChanged) {
451 invalidate();
452 return true;
453 } else {
454 return super.onKeyDown(keyCode, event);
455 }
456 }
457
458 private boolean moveOneDay(boolean positive) {
459 ensureFocusedDay();
460 boolean focusChanged = false;
461 if (positive) {
462 if (!isLastDayOfWeek(mHighlightedDay) && mHighlightedDay < mDaysInMonth) {
463 mHighlightedDay++;
464 focusChanged = true;
465 }
466 } else {
467 if (!isFirstDayOfWeek(mHighlightedDay) && mHighlightedDay > 1) {
468 mHighlightedDay--;
469 focusChanged = true;
470 }
471 }
472 return focusChanged;
473 }
474
475 @Override
476 protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
477 @Nullable Rect previouslyFocusedRect) {
478 if (gainFocus) {
479 // If we've gained focus through arrow keys, we should find the day closest
480 // to the focus rect. If we've gained focus through forward/back, we should
481 // focus on the selected day if there is one.
482 final int offset = findDayOffset();
483 switch(direction) {
484 case View.FOCUS_RIGHT: {
485 int row = findClosestRow(previouslyFocusedRect);
486 mHighlightedDay = row == 0 ? 1 : (row * DAYS_IN_WEEK) - offset + 1;
487 break;
488 }
489 case View.FOCUS_LEFT: {
490 int row = findClosestRow(previouslyFocusedRect) + 1;
491 mHighlightedDay = Math.min(mDaysInMonth, (row * DAYS_IN_WEEK) - offset);
492 break;
493 }
494 case View.FOCUS_DOWN: {
495 final int col = findClosestColumn(previouslyFocusedRect);
496 final int day = col - offset + 1;
497 mHighlightedDay = day < 1 ? day + DAYS_IN_WEEK : day;
498 break;
499 }
500 case View.FOCUS_UP: {
501 final int col = findClosestColumn(previouslyFocusedRect);
502 final int maxWeeks = (offset + mDaysInMonth) / DAYS_IN_WEEK;
503 final int day = col - offset + (DAYS_IN_WEEK * maxWeeks) + 1;
504 mHighlightedDay = day > mDaysInMonth ? day - DAYS_IN_WEEK : day;
505 break;
506 }
507 }
508 ensureFocusedDay();
509 invalidate();
510 }
511 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
512 }
513
514 /**
515 * Returns the row (0 indexed) closest to previouslyFocusedRect or center if null.
516 */
517 private int findClosestRow(@Nullable Rect previouslyFocusedRect) {
518 if (previouslyFocusedRect == null) {
519 return 3;
George Mount45362a62017-08-04 10:17:36 -0700520 } else if (mDayHeight == 0) {
521 return 0; // There hasn't been a layout, so just choose the first row
George Mounte998c3f2015-10-27 08:46:44 -0700522 } else {
523 int centerY = previouslyFocusedRect.centerY();
524
525 final TextPaint p = mDayPaint;
526 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
527 final int rowHeight = mDayHeight;
528
529 // Text is vertically centered within the row height.
530 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
531 final int rowCenter = headerHeight + rowHeight / 2;
532
533 centerY -= rowCenter - halfLineHeight;
534 int row = Math.round(centerY / (float) rowHeight);
535 final int maxDay = findDayOffset() + mDaysInMonth;
536 final int maxRows = (maxDay / DAYS_IN_WEEK) - ((maxDay % DAYS_IN_WEEK == 0) ? 1 : 0);
537
538 row = MathUtils.constrain(row, 0, maxRows);
539 return row;
540 }
541 }
542
543 /**
544 * Returns the column (0 indexed) closest to the previouslyFocusedRect or center if null.
545 * The 0 index is related to the first day of the week.
546 */
547 private int findClosestColumn(@Nullable Rect previouslyFocusedRect) {
548 if (previouslyFocusedRect == null) {
549 return DAYS_IN_WEEK / 2;
George Mount45362a62017-08-04 10:17:36 -0700550 } else if (mCellWidth == 0) {
551 return 0; // There hasn't been a layout, so we can just choose the first column
George Mounte998c3f2015-10-27 08:46:44 -0700552 } else {
553 int centerX = previouslyFocusedRect.centerX() - mPaddingLeft;
554 final int columnFromLeft =
555 MathUtils.constrain(centerX / mCellWidth, 0, DAYS_IN_WEEK - 1);
556 return isLayoutRtl() ? DAYS_IN_WEEK - columnFromLeft - 1: columnFromLeft;
557 }
558 }
559
560 @Override
561 public void getFocusedRect(Rect r) {
562 if (mHighlightedDay > 0) {
563 getBoundsForDay(mHighlightedDay, r);
564 } else {
565 super.getFocusedRect(r);
566 }
567 }
568
569 @Override
570 protected void onFocusLost() {
571 if (!mIsTouchHighlighted) {
572 // Unhighlight a day.
573 mPreviouslyHighlightedDay = mHighlightedDay;
574 mHighlightedDay = -1;
575 invalidate();
576 }
577 super.onFocusLost();
578 }
579
580 /**
581 * Ensure some day is highlighted. If a day isn't highlighted, it chooses the selected day,
582 * if possible, or the first day of the month if not.
583 */
584 private void ensureFocusedDay() {
585 if (mHighlightedDay != -1) {
586 return;
587 }
588 if (mPreviouslyHighlightedDay != -1) {
589 mHighlightedDay = mPreviouslyHighlightedDay;
590 return;
591 }
592 if (mActivatedDay != -1) {
593 mHighlightedDay = mActivatedDay;
594 return;
595 }
596 mHighlightedDay = 1;
597 }
598
599 private boolean isFirstDayOfWeek(int day) {
600 final int offset = findDayOffset();
601 return (offset + day - 1) % DAYS_IN_WEEK == 0;
602 }
603
604 private boolean isLastDayOfWeek(int day) {
605 final int offset = findDayOffset();
606 return (offset + day) % DAYS_IN_WEEK == 0;
607 }
608
609 @Override
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700610 protected void onDraw(Canvas canvas) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700611 final int paddingLeft = getPaddingLeft();
612 final int paddingTop = getPaddingTop();
613 canvas.translate(paddingLeft, paddingTop);
614
615 drawMonth(canvas);
616 drawDaysOfWeek(canvas);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700617 drawDays(canvas);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700618
619 canvas.translate(-paddingLeft, -paddingTop);
620 }
621
622 private void drawMonth(Canvas canvas) {
623 final float x = mPaddedWidth / 2f;
624
625 // Vertically centered within the month header height.
626 final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
627 final float y = (mMonthHeight - lineHeight) / 2f;
628
Alan Viverettee264f952016-03-04 13:18:48 -0500629 canvas.drawText(mMonthYearLabel, x, y, mMonthPaint);
630 }
631
632 public String getMonthYearLabel() {
633 return mMonthYearLabel;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700634 }
635
636 private void drawDaysOfWeek(Canvas canvas) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700637 final TextPaint p = mDayOfWeekPaint;
638 final int headerHeight = mMonthHeight;
639 final int rowHeight = mDayOfWeekHeight;
Alan Viverettef63757b2015-04-01 17:14:45 -0700640 final int colWidth = mCellWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700641
Alan Viverette60b674e2015-03-25 13:00:42 -0700642 // Text is vertically centered within the day of week height.
643 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
644 final int rowCenter = headerHeight + rowHeight / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700645
Alan Viverette60b674e2015-03-25 13:00:42 -0700646 for (int col = 0; col < DAYS_IN_WEEK; col++) {
647 final int colCenter = colWidth * col + colWidth / 2;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700648 final int colCenterRtl;
649 if (isLayoutRtl()) {
650 colCenterRtl = mPaddedWidth - colCenter;
651 } else {
652 colCenterRtl = colCenter;
653 }
654
Alan Viveretted5c85c82016-03-29 11:44:32 -0400655 final String label = mDayOfWeekLabels[col];
Alan Viveretteddf655c2015-04-22 13:43:31 -0700656 canvas.drawText(label, colCenterRtl, rowCenter - halfLineHeight, p);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700657 }
658 }
659
660 /**
661 * Draws the month days.
662 */
663 private void drawDays(Canvas canvas) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700664 final TextPaint p = mDayPaint;
665 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
666 final int rowHeight = mDayHeight;
Alan Viverettef63757b2015-04-01 17:14:45 -0700667 final int colWidth = mCellWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700668
Alan Viverette60b674e2015-03-25 13:00:42 -0700669 // Text is vertically centered within the row height.
670 final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
671 int rowCenter = headerHeight + rowHeight / 2;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700672
Alan Viverette60b674e2015-03-25 13:00:42 -0700673 for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) {
674 final int colCenter = colWidth * col + colWidth / 2;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700675 final int colCenterRtl;
676 if (isLayoutRtl()) {
677 colCenterRtl = mPaddedWidth - colCenter;
678 } else {
679 colCenterRtl = colCenter;
680 }
681
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700682 int stateMask = 0;
683
Alan Viverette5c339492015-04-28 14:07:36 -0700684 final boolean isDayEnabled = isDayEnabled(day);
685 if (isDayEnabled) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700686 stateMask |= StateSet.VIEW_STATE_ENABLED;
687 }
688
689 final boolean isDayActivated = mActivatedDay == day;
George Mounte998c3f2015-10-27 08:46:44 -0700690 final boolean isDayHighlighted = mHighlightedDay == day;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700691 if (isDayActivated) {
692 stateMask |= StateSet.VIEW_STATE_ACTIVATED;
693
694 // Adjust the circle to be centered on the row.
George Mounte998c3f2015-10-27 08:46:44 -0700695 final Paint paint = isDayHighlighted ? mDayHighlightSelectorPaint :
696 mDaySelectorPaint;
697 canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, paint);
698 } else if (isDayHighlighted) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700699 stateMask |= StateSet.VIEW_STATE_PRESSED;
700
Alan Viverette5c339492015-04-28 14:07:36 -0700701 if (isDayEnabled) {
702 // Adjust the circle to be centered on the row.
703 canvas.drawCircle(colCenterRtl, rowCenter,
704 mDaySelectorRadius, mDayHighlightPaint);
705 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700706 }
707
708 final boolean isDayToday = mToday == day;
709 final int dayTextColor;
710 if (isDayToday && !isDayActivated) {
711 dayTextColor = mDaySelectorPaint.getColor();
712 } else {
713 final int[] stateSet = StateSet.get(stateMask);
714 dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
715 }
Alan Viverette60b674e2015-03-25 13:00:42 -0700716 p.setColor(dayTextColor);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700717
Chet Haase1ea47152015-07-17 12:42:08 -0700718 canvas.drawText(mDayFormatter.format(day), colCenterRtl, rowCenter - halfLineHeight, p);
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700719
Alan Viverette60b674e2015-03-25 13:00:42 -0700720 col++;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700721
Alan Viverette60b674e2015-03-25 13:00:42 -0700722 if (col == DAYS_IN_WEEK) {
723 col = 0;
724 rowCenter += rowHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700725 }
726 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700727 }
728
Alan Viverette5c339492015-04-28 14:07:36 -0700729 private boolean isDayEnabled(int day) {
730 return day >= mEnabledDayStart && day <= mEnabledDayEnd;
731 }
732
733 private boolean isValidDayOfMonth(int day) {
734 return day >= 1 && day <= mDaysInMonth;
735 }
736
Alan Viverette518ff0d2014-08-15 14:20:35 -0700737 private static boolean isValidDayOfWeek(int day) {
Alan Viverette58780762014-09-10 17:09:13 -0700738 return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
739 }
740
741 private static boolean isValidMonth(int month) {
742 return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700743 }
744
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700745 /**
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700746 * Sets the selected day.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700747 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700748 * @param dayOfMonth the selected day of the month, or {@code -1} to clear
749 * the selection
750 */
751 public void setSelectedDay(int dayOfMonth) {
752 mActivatedDay = dayOfMonth;
753
754 // Invalidate cached accessibility information.
755 mTouchHelper.invalidateRoot();
756 invalidate();
757 }
758
759 /**
760 * Sets the first day of the week.
761 *
762 * @param weekStart which day the week should start on, valid values are
763 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
764 */
765 public void setFirstDayOfWeek(int weekStart) {
766 if (isValidDayOfWeek(weekStart)) {
767 mWeekStart = weekStart;
768 } else {
769 mWeekStart = mCalendar.getFirstDayOfWeek();
770 }
771
Alan Viverettee264f952016-03-04 13:18:48 -0500772 updateDayOfWeekLabels();
773
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700774 // Invalidate cached accessibility information.
775 mTouchHelper.invalidateRoot();
776 invalidate();
777 }
778
779 /**
780 * Sets all the parameters for displaying this week.
781 * <p>
782 * Parameters have a default value and will only update if a new value is
783 * included, except for focus month, which will always default to no focus
784 * month if no value is passed in. The only required parameter is the week
785 * start.
786 *
787 * @param selectedDay the selected day of the month, or -1 for no selection
788 * @param month the month
789 * @param year the year
790 * @param weekStart which day the week should start on, valid values are
791 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
792 * @param enabledDayStart the first enabled day
793 * @param enabledDayEnd the last enabled day
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700794 */
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700795 void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
796 int enabledDayEnd) {
Alan Viverettec5b95c22015-01-07 13:57:12 -0800797 mActivatedDay = selectedDay;
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700798
Alan Viverette58780762014-09-10 17:09:13 -0700799 if (isValidMonth(month)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700800 mMonth = month;
801 }
802 mYear = year;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700803
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700804 mCalendar.set(Calendar.MONTH, mMonth);
805 mCalendar.set(Calendar.YEAR, mYear);
806 mCalendar.set(Calendar.DAY_OF_MONTH, 1);
807 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
808
Alan Viverette518ff0d2014-08-15 14:20:35 -0700809 if (isValidDayOfWeek(weekStart)) {
Fabrice Di Meglio75b12152014-07-25 15:26:18 -0700810 mWeekStart = weekStart;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700811 } else {
812 mWeekStart = mCalendar.getFirstDayOfWeek();
813 }
814
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700815 // Figure out what day today is.
816 final Calendar today = Calendar.getInstance();
817 mToday = -1;
818 mDaysInMonth = getDaysInMonth(mMonth, mYear);
819 for (int i = 0; i < mDaysInMonth; i++) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700820 final int day = i + 1;
821 if (sameDay(day, today)) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700822 mToday = day;
823 }
824 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700825
Alan Viverette5c339492015-04-28 14:07:36 -0700826 mEnabledDayStart = MathUtils.constrain(enabledDayStart, 1, mDaysInMonth);
827 mEnabledDayEnd = MathUtils.constrain(enabledDayEnd, mEnabledDayStart, mDaysInMonth);
828
Alan Viverette08dbbcd2016-04-14 10:15:04 -0400829 updateMonthYearLabel();
830 updateDayOfWeekLabels();
831
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700832 // Invalidate cached accessibility information.
833 mTouchHelper.invalidateRoot();
Alan Viverette08dbbcd2016-04-14 10:15:04 -0400834 invalidate();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700835 }
836
837 private static int getDaysInMonth(int month, int year) {
838 switch (month) {
839 case Calendar.JANUARY:
840 case Calendar.MARCH:
841 case Calendar.MAY:
842 case Calendar.JULY:
843 case Calendar.AUGUST:
844 case Calendar.OCTOBER:
845 case Calendar.DECEMBER:
846 return 31;
847 case Calendar.APRIL:
848 case Calendar.JUNE:
849 case Calendar.SEPTEMBER:
850 case Calendar.NOVEMBER:
851 return 30;
852 case Calendar.FEBRUARY:
853 return (year % 4 == 0) ? 29 : 28;
854 default:
855 throw new IllegalArgumentException("Invalid Month");
856 }
857 }
858
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700859 private boolean sameDay(int day, Calendar today) {
860 return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
861 && day == today.get(Calendar.DAY_OF_MONTH);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700862 }
863
864 @Override
865 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Alan Viverette816aa142015-04-10 15:41:10 -0700866 final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH
867 + mDesiredDayOfWeekHeight + mDesiredMonthHeight
868 + getPaddingTop() + getPaddingBottom();
Alan Viverettef63757b2015-04-01 17:14:45 -0700869 final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700870 + getPaddingStart() + getPaddingEnd();
871 final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
872 final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
873 setMeasuredDimension(resolvedWidth, resolvedHeight);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700874 }
875
876 @Override
Alan Viveretteddf655c2015-04-22 13:43:31 -0700877 public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
878 super.onRtlPropertiesChanged(layoutDirection);
879
880 requestLayout();
881 }
882
883 @Override
Alan Viverettef63757b2015-04-01 17:14:45 -0700884 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
885 if (!changed) {
886 return;
887 }
Alan Viverette60b674e2015-03-25 13:00:42 -0700888
Alan Viverettef63757b2015-04-01 17:14:45 -0700889 // Let's initialize a completely reasonable number of variables.
890 final int w = right - left;
891 final int h = bottom - top;
892 final int paddingLeft = getPaddingLeft();
893 final int paddingTop = getPaddingTop();
894 final int paddingRight = getPaddingRight();
895 final int paddingBottom = getPaddingBottom();
896 final int paddedRight = w - paddingRight;
897 final int paddedBottom = h - paddingBottom;
898 final int paddedWidth = paddedRight - paddingLeft;
899 final int paddedHeight = paddedBottom - paddingTop;
900 if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) {
901 return;
902 }
903
904 mPaddedWidth = paddedWidth;
905 mPaddedHeight = paddedHeight;
906
907 // We may have been laid out smaller than our preferred size. If so,
908 // scale all dimensions to fit.
909 final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom;
910 final float scaleH = paddedHeight / (float) measuredPaddedHeight;
911 final int monthHeight = (int) (mDesiredMonthHeight * scaleH);
Alan Viverette60b674e2015-03-25 13:00:42 -0700912 final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
Alan Viverettef63757b2015-04-01 17:14:45 -0700913 mMonthHeight = monthHeight;
914 mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH);
915 mDayHeight = (int) (mDesiredDayHeight * scaleH);
916 mCellWidth = cellWidth;
917
918 // Compute the largest day selector radius that's still within the clip
919 // bounds and desired selector radius.
920 final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
921 final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
922 mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
923 Math.min(maxSelectorWidth, maxSelectorHeight));
Alan Viverette60b674e2015-03-25 13:00:42 -0700924
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700925 // Invalidate cached accessibility information.
926 mTouchHelper.invalidateRoot();
927 }
928
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700929 private int findDayOffset() {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700930 final int offset = mDayOfWeekStart - mWeekStart;
931 if (mDayOfWeekStart < mWeekStart) {
932 return offset + DAYS_IN_WEEK;
933 }
934 return offset;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700935 }
936
937 /**
Alan Viverette78bf1d32015-04-17 10:39:22 -0700938 * Calculates the day of the month at the specified touch position. Returns
939 * the day of the month or -1 if the position wasn't in a valid day.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700940 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700941 * @param x the x position of the touch event
942 * @param y the y position of the touch event
Alan Viverette78bf1d32015-04-17 10:39:22 -0700943 * @return the day of the month at (x, y), or -1 if the position wasn't in
944 * a valid day
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700945 */
Alan Viverette78bf1d32015-04-17 10:39:22 -0700946 private int getDayAtLocation(int x, int y) {
Alan Viverette60b674e2015-03-25 13:00:42 -0700947 final int paddedX = x - getPaddingLeft();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700948 if (paddedX < 0 || paddedX >= mPaddedWidth) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700949 return -1;
950 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700951
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700952 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
Alan Viverette60b674e2015-03-25 13:00:42 -0700953 final int paddedY = y - getPaddingTop();
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700954 if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700955 return -1;
956 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700957
Alan Viveretteddf655c2015-04-22 13:43:31 -0700958 // Adjust for RTL after applying padding.
959 final int paddedXRtl;
960 if (isLayoutRtl()) {
961 paddedXRtl = mPaddedWidth - paddedX;
962 } else {
963 paddedXRtl = paddedX;
964 }
965
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700966 final int row = (paddedY - headerHeight) / mDayHeight;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700967 final int col = (paddedXRtl * DAYS_IN_WEEK) / mPaddedWidth;
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700968 final int index = col + row * DAYS_IN_WEEK;
969 final int day = index + 1 - findDayOffset();
Alan Viverette5c339492015-04-28 14:07:36 -0700970 if (!isValidDayOfMonth(day)) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700971 return -1;
972 }
973
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -0700974 return day;
975 }
976
977 /**
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700978 * Calculates the bounds of the specified day.
979 *
Alan Viverette78bf1d32015-04-17 10:39:22 -0700980 * @param id the day of the month
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700981 * @param outBounds the rect to populate with bounds
982 */
Kirill Grouchnikov698b7512016-04-11 17:16:52 -0400983 public boolean getBoundsForDay(int id, Rect outBounds) {
Alan Viverette5c339492015-04-28 14:07:36 -0700984 if (!isValidDayOfMonth(id)) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700985 return false;
986 }
987
Alan Viverette60b674e2015-03-25 13:00:42 -0700988 final int index = id - 1 + findDayOffset();
989
Alan Viveretteddf655c2015-04-22 13:43:31 -0700990 // Compute left edge, taking into account RTL.
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700991 final int col = index % DAYS_IN_WEEK;
Alan Viverettef63757b2015-04-01 17:14:45 -0700992 final int colWidth = mCellWidth;
Alan Viveretteddf655c2015-04-22 13:43:31 -0700993 final int left;
994 if (isLayoutRtl()) {
995 left = getWidth() - getPaddingRight() - (col + 1) * colWidth;
996 } else {
997 left = getPaddingLeft() + col * colWidth;
998 }
Alan Viverette0ef59ac2015-03-23 13:13:25 -0700999
Alan Viverette60b674e2015-03-25 13:00:42 -07001000 // Compute top edge.
1001 final int row = index / DAYS_IN_WEEK;
1002 final int rowHeight = mDayHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001003 final int headerHeight = mMonthHeight + mDayOfWeekHeight;
Alan Viverette60b674e2015-03-25 13:00:42 -07001004 final int top = getPaddingTop() + headerHeight + row * rowHeight;
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001005
Alan Viverette60b674e2015-03-25 13:00:42 -07001006 outBounds.set(left, top, left + colWidth, top + rowHeight);
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001007
Alan Viverette78bf1d32015-04-17 10:39:22 -07001008 return true;
Alan Viverette60b674e2015-03-25 13:00:42 -07001009 }
1010
1011 /**
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001012 * Called when the user clicks on a day. Handles callbacks to the
1013 * {@link OnDayClickListener} if one is set.
1014 *
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001015 * @param day the day that was clicked
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001016 */
Alan Viverette60b674e2015-03-25 13:00:42 -07001017 private boolean onDayClicked(int day) {
Alan Viverette5c339492015-04-28 14:07:36 -07001018 if (!isValidDayOfMonth(day) || !isDayEnabled(day)) {
Alan Viverette60b674e2015-03-25 13:00:42 -07001019 return false;
1020 }
1021
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001022 if (mOnDayClickListener != null) {
Alan Viverette60b674e2015-03-25 13:00:42 -07001023 final Calendar date = Calendar.getInstance();
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001024 date.set(mYear, mMonth, day);
1025 mOnDayClickListener.onDayClick(this, date);
1026 }
1027
1028 // This is a no-op if accessibility is turned off.
1029 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
Alan Viverette60b674e2015-03-25 13:00:42 -07001030 return true;
1031 }
1032
Vladislav Kaznacheev47f333a2016-09-21 11:37:08 -07001033 @Override
1034 public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
1035 if (!isEnabled()) {
1036 return null;
1037 }
1038 // Add 0.5f to event coordinates to match the logic in onTouchEvent.
1039 final int x = (int) (event.getX() + 0.5f);
1040 final int y = (int) (event.getY() + 0.5f);
1041 final int dayUnderPointer = getDayAtLocation(x, y);
1042 if (dayUnderPointer >= 0) {
1043 return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
1044 }
1045 return super.onResolvePointerIcon(event, pointerIndex);
1046 }
1047
Alan Viverette60b674e2015-03-25 13:00:42 -07001048 /**
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001049 * Provides a virtual view hierarchy for interfacing with an accessibility
1050 * service.
1051 */
1052 private class MonthViewTouchHelper extends ExploreByTouchHelper {
1053 private static final String DATE_FORMAT = "dd MMMM yyyy";
1054
1055 private final Rect mTempRect = new Rect();
1056 private final Calendar mTempCalendar = Calendar.getInstance();
1057
1058 public MonthViewTouchHelper(View host) {
1059 super(host);
1060 }
1061
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001062 @Override
1063 protected int getVirtualViewAt(float x, float y) {
Alan Viverette78bf1d32015-04-17 10:39:22 -07001064 final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f));
Alan Viverette5c339492015-04-28 14:07:36 -07001065 if (day != -1) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001066 return day;
1067 }
1068 return ExploreByTouchHelper.INVALID_ID;
1069 }
1070
1071 @Override
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001072 protected void getVisibleVirtualViews(IntArray virtualViewIds) {
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001073 for (int day = 1; day <= mDaysInMonth; day++) {
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001074 virtualViewIds.add(day);
1075 }
1076 }
1077
1078 @Override
1079 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
Alan Viverette78bf1d32015-04-17 10:39:22 -07001080 event.setContentDescription(getDayDescription(virtualViewId));
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001081 }
1082
1083 @Override
1084 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
Alan Viverette78bf1d32015-04-17 10:39:22 -07001085 final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect);
Alan Viverette0ef59ac2015-03-23 13:13:25 -07001086
1087 if (!hasBounds) {
1088 // The day is invalid, kill the node.
1089 mTempRect.setEmpty();
1090 node.setContentDescription("");
1091 node.setBoundsInParent(mTempRect);
1092 node.setVisibleToUser(false);
1093 return;
1094 }
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001095
Alan Viverette78bf1d32015-04-17 10:39:22 -07001096 node.setText(getDayText(virtualViewId));
1097 node.setContentDescription(getDayDescription(virtualViewId));
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001098 node.setBoundsInParent(mTempRect);
Alan Viverette5c339492015-04-28 14:07:36 -07001099
1100 final boolean isDayEnabled = isDayEnabled(virtualViewId);
1101 if (isDayEnabled) {
1102 node.addAction(AccessibilityAction.ACTION_CLICK);
1103 }
1104
1105 node.setEnabled(isDayEnabled);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001106
Alan Viverettec5b95c22015-01-07 13:57:12 -08001107 if (virtualViewId == mActivatedDay) {
Alan Viverette60b674e2015-03-25 13:00:42 -07001108 // TODO: This should use activated once that's supported.
1109 node.setChecked(true);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001110 }
1111
1112 }
1113
1114 @Override
1115 protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
1116 Bundle arguments) {
1117 switch (action) {
1118 case AccessibilityNodeInfo.ACTION_CLICK:
Alan Viverette78bf1d32015-04-17 10:39:22 -07001119 return onDayClicked(virtualViewId);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001120 }
1121
1122 return false;
1123 }
1124
1125 /**
Alan Viverette60b674e2015-03-25 13:00:42 -07001126 * Generates a description for a given virtual view.
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001127 *
Alan Viverette78bf1d32015-04-17 10:39:22 -07001128 * @param id the day to generate a description for
Alan Viverette60b674e2015-03-25 13:00:42 -07001129 * @return a description of the virtual view
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001130 */
Alan Viverette78bf1d32015-04-17 10:39:22 -07001131 private CharSequence getDayDescription(int id) {
Alan Viverette5c339492015-04-28 14:07:36 -07001132 if (isValidDayOfMonth(id)) {
Alan Viverette60b674e2015-03-25 13:00:42 -07001133 mTempCalendar.set(mYear, mMonth, id);
1134 return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001135 }
1136
Alan Viverette60b674e2015-03-25 13:00:42 -07001137 return "";
1138 }
1139
1140 /**
1141 * Generates displayed text for a given virtual view.
1142 *
Alan Viverette78bf1d32015-04-17 10:39:22 -07001143 * @param id the day to generate text for
Alan Viverette60b674e2015-03-25 13:00:42 -07001144 * @return the visible text of the virtual view
1145 */
Alan Viverette78bf1d32015-04-17 10:39:22 -07001146 private CharSequence getDayText(int id) {
Alan Viverette5c339492015-04-28 14:07:36 -07001147 if (isValidDayOfMonth(id)) {
Chet Haase1ea47152015-07-17 12:42:08 -07001148 return mDayFormatter.format(id);
Alan Viverette60b674e2015-03-25 13:00:42 -07001149 }
1150
1151 return null;
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001152 }
1153 }
1154
1155 /**
1156 * Handles callbacks when the user clicks on a time object.
1157 */
1158 public interface OnDayClickListener {
Alan Viverette78bf1d32015-04-17 10:39:22 -07001159 void onDayClick(SimpleMonthView view, Calendar day);
Fabrice Di Megliobd9152f2013-10-01 11:21:31 -07001160 }
1161}