/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.widget;

import android.annotation.Nullable;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.icu.text.DisplayContext;
import android.icu.text.SimpleDateFormat;
import android.icu.util.Calendar;
import android.os.Bundle;
import android.text.TextPaint;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.IntArray;
import android.util.MathUtils;
import android.util.StateSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;

import com.android.internal.R;
import com.android.internal.widget.ExploreByTouchHelper;

import libcore.icu.LocaleData;

import java.text.NumberFormat;
import java.util.Locale;

A calendar-like view displaying a specified month and the appropriate selectable day numbers within the specified month.
/** * A calendar-like view displaying a specified month and the appropriate selectable day numbers * within the specified month. */
class SimpleMonthView extends View { private static final int DAYS_IN_WEEK = 7; private static final int MAX_WEEKS_IN_MONTH = 6; private static final int DEFAULT_SELECTED_DAY = -1; private static final int DEFAULT_WEEK_START = Calendar.SUNDAY; private static final String MONTH_YEAR_FORMAT = "MMMMy"; private static final int SELECTED_HIGHLIGHT_ALPHA = 0xB0; private final TextPaint mMonthPaint = new TextPaint(); private final TextPaint mDayOfWeekPaint = new TextPaint(); private final TextPaint mDayPaint = new TextPaint(); private final Paint mDaySelectorPaint = new Paint(); private final Paint mDayHighlightPaint = new Paint(); private final Paint mDayHighlightSelectorPaint = new Paint();
Array of single-character weekday labels ordered by column index.
/** Array of single-character weekday labels ordered by column index. */
private final String[] mDayOfWeekLabels = new String[7]; private final Calendar mCalendar; private final Locale mLocale; private final MonthViewTouchHelper mTouchHelper; private final NumberFormat mDayFormatter; // Desired dimensions. private final int mDesiredMonthHeight; private final int mDesiredDayOfWeekHeight; private final int mDesiredDayHeight; private final int mDesiredCellWidth; private final int mDesiredDaySelectorRadius; private String mMonthYearLabel; private int mMonth; private int mYear; // Dimensions as laid out. private int mMonthHeight; private int mDayOfWeekHeight; private int mDayHeight; private int mCellWidth; private int mDaySelectorRadius; private int mPaddedWidth; private int mPaddedHeight;
The day of month for the selected day, or -1 if no day is selected.
/** The day of month for the selected day, or -1 if no day is selected. */
private int mActivatedDay = -1;
The day of month for today, or -1 if the today is not in the current month.
/** * The day of month for today, or -1 if the today is not in the current * month. */
private int mToday = DEFAULT_SELECTED_DAY;
The first day of the week (ex. Calendar.SUNDAY) indexed from one.
/** The first day of the week (ex. Calendar.SUNDAY) indexed from one. */
private int mWeekStart = DEFAULT_WEEK_START;
The number of days (ex. 28) in the current month.
/** The number of days (ex. 28) in the current month. */
private int mDaysInMonth;
The day of week (ex. Calendar.SUNDAY) for the first day of the current month.
/** * The day of week (ex. Calendar.SUNDAY) for the first day of the current * month. */
private int mDayOfWeekStart;
The day of month for the first (inclusive) enabled day.
/** The day of month for the first (inclusive) enabled day. */
private int mEnabledDayStart = 1;
The day of month for the last (inclusive) enabled day.
/** The day of month for the last (inclusive) enabled day. */
private int mEnabledDayEnd = 31;
Optional listener for handling day click actions.
/** Optional listener for handling day click actions. */
private OnDayClickListener mOnDayClickListener; private ColorStateList mDayTextColor; private int mHighlightedDay = -1; private int mPreviouslyHighlightedDay = -1; private boolean mIsTouchHighlighted = false; public SimpleMonthView(Context context) { this(context, null); } public SimpleMonthView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.datePickerStyle); } public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); final Resources res = context.getResources(); mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height); mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height); mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height); mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width); mDesiredDaySelectorRadius = res.getDimensionPixelSize( R.dimen.date_picker_day_selector_radius); // Set up accessibility components. mTouchHelper = new MonthViewTouchHelper(this); setAccessibilityDelegate(mTouchHelper); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); mLocale = res.getConfiguration().locale; mCalendar = Calendar.getInstance(mLocale); mDayFormatter = NumberFormat.getIntegerInstance(mLocale); updateMonthYearLabel(); updateDayOfWeekLabels(); initPaints(res); } private void updateMonthYearLabel() { final String format = DateFormat.getBestDateTimePattern(mLocale, MONTH_YEAR_FORMAT); final SimpleDateFormat formatter = new SimpleDateFormat(format, mLocale); formatter.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE); mMonthYearLabel = formatter.format(mCalendar.getTime()); } private void updateDayOfWeekLabels() { // Use tiny (e.g. single-character) weekday names from ICU. The indices // for this list correspond to Calendar days, e.g. SUNDAY is index 1. final String[] tinyWeekdayNames = LocaleData.get(mLocale).tinyWeekdayNames; for (int i = 0; i < DAYS_IN_WEEK; i++) { mDayOfWeekLabels[i] = tinyWeekdayNames[(mWeekStart + i - 1) % DAYS_IN_WEEK + 1]; } }
Applies the specified text appearance resource to a paint, returning the text color if one is set in the text appearance.
Params:
  • p – the paint to modify
  • resId – the resource ID of the text appearance
