Java tutorial
/* * Copyright (C) 2013 The Android Open Source Project * Modifications Copyright (C) 2015 Fred Grott(GrottWorkShop) * * 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.grottworkshop.gwsmaterialdaterangepicker.date; import android.content.Context; import android.content.res.Resources; 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.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.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import com.grottworkshop.gwsmaterialdaterangepicker.R; import com.grottworkshop.gwsmaterialdaterangepicker.TypefaceHelper; 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. * Created by fgrott on 9/15/2015. */ @SuppressWarnings("unused") 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. */ public static final String VIEW_PARAMS_MONTH = "month"; /** * This specifies the position (or weeks since the epoch) of this week. */ public static final String VIEW_PARAMS_YEAR = "year"; /** * This sets one of the days in this view as selected {@link Calendar#SUNDAY} * through {@link Calendar#SATURDAY}. */ public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; /** * Which day the week should start on. {@link Calendar#SUNDAY} through * {@link Calendar#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 Calendar#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"; 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 = 255; 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; protected DatePickerController mController; // affects the padding on the sides of this view protected int mEdgePadding = 0; private String mDayOfWeekTypeface; private String mMonthTitleTypeface; protected Paint mMonthNumPaint; protected Paint mMonthTitlePaint; protected Paint mSelectedCirclePaint; 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 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; private final Calendar mCalendar; protected final Calendar mDayLabelCalendar; private final MonthViewTouchHelper mTouchHelper; protected int mNumRows = DEFAULT_NUM_ROWS; // Optional listener for handling day click actions protected OnDayClickListener mOnDayClickListener; // Whether to prevent setting the accessibility delegate private boolean mLockAccessibilityDelegate; protected int mDayTextColor; protected int mSelectedDayTextColor; protected int mMonthDayTextColor; protected int mTodayNumberColor; protected int mHighlightedDayTextColor; protected int mDisabledDayTextColor; protected int mMonthTitleColor; public MonthView(Context context) { this(context, null, null); } @SuppressWarnings("deprecation") public MonthView(Context context, AttributeSet attr, DatePickerController controller) { super(context, attr); mController = controller; Resources res = context.getResources(); mDayLabelCalendar = Calendar.getInstance(); mCalendar = Calendar.getInstance(); mDayOfWeekTypeface = res.getString(R.string.mdtp_day_of_week_label_typeface); mMonthTitleTypeface = res.getString(R.string.mdtp_sans_serif); boolean darkTheme = mController != null && mController.isThemeDark(); if (darkTheme) { //TODO: getColor(int) depreciated mDayTextColor = res.getColor(R.color.mdtp_date_picker_text_normal_dark_theme); //TODO: getColor(int) depreciated mMonthDayTextColor = res.getColor(R.color.mdtp_date_picker_month_day_dark_theme); //TODO: getColor(int) depreciated mDisabledDayTextColor = res.getColor(R.color.mdtp_date_picker_text_disabled_dark_theme); //TODO: getColor(int) depreciated mHighlightedDayTextColor = res.getColor(R.color.mdtp_date_picker_text_highlighted_dark_theme); } else { //TODO: getColor(int) depreciated mDayTextColor = res.getColor(R.color.mdtp_date_picker_text_normal); //TODO: getColor(int) depreciated mMonthDayTextColor = res.getColor(R.color.mdtp_date_picker_month_day); //TODO: getColor(int) depreciated mDisabledDayTextColor = res.getColor(R.color.mdtp_date_picker_text_disabled); //TODO: getColor(int) depreciated mHighlightedDayTextColor = res.getColor(R.color.mdtp_date_picker_text_highlighted); } //TODO: getColor(int) depreciated mSelectedDayTextColor = res.getColor(R.color.mdtp_white); //TODO: getColor(int) depreciated mTodayNumberColor = res.getColor(R.color.mdtp_accent_color); //TODO: getColor(int) depreciated mMonthTitleColor = res.getColor(R.color.mdtp_white); mStringBuilder = new StringBuilder(50); mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_day_number_size); MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_label_size); MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_day_label_text_size); MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.mdtp_month_list_item_header_height); DAY_SELECTED_CIRCLE_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_day_number_select_circle_radius); mRowHeight = (res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height) - getMonthHeaderSize()) / MAX_NUM_ROWS; // Set up accessibility components. mTouchHelper = getMonthViewTouchHelper(); 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 setDatePickerController(DatePickerController controller) { mController = controller; } protected MonthViewTouchHelper getMonthViewTouchHelper() { return new MonthViewTouchHelper(this); } @Override public void setAccessibilityDelegate(AccessibilityDelegate delegate) { // Workaround for a JB MR1 issue where accessibility delegates on // top-level ListView items are overwritten. if (!mLockAccessibilityDelegate) { super.setAccessibilityDelegate(delegate); } } public void setOnDayClickListener(OnDayClickListener listener) { mOnDayClickListener = listener; } @Override public boolean dispatchHoverEvent(@NonNull MotionEvent event) { // First right-of-refusal goes the touch exploration helper. return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); } @Override public boolean onTouchEvent(@NonNull 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(mDayTextColor); mMonthTitlePaint.setTextAlign(Align.CENTER); mMonthTitlePaint.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); mMonthDayLabelPaint = new Paint(); mMonthDayLabelPaint.setAntiAlias(true); mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); mMonthDayLabelPaint.setColor(mMonthDayTextColor); mMonthDayLabelPaint.setTypeface(TypefaceHelper.get(getContext(), "Roboto-Medium")); 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); } public void setAccentColor(int color) { mTodayNumberColor = color; mSelectedCirclePaint.setColor(color); } @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); } // 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(); final Calendar today = Calendar.getInstance(); 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 = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH); 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 setSelectedDay(int day) { mSelectedDay = day; } 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, 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) { setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows + getMonthHeaderSize() + 5); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mWidth = w; // Invalidate cached accessibility information. mTouchHelper.invalidateRoot(); } public int getMonth() { return mMonth; } public int getYear() { return mYear; } /** * A wrapper to the MonthHeaderSize to allow override it in children */ protected int getMonthHeaderSize() { return MONTH_HEADER_SIZE; } 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(); return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, null).toString(); } protected void drawMonthTitle(Canvas canvas) { int x = (mWidth + 2 * mEdgePadding) / 2; int y = (getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE) / 2; canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); } protected void drawMonthDayLabels(Canvas canvas) { int y = getMonthHeaderSize() - (MONTH_DAY_LABEL_TEXT_SIZE / 2); int dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2); for (int i = 0; i < mNumDays; i++) { int x = (2 * i + 1) * dayWidthHalf + mEdgePadding; int calendarDay = (i + mWeekStart) % mNumDays; mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); Locale locale = Locale.getDefault(); String localWeekDisplayName = mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, locale); String weekString = localWeekDisplayName.toUpperCase(locale).substring(0, 1); if (locale.equals(Locale.CHINA) || locale.equals(Locale.CHINESE) || locale.equals(Locale.SIMPLIFIED_CHINESE) || locale.equals(Locale.TRADITIONAL_CHINESE)) { int len = localWeekDisplayName.length(); weekString = localWeekDisplayName.substring(len - 1, len); } if (locale.getLanguage().equals("he") || locale.getLanguage().equals("iw")) { if (mDayLabelCalendar.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY) { int len = localWeekDisplayName.length(); weekString = localWeekDisplayName.substring(len - 2, len - 1); } else { // I know this is duplication, but it makes the code easier to grok by // having all hebrew code in the same block weekString = localWeekDisplayName.toUpperCase(locale).substring(0, 1); } } canvas.drawText(weekString, 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) + getMonthHeaderSize(); final float dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2.0f); int j = findDayOffset(); for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) { final int x = (int) ((2 * j + 1) * dayWidthHalf + mEdgePadding); int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH; final int startX = (int) (x - dayWidthHalf); final int stopX = (int) (x + dayWidthHalf); final int startY = y - yRelativeToDay; final int stopY = startY + mRowHeight; drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY); j++; if (j == mNumDays) { j = 0; y += mRowHeight; } } } /** * 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 */ public abstract void drawMonthDay(Canvas canvas, int year, int month, int day, int x, int y, int startX, int stopX, int startY, int stopY); protected 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) { final int day = getInternalDayFromLocation(x, y); if (day < 1 || day > mNumCells) { return -1; } return day; } /** * Calculates the day that the given x position is in, accounting for week * number. * * @param x The x position of the touch event * @return The day number */ protected int getInternalDayFromLocation(float x, float y) { int dayStart = mEdgePadding; if (x < dayStart || x > mWidth - mEdgePadding) { return -1; } // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels int row = (int) (y - getMonthHeaderSize()) / mRowHeight; int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mEdgePadding)); int day = column - findDayOffset() + 1; day += row * mNumDays; return day; } /** * Called when the user clicks on a day. Handles callbacks to the * {@link OnDayClickListener} if one is set. * <p/> * If the day is out of the range set by minDate and/or maxDate, this is a no-op. * * @param day The day that was clicked */ private void onDayClick(int day) { // If the min / max date are set, only process the click if it's a valid selection. if (isOutOfRange(mYear, mMonth, day)) { return; } if (mOnDayClickListener != null) { mOnDayClickListener.onDayClick(this, new MonthAdapter.CalendarDay(mYear, mMonth, day)); } // This is a no-op if accessibility is turned off. mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); } /** * @return true if the specified year/month/day are within the selectable days or the range set by minDate and maxDate. * If one or either have not been set, they are considered as Integer.MIN_VALUE and * Integer.MAX_VALUE. */ protected boolean isOutOfRange(int year, int month, int day) { if (mController.getSelectableDays() != null) { return !isSelectable(year, month, day); } if (isBeforeMin(year, month, day)) { return true; } else if (isAfterMax(year, month, day)) { return true; } return false; } private boolean isSelectable(int year, int month, int day) { Calendar[] selectableDays = mController.getSelectableDays(); for (Calendar c : selectableDays) { if (year < c.get(Calendar.YEAR)) break; if (year > c.get(Calendar.YEAR)) continue; if (month < c.get(Calendar.MONTH)) break; if (month > c.get(Calendar.MONTH)) continue; if (day < c.get(Calendar.DAY_OF_MONTH)) break; if (day > c.get(Calendar.DAY_OF_MONTH)) continue; return true; } return false; } private boolean isBeforeMin(int year, int month, int day) { if (mController == null) { return false; } Calendar minDate = mController.getMinDate(); if (minDate == null) { return false; } if (year < minDate.get(Calendar.YEAR)) { return true; } else if (year > minDate.get(Calendar.YEAR)) { return false; } if (month < minDate.get(Calendar.MONTH)) { return true; } else if (month > minDate.get(Calendar.MONTH)) { return false; } return day < minDate.get(Calendar.DAY_OF_MONTH); } private boolean isAfterMax(int year, int month, int day) { if (mController == null) { return false; } Calendar maxDate = mController.getMaxDate(); if (maxDate == null) { return false; } if (year > maxDate.get(Calendar.YEAR)) { return true; } else if (year < maxDate.get(Calendar.YEAR)) { return false; } if (month > maxDate.get(Calendar.MONTH)) { return true; } else if (month < maxDate.get(Calendar.MONTH)) { return false; } return day > maxDate.get(Calendar.DAY_OF_MONTH); } /** * @param year the year * @param month the month * @param day the day * @return true if the given date should be highlighted */ protected boolean isHighlighted(int year, int month, int day) { Calendar[] highlightedDays = mController.getHighlightedDays(); if (highlightedDays == null) return false; for (Calendar c : highlightedDays) { if (year < c.get(Calendar.YEAR)) break; if (year > c.get(Calendar.YEAR)) continue; if (month < c.get(Calendar.MONTH)) break; if (month > c.get(Calendar.MONTH)) continue; if (day < c.get(Calendar.DAY_OF_MONTH)) break; if (day > c.get(Calendar.DAY_OF_MONTH)) continue; return true; } return false; } /** * @return The date that has accessibility focus, or {@code null} if no date * has focus */ public MonthAdapter.CalendarDay getAccessibilityFocus() { final int day = mTouchHelper.getFocusedVirtualView(); if (day >= 0) { return new MonthAdapter.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(MonthAdapter.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. */ protected 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); //TODO: was node.addAction(AccessibilityNodeInfo.ACTION_CLICK) but that is an api 16 call if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 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 */ protected void getItemBounds(int day, Rect rect) { final int offsetX = mEdgePadding; final int offsetY = getMonthHeaderSize(); final int cellHeight = mRowHeight; final int cellWidth = ((mWidth - (2 * mEdgePadding)) / 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 */ protected 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.mdtp_item_is_selected, date); } return date; } } /** * Handles callbacks when the user clicks on a time object. */ public interface OnDayClickListener { void onDayClick(MonthView view, MonthAdapter.CalendarDay day); } }