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;
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();
private final String[] mDayOfWeekLabels = new String[7];
private final Calendar mCalendar;
private final Locale mLocale;
private final MonthViewTouchHelper mTouchHelper;
private final NumberFormat mDayFormatter;
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;
private int mMonthHeight;
private int mDayOfWeekHeight;
private int mDayHeight;
private int mCellWidth;
private int mDaySelectorRadius;
private int mPaddedWidth;
private int mPaddedHeight;
private int mActivatedDay = -1;
private int mToday = DEFAULT_SELECTED_DAY;
private int mWeekStart = DEFAULT_WEEK_START;
private int mDaysInMonth;
private int mDayOfWeekStart;
private int mEnabledDayStart = 1;
private int mEnabledDayEnd = 31;
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);
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() {
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];
}
}
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();
}
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) {
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) {
return false;
}
break;
case MotionEvent.ACTION_UP:
final int clickedDay = getDayAtLocation(x, y);
onDayClicked(clickedDay);
case MotionEvent.ACTION_CANCEL:
mHighlightedDay = -1;
mIsTouchHighlighted = false;
invalidate();
break;
}
return true;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
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();
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) {
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);
}
private int findClosestRow(@Nullable Rect previouslyFocusedRect) {
if (previouslyFocusedRect == null) {
return 3;
} else if (mDayHeight == 0) {
return 0;
} else {
int centerY = previouslyFocusedRect.centerY();
final TextPaint p = mDayPaint;
final int headerHeight = mMonthHeight + mDayOfWeekHeight;
final int rowHeight = mDayHeight;
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;
}
}
private int findClosestColumn(@Nullable Rect previouslyFocusedRect) {
if (previouslyFocusedRect == null) {
return DAYS_IN_WEEK / 2;
} else if (mCellWidth == 0) {
return 0;
} 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) {
mPreviouslyHighlightedDay = mHighlightedDay;
mHighlightedDay = -1;
invalidate();
}
super.onFocusLost();
}
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;
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;
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);
}
}
private void drawDays(Canvas canvas) {
final TextPaint p = mDayPaint;
final int headerHeight = mMonthHeight + mDayOfWeekHeight;
final int rowHeight = mDayHeight;
final int colWidth = mCellWidth;
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;
final Paint paint = isDayHighlighted ? mDayHighlightSelectorPaint :
mDaySelectorPaint;
canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, paint);
} else if (isDayHighlighted) {
stateMask |= StateSet.VIEW_STATE_PRESSED;
if (isDayEnabled) {
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;
}
public void setSelectedDay(int dayOfMonth) {
mActivatedDay = dayOfMonth;
mTouchHelper.invalidateRoot();
invalidate();
}
public void setFirstDayOfWeek(int weekStart) {
if (isValidDayOfWeek(weekStart)) {
mWeekStart = weekStart;
} else {
mWeekStart = mCalendar.getFirstDayOfWeek();
}
updateDayOfWeekLabels();
mTouchHelper.invalidateRoot();
invalidate();
}
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();
}
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();
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;
}
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;
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;
final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
Math.min(maxSelectorWidth, maxSelectorHeight));
mTouchHelper.invalidateRoot();
}
private int findDayOffset() {
final int offset = mDayOfWeekStart - mWeekStart;
if (mDayOfWeekStart < mWeekStart) {
return offset + DAYS_IN_WEEK;
}
return offset;
}
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;
}
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;
}
public boolean getBoundsForDay(int id, Rect outBounds) {
if (!isValidDayOfMonth(id)) {
return false;
}
final int index = id - 1 + findDayOffset();
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;
}
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;
}
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);
}
mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
return true;
}
@Override
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
if (!isEnabled()) {
return null;
}
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);
}
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) {
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) {
node.setChecked(true);
}
}
@Override
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
Bundle arguments) {
switch (action) {
case AccessibilityNodeInfo.ACTION_CLICK:
return onDayClicked(virtualViewId);
}
return false;
}
private CharSequence getDayDescription(int id) {
if (isValidDayOfMonth(id)) {
mTempCalendar.set(mYear, mMonth, id);
return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
}
return "";
}
private CharSequence getDayText(int id) {
if (isValidDayOfMonth(id)) {
return mDayFormatter.format(id);
}
return null;
}
}
public interface OnDayClickListener {
void onDayClick(SimpleMonthView view, Calendar day);
}
}