/*
* Copyright (C) 2013 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 com.codetroopers.betterpickers.calendardatepicker;
import android.content.Context;
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.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.widget.ExploreByTouchHelper;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import com.codetroopers.betterpickers.R;
import com.codetroopers.betterpickers.Utils;
import com.codetroopers.betterpickers.calendardatepicker.MonthAdapter.CalendarDay;
import java.security.InvalidParameterException;
import java.util.Calendar;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
/**
* A calendar-like view displaying a specified month and the appropriate selectable day numbers within the specified
* month.
*/
public abstract class MonthView extends View {
private static final String TAG = "MonthView";
/**
* These params can be passed into the view to control how it appears.
* {@link #VIEW_PARAMS_WEEK} is the only required field, though the default
* values are unlikely to fit most layouts correctly.
*/
/**
* This sets the height of this week in pixels
*/
public static final String VIEW_PARAMS_HEIGHT = "height";
/**
* This specifies the position (or weeks since the epoch) of this week, calculated using {@link
* Utils#getWeeksSinceEpochFromJulianDay}
*/
public static final String VIEW_PARAMS_MONTH = "month";
/**
* This specifies the position (or weeks since the epoch) of this week, calculated using {@link
* Utils#getWeeksSinceEpochFromJulianDay}
*/
public static final String VIEW_PARAMS_YEAR = "year";
/**
* This sets one of the days in this view as selected {@link android.text.format.Time#SUNDAY} through {@link
* android.text.format.Time#SATURDAY}.
*/
public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day";
/**
* Which day the week should start on. {@link android.text.format.Time#SUNDAY} through {@link
* android.text.format.Time#SATURDAY}.
*/
public static final String VIEW_PARAMS_WEEK_START = "week_start";
/**
* How many days to display at a time. Days will be displayed starting with {@link #mWeekStart}.
*/
public static final String VIEW_PARAMS_NUM_DAYS = "num_days";
/**
* Which month is currently in focus, as defined by {@link android.text.format.Time#month} [0-11].
*/
public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month";
/**
* If this month should display week numbers. false if 0, true otherwise.
*/
public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num";
/**
* Day in month that is the start of the selected date range. Only set if the day is in the given month
*/
public static final String VIEW_PARAMS_RANGE_MIN = "range_min";
/**
* Day in month that is the end of the selected date range. Only set if the day is in the given month.
*/
public static final String VIEW_PARAMS_RANGE_MAX = "range_max";
protected static int DEFAULT_HEIGHT = 32;
protected static int MIN_HEIGHT = 10;
protected static final int DEFAULT_SELECTED_DAY = -1;
protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
protected static final int DEFAULT_NUM_DAYS = 7;
protected static final int DEFAULT_SHOW_WK_NUM = 0;
protected static final int DEFAULT_FOCUS_MONTH = -1;
protected static final int DEFAULT_NUM_ROWS = 6;
protected static final int MAX_NUM_ROWS = 6;
private static final int SELECTED_CIRCLE_ALPHA = 60;
private static final int DISABLED_DAY_SQUARE_ALPHA = 60;
protected static int DAY_SEPARATOR_WIDTH = 1;
protected static int MINI_DAY_NUMBER_TEXT_SIZE;
protected static int MONTH_LABEL_TEXT_SIZE;
protected static int MONTH_DAY_LABEL_TEXT_SIZE;
protected static int MONTH_HEADER_SIZE;
protected static int DAY_SELECTED_CIRCLE_SIZE;
// used for scaling to the device density
protected static float mScale = 0;
// affects the padding on the sides of this view
protected int mPadding = 0;
private String mDayOfWeekTypeface;
private String mMonthTitleTypeface;
protected Paint mMonthNumPaint;
protected Paint mMonthTitlePaint;
protected Paint mMonthTitleBGPaint;
protected Paint mSelectedCirclePaint;
protected Paint mDisabledDaySquarePaint;
protected Paint mMonthDayLabelPaint;
private final Formatter mFormatter;
private final StringBuilder mStringBuilder;
// The Julian day of the first day displayed by this item
protected int mFirstJulianDay = -1;
// The month of the first day in this week
protected int mFirstMonth = -1;
// The month of the last day in this week
protected int mLastMonth = -1;
protected int mMonth;
protected int mYear;
// Quick reference to the width of this view, matches parent
protected int mWidth;
// The height this view should draw at in pixels, set by height param
protected int mRowHeight = DEFAULT_HEIGHT;
// If this view contains the today
protected boolean mHasToday = false;
// Which day is selected [0-6] or -1 if no day is selected
protected int mSelectedDay = -1;
// Which days are disabled in the view so that they are unselectable
protected SparseArray<CalendarDay> mDisabledDays;
// Which day is today [0-6] or -1 if no day is today
protected int mToday = DEFAULT_SELECTED_DAY;
// Which day of the week to start on [0-6]
protected int mWeekStart = DEFAULT_WEEK_START;
// How many days to display
protected int mNumDays = DEFAULT_NUM_DAYS;
// The number of days + a spot for week number if it is displayed
protected int mNumCells = mNumDays;
// The left edge of the selected day
protected int mSelectedLeft = -1;
// The right edge of the selected day
protected int mSelectedRight = -1;
// The minimum day that fits into the provided range and is enabled.
protected int mRangeMin = -1;
// The maximum day that fits into the provided range and is enabled.
protected int mRangeMax = -1;
private final Calendar mCalendar;
private final Calendar mDayLabelCalendar;
private final MonthViewTouchHelper mTouchHelper;
private int mNumRows = DEFAULT_NUM_ROWS;
// Optional listener for handling day click actions
private OnDayClickListener mOnDayClickListener;
// Whether to prevent setting the accessibility delegate
private boolean mLockAccessibilityDelegate;
protected int mDayTextColorEnabled;
protected int mDayTextColorDisabled;
protected int mTodayNumberColor;
protected int mDayBackgroundColorDisabled;
protected int mMonthTitleColor;
public MonthView(Context context) {
super(context);
Resources res = context.getResources();
mDayLabelCalendar = Calendar.getInstance();
mCalendar = Calendar.getInstance();
mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
mMonthTitleTypeface = res.getString(R.string.sans_serif);
mDayTextColorEnabled = res.getColor(R.color.date_picker_text_normal);
mDayTextColorDisabled = res.getColor(R.color.date_picker_text_disabled);
mTodayNumberColor = res.getColor(R.color.bpBlue);
mDayBackgroundColorDisabled = res.getColor(R.color.bpDarker_red);
mMonthTitleColor = res.getColor(R.color.date_picker_text_normal);
mStringBuilder = new StringBuilder(50);
mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size);
MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size);
MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size);
MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height);
DAY_SELECTED_CIRCLE_SIZE = res
.getDimensionPixelSize(R.dimen.day_number_select_circle_radius);
mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height) - MONTH_HEADER_SIZE) / MAX_NUM_ROWS;
// Set up accessibility components.
mTouchHelper = new MonthViewTouchHelper(this);
ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
mLockAccessibilityDelegate = true;
// Sets up any standard paints that will be used
initView();
}
public void setTheme(TypedArray themeColors) {
mMonthTitleColor = themeColors.getColor(R.styleable.BetterPickersDialogs_bpAmPmTextColor, ContextCompat.getColor(getContext(), R.color.ampm_text_color));
mTodayNumberColor = themeColors.getColor(R.styleable.BetterPickersDialogs_bpBodySelectedTextColor, ContextCompat.getColor(getContext(), R.color.bpBlue));
mDayTextColorEnabled = themeColors.getColor(R.styleable.BetterPickersDialogs_bpBodyUnselectedTextColor, ContextCompat.getColor(getContext(), R.color.date_picker_text_disabled));
mDayTextColorDisabled = themeColors.getColor(R.styleable.BetterPickersDialogs_bpDisabledDayTextColor, ContextCompat.getColor(getContext(), R.color.date_picker_text_disabled));
mDayBackgroundColorDisabled = themeColors.getColor(R.styleable.BetterPickersDialogs_bpDisabledDayBackgroundColor, ContextCompat.getColor(getContext(), R.color.bpDarker_red));
initView();
}
@Override
public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
// Workaround for a JB MR1 issue where accessibility delegates on
// top-level ListView items are overwritten.
if (!mLockAccessibilityDelegate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
super.setAccessibilityDelegate(delegate);
}
}
public void setOnDayClickListener(OnDayClickListener listener) {
mOnDayClickListener = listener;
}
/* Removed for backwards compatibility with Gingerbread
@Override
public boolean dispatchHoverEvent(MotionEvent event) {
// First right-of-refusal goes the touch exploration helper.
if (mTouchHelper.dispatchHoverEvent(event)) {
return true;
}
return super.dispatchHoverEvent(event);
}*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
final int day = getDayFromLocation(event.getX(), event.getY());
if (day >= 0) {
onDayClick(day);
}
break;
}
return true;
}
/**
* Sets up the text and style properties for painting. Override this if you want to use a different paint.
*/
protected void initView() {
mMonthTitlePaint = new Paint();
mMonthTitlePaint.setFakeBoldText(true);
mMonthTitlePaint.setAntiAlias(true);
mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
mMonthTitlePaint.setColor(mDayTextColorEnabled);
mMonthTitlePaint.setTextAlign(Align.CENTER);
mMonthTitlePaint.setStyle(Style.FILL);
mMonthTitleBGPaint = new Paint();
mMonthTitleBGPaint.setFakeBoldText(true);
mMonthTitleBGPaint.setAntiAlias(true);
mMonthTitleBGPaint.setTextAlign(Align.CENTER);
mMonthTitleBGPaint.setStyle(Style.FILL);
mSelectedCirclePaint = new Paint();
mSelectedCirclePaint.setFakeBoldText(true);
mSelectedCirclePaint.setAntiAlias(true);
mSelectedCirclePaint.setColor(mTodayNumberColor);
mSelectedCirclePaint.setTextAlign(Align.CENTER);
mSelectedCirclePaint.setStyle(Style.FILL);
mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
mDisabledDaySquarePaint = new Paint();
mDisabledDaySquarePaint.setFakeBoldText(true);
mDisabledDaySquarePaint.setAntiAlias(true);
mDisabledDaySquarePaint.setColor(mDayBackgroundColorDisabled);
mDisabledDaySquarePaint.setTextAlign(Align.CENTER);
mDisabledDaySquarePaint.setStyle(Style.FILL);
mDisabledDaySquarePaint.setAlpha(DISABLED_DAY_SQUARE_ALPHA);
mMonthDayLabelPaint = new Paint();
mMonthDayLabelPaint.setAntiAlias(true);
mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
mMonthDayLabelPaint.setColor(mDayTextColorEnabled);
mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
mMonthDayLabelPaint.setStyle(Style.FILL);
mMonthDayLabelPaint.setTextAlign(Align.CENTER);
mMonthDayLabelPaint.setFakeBoldText(true);
mMonthNumPaint = new Paint();
mMonthNumPaint.setAntiAlias(true);
mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
mMonthNumPaint.setStyle(Style.FILL);
mMonthNumPaint.setTextAlign(Align.CENTER);
mMonthNumPaint.setFakeBoldText(false);
}
@Override
protected void onDraw(Canvas canvas) {
drawMonthTitle(canvas);
drawMonthDayLabels(canvas);
drawMonthNums(canvas);
}
private int mDayOfWeekStart = 0;
/**
* Sets all the parameters for displaying this week. The only required parameter is the week number. Other
* 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. See {@link #VIEW_PARAMS_HEIGHT} for more info on
* parameters.
*
* @param params A map of the new parameters, see {@link #VIEW_PARAMS_HEIGHT}
*/
public void setMonthParams(HashMap<String, Integer> params) {
if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) {
throw new InvalidParameterException("You must specify month and year for this view");
}
setTag(params);
// We keep the current value for any params not present
if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
mRowHeight = params.get(VIEW_PARAMS_HEIGHT);
if (mRowHeight < MIN_HEIGHT) {
mRowHeight = MIN_HEIGHT;
}
}
if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
}
if (params.containsKey(VIEW_PARAMS_RANGE_MIN)) {
mRangeMin = params.get(VIEW_PARAMS_RANGE_MIN);
}
if (params.containsKey(VIEW_PARAMS_RANGE_MAX)) {
mRangeMax = params.get(VIEW_PARAMS_RANGE_MAX);
}
// Allocate space for caching the day numbers and focus values
mMonth = params.get(VIEW_PARAMS_MONTH);
mYear = params.get(VIEW_PARAMS_YEAR);
// Figure out what day today is
final Time today = new Time(Time.getCurrentTimezone());
today.setToNow();
mHasToday = false;
mToday = -1;
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 (params.containsKey(VIEW_PARAMS_WEEK_START)) {
mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
} else {
mWeekStart = mCalendar.getFirstDayOfWeek();
}
mNumCells = Utils.getDaysInMonth(mMonth, mYear);
for (int i = 0; i < mNumCells; i++) {
final int day = i + 1;
if (sameDay(day, today)) {
mHasToday = true;
mToday = day;
}
}
mNumRows = calculateNumRows();
// Invalidate cached accessibility information.
mTouchHelper.invalidateRoot();
}
public void setDisabledDays(@NonNull SparseArray<CalendarDay> disabledDays) {
mDisabledDays = disabledDays;
}
public void reuse() {
mNumRows = DEFAULT_NUM_ROWS;
requestLayout();
}
private int calculateNumRows() {
int offset = findDayOffset();
int dividend = (offset + mNumCells) / mNumDays;
int remainder = (offset + mNumCells) % mNumDays;
return (dividend + (remainder > 0 ? 1 : 0));
}
private boolean sameDay(int day, Time today) {
return mYear == today.year &&
mMonth == today.month &&
day == today.monthDay;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
+ MONTH_HEADER_SIZE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mWidth = w;
// Invalidate cached accessibility information.
mTouchHelper.invalidateRoot();
}
private String getMonthAndYearString() {
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_NO_MONTH_DAY;
mStringBuilder.setLength(0);
long millis = mCalendar.getTimeInMillis();
String monthTitle = DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, Time.getCurrentTimezone()).toString();
return monthTitle.substring(0, 1).toUpperCase() + monthTitle.substring(1).toLowerCase();
}
private void drawMonthTitle(Canvas canvas) {
int x = (mWidth + 2 * mPadding) / 2;
int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3);
canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
}
private void drawMonthDayLabels(Canvas canvas) {
int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
for (int i = 0; i < mNumDays; i++) {
int calendarDay = (i + mWeekStart) % mNumDays;
int x = (2 * i + 1) * dayWidthHalf + mPadding;
mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
mMonthDayLabelPaint);
}
}
/**
* Draws the week and month day numbers for this week. Override this method if you need different placement.
*
* @param canvas The canvas to draw on
*/
protected void drawMonthNums(Canvas canvas) {
int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
+ MONTH_HEADER_SIZE;
int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2);
int j = findDayOffset();
for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
int x = (2 * j + 1) * dayWidthHalf + mPadding;
int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH;
int startX = x - dayWidthHalf;
int stopX = x + dayWidthHalf;
int startY = y - yRelativeToDay;
int stopY = startY + mRowHeight;
int disabledDayKey = Utils.formatDisabledDayForKey(mYear, mMonth, dayNumber);
// A day is enabled if it is in range and is not explicitly set as disabled
boolean dayIsEnabled = isDayInRange(dayNumber);
if (mDisabledDays != null) {
dayIsEnabled = dayIsEnabled && mDisabledDays.indexOfKey(disabledDayKey) < 0;
}
drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY, dayIsEnabled);
j++;
if (j == mNumDays) {
j = 0;
y += mRowHeight;
}
}
}
private boolean isDayInRange(int day) {
return !(mRangeMax >= 0 && day > mRangeMax) && !(mRangeMin >= 0 && day < mRangeMin);
}
/**
* This method should draw the month day. Implemented by sub-classes to allow customization.
*
* @param canvas The canvas to draw on
* @param year The year of this month day
* @param month The month of this month day
* @param day The day number of this month day
* @param x The default x position to draw the day number
* @param y The default y position to draw the day number
* @param startX The left boundary of the day number rect
* @param stopX The right boundary of the day number rect
* @param startY The top boundary of the day number rect
* @param stopY The bottom boundary of the day number rect
* @param stopY The bottom boundary of the day number rect
* @param isEnabled The flag to show if the day should look enabled or not
*/
public abstract void drawMonthDay(Canvas canvas, int year, int month, int day,
int x, int y, int startX, int stopX, int startY, int stopY, boolean isEnabled);
private int findDayOffset() {
return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
- mWeekStart;
}
/**
* Calculates the day that the given x position is in, accounting for week number. Returns the day or -1 if the
* position wasn't in a day.
*
* @param x The x position of the touch event
* @return The day number, or -1 if the position wasn't in a day
*/
public int getDayFromLocation(float x, float y) {
int dayStart = mPadding;
if (x < dayStart || x > mWidth - mPadding) {
return -1;
}
// Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
int row = (int) (y - MONTH_HEADER_SIZE) / mRowHeight;
int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding));
int day = column - findDayOffset() + 1;
day += row * mNumDays;
if (day < 1 || day > mNumCells) {
return -1;
}
return day;
}
/**
* 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 void onDayClick(int day) {
if (mOnDayClickListener != null) {
mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day));
}
// This is a no-op if accessibility is turned off.
mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
}
/**
* @return The date that has accessibility focus, or {@code null} if no date has focus
*/
public CalendarDay getAccessibilityFocus() {
final int day = mTouchHelper.getFocusedVirtualView();
if (day >= 0) {
return new CalendarDay(mYear, mMonth, day);
}
return null;
}
/**
* Clears accessibility focus within the view. No-op if the view does not contain accessibility focus.
*/
public void clearAccessibilityFocus() {
mTouchHelper.clearFocusedVirtualView();
}
/**
* Attempts to restore accessibility focus to the specified date.
*
* @param day The date which should receive focus
* @return {@code false} if the date is not valid for this month view, or {@code true} if the date received focus
*/
public boolean restoreAccessibilityFocus(CalendarDay day) {
if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) {
return false;
}
mTouchHelper.setFocusedVirtualView(day.day);
return true;
}
/**
* 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);
}
public void setFocusedVirtualView(int virtualViewId) {
getAccessibilityNodeProvider(MonthView.this).performAction(
virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
}
public void clearFocusedVirtualView() {
final int focusedVirtualView = getFocusedVirtualView();
if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
getAccessibilityNodeProvider(MonthView.this).performAction(
focusedVirtualView,
AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
null);
}
}
@Override
protected int getVirtualViewAt(float x, float y) {
final int day = getDayFromLocation(x, y);
if (day >= 0) {
return day;
}
return ExploreByTouchHelper.INVALID_ID;
}
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
for (int day = 1; day <= mNumCells; day++) {
virtualViewIds.add(day);
}
}
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
event.setContentDescription(getItemDescription(virtualViewId));
}
@Override
protected void onPopulateNodeForVirtualView(int virtualViewId,
AccessibilityNodeInfoCompat node) {
getItemBounds(virtualViewId, mTempRect);
node.setContentDescription(getItemDescription(virtualViewId));
node.setBoundsInParent(mTempRect);
node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
if (virtualViewId == mSelectedDay) {
node.setSelected(true);
}
}
@Override
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
Bundle arguments) {
switch (action) {
case AccessibilityNodeInfo.ACTION_CLICK:
onDayClick(virtualViewId);
return true;
}
return false;
}
/**
* Calculates the bounding rectangle of a given time object.
*
* @param day The day to calculate bounds for
* @param rect The rectangle in which to store the bounds
*/
private void getItemBounds(int day, Rect rect) {
final int offsetX = mPadding;
final int offsetY = MONTH_HEADER_SIZE;
final int cellHeight = mRowHeight;
final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays);
final int index = ((day - 1) + findDayOffset());
final int row = (index / mNumDays);
final int column = (index % mNumDays);
final int x = (offsetX + (column * cellWidth));
final int y = (offsetY + (row * cellHeight));
rect.set(x, y, (x + cellWidth), (y + cellHeight));
}
/**
* Generates a description for a given time object. Since this description will be spoken, the components are
* ordered by descending specificity as DAY MONTH YEAR.
*
* @param day The day to generate a description for
* @return A description of the time object
*/
private CharSequence getItemDescription(int day) {
mTempCalendar.set(mYear, mMonth, day);
final CharSequence date = DateFormat.format(DATE_FORMAT,
mTempCalendar.getTimeInMillis());
if (day == mSelectedDay) {
return getContext().getString(R.string.item_is_selected, date);
}
return date;
}
}
/**
* Handles callbacks when the user clicks on a time object.
*/
public interface OnDayClickListener {
public void onDayClick(MonthView view, CalendarDay day);
}
}