Returns:the text color, if available
/** * Applies the specified text appearance resource to a paint, returning the * text color if one is set in the text appearance. * * @param p the paint to modify * @param resId the resource ID of the text appearance * @return the text color, if available */
private ColorStateList applyTextAppearance(Paint p, int resId) { final TypedArray ta = mContext.obtainStyledAttributes(null, R.styleable.TextAppearance, 0, resId); final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily); if (fontFamily != null) { p.setTypeface(Typeface.create(fontFamily, 0)); } p.setTextSize(ta.getDimensionPixelSize( R.styleable.TextAppearance_textSize, (int) p.getTextSize())); final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor); if (textColor != null) { final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0); p.setColor(enabledColor); } ta.recycle(); return textColor; } public int getMonthHeight() { return mMonthHeight; } public int getCellWidth() { return mCellWidth; } public void setMonthTextAppearance(int resId) { applyTextAppearance(mMonthPaint, resId); invalidate(); } public void setDayOfWeekTextAppearance(int resId) { applyTextAppearance(mDayOfWeekPaint, resId); invalidate(); } public void setDayTextAppearance(int resId) { final ColorStateList textColor = applyTextAppearance(mDayPaint, resId); if (textColor != null) { mDayTextColor = textColor; } invalidate(); }
Sets up the text and style properties for painting.
/** * Sets up the text and style properties for painting. */
private void initPaints(Resources res) { final String monthTypeface = res.getString(R.string.date_picker_month_typeface); final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface); final String dayTypeface = res.getString(R.string.date_picker_day_typeface); final int monthTextSize = res.getDimensionPixelSize( R.dimen.date_picker_month_text_size); final int dayOfWeekTextSize = res.getDimensionPixelSize( R.dimen.date_picker_day_of_week_text_size); final int dayTextSize = res.getDimensionPixelSize( R.dimen.date_picker_day_text_size); mMonthPaint.setAntiAlias(true); mMonthPaint.setTextSize(monthTextSize); mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0)); mMonthPaint.setTextAlign(Align.CENTER); mMonthPaint.setStyle(Style.FILL); mDayOfWeekPaint.setAntiAlias(true); mDayOfWeekPaint.setTextSize(dayOfWeekTextSize); mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0)); mDayOfWeekPaint.setTextAlign(Align.CENTER); mDayOfWeekPaint.setStyle(Style.FILL); mDaySelectorPaint.setAntiAlias(true); mDaySelectorPaint.setStyle(Style.FILL); mDayHighlightPaint.setAntiAlias(true); mDayHighlightPaint.setStyle(Style.FILL); mDayHighlightSelectorPaint.setAntiAlias(true); mDayHighlightSelectorPaint.setStyle(Style.FILL); mDayPaint.setAntiAlias(true); mDayPaint.setTextSize(dayTextSize); mDayPaint.setTypeface(Typeface.create(dayTypeface, 0)); mDayPaint.setTextAlign(Align.CENTER); mDayPaint.setStyle(Style.FILL); } void setMonthTextColor(ColorStateList monthTextColor) { final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0); mMonthPaint.setColor(enabledColor); invalidate(); } void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) { final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0); mDayOfWeekPaint.setColor(enabledColor); invalidate(); } void setDayTextColor(ColorStateList dayTextColor) { mDayTextColor = dayTextColor; invalidate(); } void setDaySelectorColor(ColorStateList dayBackgroundColor) { final int activatedColor = dayBackgroundColor.getColorForState( StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0); mDaySelectorPaint.setColor(activatedColor); mDayHighlightSelectorPaint.setColor(activatedColor); mDayHighlightSelectorPaint.setAlpha(SELECTED_HIGHLIGHT_ALPHA); invalidate(); } void setDayHighlightColor(ColorStateList dayHighlightColor) { final int pressedColor = dayHighlightColor.getColorForState( StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0); mDayHighlightPaint.setColor(pressedColor); invalidate(); } public void setOnDayClickListener(OnDayClickListener listener) { mOnDayClickListener = listener; } @Override public boolean dispatchHoverEvent(MotionEvent event) { // First right-of-refusal goes the touch exploration helper. return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { final int x = (int) (event.getX() + 0.5f); final int y = (int) (event.getY() + 0.5f); final int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: final int touchedItem = getDayAtLocation(x, y); mIsTouchHighlighted = true; if (mHighlightedDay != touchedItem) { mHighlightedDay = touchedItem; mPreviouslyHighlightedDay = touchedItem; invalidate(); } if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) { // Touch something that's not an item, reject event. return false; } break; case MotionEvent.ACTION_UP: final int clickedDay = getDayAtLocation(x, y); onDayClicked(clickedDay); // Fall through. case MotionEvent.ACTION_CANCEL: // Reset touched day on stream end. mHighlightedDay = -1; mIsTouchHighlighted = false; invalidate(); break; } return true; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // We need to handle focus change within the SimpleMonthView because we are simulating // multiple Views. The arrow keys will move between days until there is no space (no // day to the left, top, right, or bottom). Focus forward and back jumps out of the // SimpleMonthView, skipping over other SimpleMonthViews in the parent ViewPager // to the next focusable View in the hierarchy. boolean focusChanged = false; switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_LEFT: if (event.hasNoModifiers()) { focusChanged = moveOneDay(isLayoutRtl()); } break; case KeyEvent.KEYCODE_DPAD_RIGHT: if (event.hasNoModifiers()) { focusChanged = moveOneDay(!isLayoutRtl()); } break; case KeyEvent.KEYCODE_DPAD_UP: if (event.hasNoModifiers()) { ensureFocusedDay(); if (mHighlightedDay > 7) { mHighlightedDay -= 7; focusChanged = true; } } break; case KeyEvent.KEYCODE_DPAD_DOWN: if (event.hasNoModifiers()) { ensureFocusedDay(); if (mHighlightedDay <= mDaysInMonth - 7) { mHighlightedDay += 7; focusChanged = true; } } break; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: if (mHighlightedDay != -1) { onDayClicked(mHighlightedDay); return true; } break; case KeyEvent.KEYCODE_TAB: { int focusChangeDirection = 0; if (event.hasNoModifiers()) { focusChangeDirection = View.FOCUS_FORWARD; } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { focusChangeDirection = View.FOCUS_BACKWARD; } if (focusChangeDirection != 0) { final ViewParent parent = getParent(); // move out of the ViewPager next/previous View nextFocus = this; do { nextFocus = nextFocus.focusSearch(focusChangeDirection); } while (nextFocus != null && nextFocus != this && nextFocus.getParent() == parent); if (nextFocus != null) { nextFocus.requestFocus(); return true; } } break; } } if (focusChanged) { invalidate(); return true; } else { return super.onKeyDown(keyCode, event); } } private boolean moveOneDay(boolean positive) { ensureFocusedDay(); boolean focusChanged = false; if (positive) { if (!isLastDayOfWeek(mHighlightedDay) && mHighlightedDay < mDaysInMonth) { mHighlightedDay++; focusChanged = true; } } else { if (!isFirstDayOfWeek(mHighlightedDay) && mHighlightedDay > 1) { mHighlightedDay--; focusChanged = true; } } return focusChanged; } @Override protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction, @Nullable Rect previouslyFocusedRect) { if (gainFocus) { // If we've gained focus through arrow keys, we should find the day closest // to the focus rect. If we've gained focus through forward/back, we should // focus on the selected day if there is one. final int offset = findDayOffset(); switch(direction) { case View.FOCUS_RIGHT: { int row = findClosestRow(previouslyFocusedRect); mHighlightedDay = row == 0 ? 1 : (row * DAYS_IN_WEEK) - offset + 1; break; } case View.FOCUS_LEFT: { int row = findClosestRow(previouslyFocusedRect) + 1; mHighlightedDay = Math.min(mDaysInMonth, (row * DAYS_IN_WEEK) - offset); break; } case View.FOCUS_DOWN: { final int col = findClosestColumn(previouslyFocusedRect); final int day = col - offset + 1; mHighlightedDay = day < 1 ? day + DAYS_IN_WEEK : day; break; } case View.FOCUS_UP: { final int col = findClosestColumn(previouslyFocusedRect); final int maxWeeks = (offset + mDaysInMonth) / DAYS_IN_WEEK; final int day = col - offset + (DAYS_IN_WEEK * maxWeeks) + 1; mHighlightedDay = day > mDaysInMonth ? day - DAYS_IN_WEEK : day; break; } } ensureFocusedDay(); invalidate(); } super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); }
Returns the row (0 indexed) closest to previouslyFocusedRect or center if null.
/** * Returns the row (0 indexed) closest to previouslyFocusedRect or center if null. */
private int findClosestRow(@Nullable Rect previouslyFocusedRect) { if (previouslyFocusedRect == null) { return 3; } else if (mDayHeight == 0) { return 0; // There hasn't been a layout, so just choose the first row } else { int centerY = previouslyFocusedRect.centerY(); final TextPaint p = mDayPaint; final int headerHeight = mMonthHeight + mDayOfWeekHeight; final int rowHeight = mDayHeight; // Text is vertically centered within the row height. final float halfLineHeight = (p.ascent() + p.descent()) / 2f; final int rowCenter = headerHeight + rowHeight / 2; centerY -= rowCenter - halfLineHeight; int row = Math.round(centerY / (float) rowHeight); final int maxDay = findDayOffset() + mDaysInMonth; final int maxRows = (maxDay / DAYS_IN_WEEK) - ((maxDay % DAYS_IN_WEEK == 0) ? 1 : 0); row = MathUtils.constrain(row, 0, maxRows); return row; } }
Returns the column (0 indexed) closest to the previouslyFocusedRect or center if null. The 0 index is related to the first day of the week.
/** * Returns the column (0 indexed) closest to the previouslyFocusedRect or center if null. * The 0 index is related to the first day of the week. */
private int findClosestColumn(@Nullable Rect previouslyFocusedRect) { if (previouslyFocusedRect == null) { return DAYS_IN_WEEK / 2; } else if (mCellWidth == 0) { return 0; // There hasn't been a layout, so we can just choose the first column } else { int centerX = previouslyFocusedRect.centerX() - mPaddingLeft; final int columnFromLeft = MathUtils.constrain(centerX / mCellWidth, 0, DAYS_IN_WEEK - 1); return isLayoutRtl() ? DAYS_IN_WEEK - columnFromLeft - 1: columnFromLeft; } } @Override public void getFocusedRect(Rect r) { if (mHighlightedDay > 0) { getBoundsForDay(mHighlightedDay, r); } else { super.getFocusedRect(r); } } @Override protected void onFocusLost() { if (!mIsTouchHighlighted) { // Unhighlight a day. mPreviouslyHighlightedDay = mHighlightedDay; mHighlightedDay = -1; invalidate(); } super.onFocusLost(); }
Ensure some day is highlighted. If a day isn't highlighted, it chooses the selected day, if possible, or the first day of the month if not.
/** * Ensure some day is highlighted. If a day isn't highlighted, it chooses the selected day, * if possible, or the first day of the month if not. */
private void ensureFocusedDay() { if (mHighlightedDay != -1) { return; } if (mPreviouslyHighlightedDay != -1) { mHighlightedDay = mPreviouslyHighlightedDay; return; } if (mActivatedDay != -1) { mHighlightedDay = mActivatedDay; return; } mHighlightedDay = 1; } private boolean isFirstDayOfWeek(int day) { final int offset = findDayOffset(); return (offset + day - 1) % DAYS_IN_WEEK == 0; } private boolean isLastDayOfWeek(int day) { final int offset = findDayOffset(); return (offset + day) % DAYS_IN_WEEK == 0; } @Override protected void onDraw(Canvas canvas) { final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); canvas.translate(paddingLeft, paddingTop); drawMonth(canvas); drawDaysOfWeek(canvas); drawDays(canvas); canvas.translate(-paddingLeft, -paddingTop); } private void drawMonth(Canvas canvas) { final float x = mPaddedWidth / 2f; // Vertically centered within the month header height. final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent(); final float y = (mMonthHeight - lineHeight) / 2f; canvas.drawText(mMonthYearLabel, x, y, mMonthPaint); } public String getMonthYearLabel() { return mMonthYearLabel; } private void drawDaysOfWeek(Canvas canvas) { final TextPaint p = mDayOfWeekPaint; final int headerHeight = mMonthHeight; final int rowHeight = mDayOfWeekHeight; final int colWidth = mCellWidth; // Text is vertically centered within the day of week height. final float halfLineHeight = (p.ascent() + p.descent()) / 2f; final int rowCenter = headerHeight + rowHeight / 2; for (int col = 0; col < DAYS_IN_WEEK; col++) { final int colCenter = colWidth * col + colWidth / 2; final int colCenterRtl; if (isLayoutRtl()) { colCenterRtl = mPaddedWidth - colCenter; } else { colCenterRtl = colCenter; } final String label = mDayOfWeekLabels[col]; canvas.drawText(label, colCenterRtl, rowCenter - halfLineHeight, p); } }
Draws the month days.
/** * Draws the month days. */
private void drawDays(Canvas canvas) { final TextPaint p = mDayPaint; final int headerHeight = mMonthHeight + mDayOfWeekHeight; final int rowHeight = mDayHeight; final int colWidth = mCellWidth; // Text is vertically centered within the row height. final float halfLineHeight = (p.ascent() + p.descent()) / 2f; int rowCenter = headerHeight + rowHeight / 2; for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) { final int colCenter = colWidth * col + colWidth / 2; final int colCenterRtl; if (isLayoutRtl()) { colCenterRtl = mPaddedWidth - colCenter; } else { colCenterRtl = colCenter; } int stateMask = 0; final boolean isDayEnabled = isDayEnabled(day); if (isDayEnabled) { stateMask |= StateSet.VIEW_STATE_ENABLED; } final boolean isDayActivated = mActivatedDay == day; final boolean isDayHighlighted = mHighlightedDay == day; if (isDayActivated) { stateMask |= StateSet.VIEW_STATE_ACTIVATED; // Adjust the circle to be centered on the row. final Paint paint = isDayHighlighted ? mDayHighlightSelectorPaint : mDaySelectorPaint; canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, paint); } else if (isDayHighlighted) { stateMask |= StateSet.VIEW_STATE_PRESSED; if (isDayEnabled) { // Adjust the circle to be centered on the row. canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, mDayHighlightPaint); } } final boolean isDayToday = mToday == day; final int dayTextColor; if (isDayToday && !isDayActivated) { dayTextColor = mDaySelectorPaint.getColor(); } else { final int[] stateSet = StateSet.get(stateMask); dayTextColor = mDayTextColor.getColorForState(stateSet, 0); } p.setColor(dayTextColor); canvas.drawText(mDayFormatter.format(day), colCenterRtl, rowCenter - halfLineHeight, p); col++; if (col == DAYS_IN_WEEK) { col = 0; rowCenter += rowHeight; } } } private boolean isDayEnabled(int day) { return day >= mEnabledDayStart && day <= mEnabledDayEnd; } private boolean isValidDayOfMonth(int day) { return day >= 1 && day <= mDaysInMonth; } private static boolean isValidDayOfWeek(int day) { return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY; } private static boolean isValidMonth(int month) { return month >= Calendar.JANUARY && month <= Calendar.DECEMBER; }
Sets the selected day.
Params:
  • dayOfMonth – the selected day of the month, or -1 to clear the selection
/** * Sets the selected day. * * @param dayOfMonth the selected day of the month, or {@code -1} to clear * the selection */
public void setSelectedDay(int dayOfMonth) { mActivatedDay = dayOfMonth; // Invalidate cached accessibility information. mTouchHelper.invalidateRoot(); invalidate(); }
Sets the first day of the week.
Params:
  • weekStart – which day the week should start on, valid values are SUNDAY.SUNDAY through SATURDAY.SATURDAY
/** * Sets the first day of the week. * * @param weekStart which day the week should start on, valid values are * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY} */
public void setFirstDayOfWeek(int weekStart) { if (isValidDayOfWeek(weekStart)) { mWeekStart = weekStart; } else { mWeekStart = mCalendar.getFirstDayOfWeek(); } updateDayOfWeekLabels(); // Invalidate cached accessibility information. mTouchHelper.invalidateRoot(); invalidate(); }
Sets all the parameters for displaying this week.

Parameters have a default value and will only update if a new value is included, except for focus month, which will always default to no focus month if no value is passed in. The only required parameter is the week start.

Params:
  • selectedDay – the selected day of the month, or -1 for no selection
  • month – the month
  • year – the year
  • weekStart – which day the week should start on, valid values are SUNDAY.SUNDAY through SATURDAY.SATURDAY
  • enabledDayStart – the first enabled day
  • enabledDayEnd – the last enabled day
/** * Sets all the parameters for displaying this week. * <p> * Parameters have a default value and will only update if a new value is * included, except for focus month, which will always default to no focus * month if no value is passed in. The only required parameter is the week * start. * * @param selectedDay the selected day of the month, or -1 for no selection * @param month the month * @param year the year * @param weekStart which day the week should start on, valid values are * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY} * @param enabledDayStart the first enabled day * @param enabledDayEnd the last enabled day */
void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart, int enabledDayEnd) { mActivatedDay = selectedDay; if (isValidMonth(month)) { mMonth = month; } mYear = year; mCalendar.set(Calendar.MONTH, mMonth); mCalendar.set(Calendar.YEAR, mYear); mCalendar.set(Calendar.DAY_OF_MONTH, 1); mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); if (isValidDayOfWeek(weekStart)) { mWeekStart = weekStart; } else { mWeekStart = mCalendar.getFirstDayOfWeek(); } // Figure out what day today is. final Calendar today = Calendar.getInstance(); mToday = -1; mDaysInMonth = getDaysInMonth(mMonth, mYear); for (int i = 0; i < mDaysInMonth; i++) { final int day = i + 1; if (sameDay(day, today)) { mToday = day; } } mEnabledDayStart = MathUtils.constrain(enabledDayStart, 1, mDaysInMonth); mEnabledDayEnd = MathUtils.constrain(enabledDayEnd, mEnabledDayStart, mDaysInMonth); updateMonthYearLabel(); updateDayOfWeekLabels(); // Invalidate cached accessibility information. mTouchHelper.invalidateRoot(); invalidate(); } private static int getDaysInMonth(int month, int year) { switch (month) { case Calendar.JANUARY: case Calendar.MARCH: case Calendar.MAY: case Calendar.JULY: case Calendar.AUGUST: case Calendar.OCTOBER: case Calendar.DECEMBER: return 31; case Calendar.APRIL: case Calendar.JUNE: case Calendar.SEPTEMBER: case Calendar.NOVEMBER: return 30; case Calendar.FEBRUARY: return (year % 4 == 0) ? 29 : 28; default: throw new IllegalArgumentException("Invalid Month"); } } private boolean sameDay(int day, Calendar today) { return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH) && day == today.get(Calendar.DAY_OF_MONTH); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH + mDesiredDayOfWeekHeight + mDesiredMonthHeight + getPaddingTop() + getPaddingBottom(); final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK + getPaddingStart() + getPaddingEnd(); final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec); final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec); setMeasuredDimension(resolvedWidth, resolvedHeight); } @Override public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); requestLayout(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (!changed) { return; } // Let's initialize a completely reasonable number of variables. final int w = right - left; final int h = bottom - top; final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); final int paddingRight = getPaddingRight(); final int paddingBottom = getPaddingBottom(); final int paddedRight = w - paddingRight; final int paddedBottom = h - paddingBottom; final int paddedWidth = paddedRight - paddingLeft; final int paddedHeight = paddedBottom - paddingTop; if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) { return; } mPaddedWidth = paddedWidth; mPaddedHeight = paddedHeight; // We may have been laid out smaller than our preferred size. If so, // scale all dimensions to fit. final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom; final float scaleH = paddedHeight / (float) measuredPaddedHeight; final int monthHeight = (int) (mDesiredMonthHeight * scaleH); final int cellWidth = mPaddedWidth / DAYS_IN_WEEK; mMonthHeight = monthHeight; mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH); mDayHeight = (int) (mDesiredDayHeight * scaleH); mCellWidth = cellWidth; // Compute the largest day selector radius that's still within the clip // bounds and desired selector radius. final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight); final int maxSelectorHeight = mDayHeight / 2 + paddingBottom; mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius, Math.min(maxSelectorWidth, maxSelectorHeight)); // Invalidate cached accessibility information. mTouchHelper.invalidateRoot(); } private int findDayOffset() { final int offset = mDayOfWeekStart - mWeekStart; if (mDayOfWeekStart < mWeekStart) { return offset + DAYS_IN_WEEK; } return offset; }
Calculates the day of the month at the specified touch position. Returns the day of the month or -1 if the position wasn't in a valid day.
Params:
  • x – the x position of the touch event
  • y – the y position of the touch event
Returns:the day of the month at (x, y), or -1 if the position wasn't in a valid day
/** * Calculates the day of the month at the specified touch position. Returns * the day of the month or -1 if the position wasn't in a valid day. * * @param x the x position of the touch event * @param y the y position of the touch event * @return the day of the month at (x, y), or -1 if the position wasn't in * a valid day */
private int getDayAtLocation(int x, int y) { final int paddedX = x - getPaddingLeft(); if (paddedX < 0 || paddedX >= mPaddedWidth) { return -1; } final int headerHeight = mMonthHeight + mDayOfWeekHeight; final int paddedY = y - getPaddingTop(); if (paddedY < headerHeight || paddedY >= mPaddedHeight) { return -1; } // Adjust for RTL after applying padding. final int paddedXRtl; if (isLayoutRtl()) { paddedXRtl = mPaddedWidth - paddedX; } else { paddedXRtl = paddedX; } final int row = (paddedY - headerHeight) / mDayHeight; final int col = (paddedXRtl * DAYS_IN_WEEK) / mPaddedWidth; final int index = col + row * DAYS_IN_WEEK; final int day = index + 1 - findDayOffset(); if (!isValidDayOfMonth(day)) { return -1; } return day; }
Calculates the bounds of the specified day.
Params:
  • id – the day of the month
  • outBounds – the rect to populate with bounds
/** * Calculates the bounds of the specified day. * * @param id the day of the month * @param outBounds the rect to populate with bounds */
public boolean getBoundsForDay(int id, Rect outBounds) { if (!isValidDayOfMonth(id)) { return false; } final int index = id - 1 + findDayOffset(); // Compute left edge, taking into account RTL. final int col = index % DAYS_IN_WEEK; final int colWidth = mCellWidth; final int left; if (isLayoutRtl()) { left = getWidth() - getPaddingRight() - (col + 1) * colWidth; } else { left = getPaddingLeft() + col * colWidth; } // Compute top edge. final int row = index / DAYS_IN_WEEK; final int rowHeight = mDayHeight; final int headerHeight = mMonthHeight + mDayOfWeekHeight; final int top = getPaddingTop() + headerHeight + row * rowHeight; outBounds.set(left, top, left + colWidth, top + rowHeight); return true; }
Called when the user clicks on a day. Handles callbacks to the OnDayClickListener if one is set.
Params:
  • day – the day that was clicked
/** * Called when the user clicks on a day. Handles callbacks to the * {@link OnDayClickListener} if one is set. * * @param day the day that was clicked */
private boolean onDayClicked(int day) { if (!isValidDayOfMonth(day) || !isDayEnabled(day)) { return false; } if (mOnDayClickListener != null) { final Calendar date = Calendar.getInstance(); date.set(mYear, mMonth, day); mOnDayClickListener.onDayClick(this, date); } // This is a no-op if accessibility is turned off. mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); return true; } @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { if (!isEnabled()) { return null; } // Add 0.5f to event coordinates to match the logic in onTouchEvent. final int x = (int) (event.getX() + 0.5f); final int y = (int) (event.getY() + 0.5f); final int dayUnderPointer = getDayAtLocation(x, y); if (dayUnderPointer >= 0) { return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND); } return super.onResolvePointerIcon(event, pointerIndex); }
Provides a virtual view hierarchy for interfacing with an accessibility service.
/** * Provides a virtual view hierarchy for interfacing with an accessibility * service. */
private class MonthViewTouchHelper extends ExploreByTouchHelper { private static final String DATE_FORMAT = "dd MMMM yyyy"; private final Rect mTempRect = new Rect(); private final Calendar mTempCalendar = Calendar.getInstance(); public MonthViewTouchHelper(View host) { super(host); } @Override protected int getVirtualViewAt(float x, float y) { final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f)); if (day != -1) { return day; } return ExploreByTouchHelper.INVALID_ID; } @Override protected void getVisibleVirtualViews(IntArray virtualViewIds) { for (int day = 1; day <= mDaysInMonth; day++) { virtualViewIds.add(day); } } @Override protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { event.setContentDescription(getDayDescription(virtualViewId)); } @Override protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect); if (!hasBounds) { // The day is invalid, kill the node. mTempRect.setEmpty(); node.setContentDescription(""); node.setBoundsInParent(mTempRect); node.setVisibleToUser(false); return; } node.setText(getDayText(virtualViewId)); node.setContentDescription(getDayDescription(virtualViewId)); node.setBoundsInParent(mTempRect); final boolean isDayEnabled = isDayEnabled(virtualViewId); if (isDayEnabled) { node.addAction(AccessibilityAction.ACTION_CLICK); } node.setEnabled(isDayEnabled); if (virtualViewId == mActivatedDay) { // TODO: This should use activated once that's supported. node.setChecked(true); } } @Override protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) { switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: return onDayClicked(virtualViewId); } return false; }
Generates a description for a given virtual view.
Params:
  • id – the day to generate a description for
Returns:a description of the virtual view
/** * Generates a description for a given virtual view. * * @param id the day to generate a description for * @return a description of the virtual view */
private CharSequence getDayDescription(int id) { if (isValidDayOfMonth(id)) { mTempCalendar.set(mYear, mMonth, id); return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis()); } return ""; }
Generates displayed text for a given virtual view.
Params:
  • id – the day to generate text for
Returns:the visible text of the virtual view
/** * Generates displayed text for a given virtual view. * * @param id the day to generate text for * @return the visible text of the virtual view */
private CharSequence getDayText(int id) { if (isValidDayOfMonth(id)) { return mDayFormatter.format(id); } return null; } }
Handles callbacks when the user clicks on a time object.
/** * Handles callbacks when the user clicks on a time object. */
public interface OnDayClickListener { void onDayClick(SimpleMonthView view, Calendar day); } }