From bd9152f6ee156ee473f05f6f05f238605996fca4 Mon Sep 17 00:00:00 2001 From: Fabrice Di Meglio Date: Tue, 1 Oct 2013 11:21:31 -0700 Subject: Update DatePicker widget and its related dialog - the old DatePicker widget is still there for obvious layout compatibility reasons - add a new delegate implementation for having a new UI - use the new delegate only for the DatePickerDialog (which does not need to be the same) - added support for Theming and light/dark Themes - added support for RTL - added support for Accessibility - verified support for Keyboard - verified that CTS tests for DatePicker are passing (for both the legacy and the new widgets) Also added a new HapticFeedbackConstants.CALENDAR_DATE and its related code for enabling day selection vibration Change-Id: I256bd7c21edd8f3b910413ca15ce26d3a5ef7d9c --- core/java/android/widget/SimpleMonthView.java | 720 ++++++++++++++++++++++++++ 1 file changed, 720 insertions(+) create mode 100644 core/java/android/widget/SimpleMonthView.java (limited to 'core/java/android/widget/SimpleMonthView.java') diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java new file mode 100644 index 0000000..7589711 --- /dev/null +++ b/core/java/android/widget/SimpleMonthView.java @@ -0,0 +1,720 @@ +/* + * 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.content.Context; +import android.content.res.ColorStateList; +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.Bundle; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.text.format.Time; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.internal.R; +import com.android.internal.widget.ExploreByTouchHelper; + +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. + */ +class SimpleMonthView extends View { + private static final String TAG = "SimpleMonthView"; + + /** + * 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 + */ + static final String VIEW_PARAMS_HEIGHT = "height"; + /** + * This specifies the position (or weeks since the epoch) of this week, + * calculated using + */ + static final String VIEW_PARAMS_MONTH = "month"; + /** + * This specifies the position (or weeks since the epoch) of this week, + * calculated using + */ + static final String VIEW_PARAMS_YEAR = "year"; + /** + * This sets one of the days in this view as selected {@link Time#SUNDAY} + * through {@link Time#SATURDAY}. + */ + static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; + /** + * Which day the week should start on. {@link Time#SUNDAY} through + * {@link Time#SATURDAY}. + */ + static final String VIEW_PARAMS_WEEK_START = "week_start"; + /** + * First enabled day. + */ + static final String VIEW_PARAMS_ENABLEDDAYRANGE_START = "enabled_day_range_start"; + /** + * Last enabled day. + */ + static final String VIEW_PARAMS_ENABLEDDAYRANGE_END = "enabled_day_range_end"; + + private static int DEFAULT_HEIGHT = 32; + private static int MIN_HEIGHT = 10; + + private static final int DEFAULT_SELECTED_DAY = -1; + private static final int DEFAULT_WEEK_START = Calendar.SUNDAY; + private static final int DEFAULT_NUM_DAYS = 7; + private static final int DEFAULT_NUM_ROWS = 6; + private static final int MAX_NUM_ROWS = 6; + + private static final int SELECTED_CIRCLE_ALPHA = 60; + + private static int DAY_SEPARATOR_WIDTH = 1; + + private int mMiniDayNumberTextSize; + private int mMonthLabelTextSize; + private int mMonthDayLabelTextSize; + private int mMonthHeaderSize; + private int mDaySelectedCircleSize; + + // used for scaling to the device density + private static float mScale = 0; + + // affects the padding on the sides of this view + private int mPadding = 0; + + private String mDayOfWeekTypeface; + private String mMonthTitleTypeface; + + private Paint mDayNumberPaint; + private Paint mDayNumberDisabledPaint; + private Paint mDayNumberSelectedPaint; + + private Paint mMonthTitlePaint; + private Paint mMonthDayLabelPaint; + + private final Formatter mFormatter; + private final StringBuilder mStringBuilder; + + private int mMonth; + private int mYear; + + // Quick reference to the width of this view, matches parent + private int mWidth; + + // The height this view should draw at in pixels, set by height param + private int mRowHeight = DEFAULT_HEIGHT; + + // If this view contains the today + private boolean mHasToday = false; + + // Which day is selected [0-6] or -1 if no day is selected + private int mSelectedDay = -1; + + // Which day is today [0-6] or -1 if no day is today + private int mToday = DEFAULT_SELECTED_DAY; + + // Which day of the week to start on [0-6] + private int mWeekStart = DEFAULT_WEEK_START; + + // How many days to display + private int mNumDays = DEFAULT_NUM_DAYS; + + // The number of days + a spot for week number if it is displayed + private int mNumCells = mNumDays; + + private int mDayOfWeekStart = 0; + + // First enabled day + private int mEnabledDayStart = 1; + + // Last enabled day + private int mEnabledDayEnd = 31; + + private final Calendar mCalendar = Calendar.getInstance(); + private final Calendar mDayLabelCalendar = Calendar.getInstance(); + + 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; + + private int mNormalTextColor; + private int mDisabledTextColor; + private int mSelectedDayColor; + + 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 defStyle) { + super(context, attrs); + + final Resources res = context.getResources(); + + mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface); + mMonthTitleTypeface = res.getString(R.string.sans_serif); + + mStringBuilder = new StringBuilder(50); + mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); + + mMiniDayNumberTextSize = res.getDimensionPixelSize(R.dimen.datepicker_day_number_size); + mMonthLabelTextSize = res.getDimensionPixelSize(R.dimen.datepicker_month_label_size); + mMonthDayLabelTextSize = res.getDimensionPixelSize( + R.dimen.datepicker_month_day_label_text_size); + mMonthHeaderSize = res.getDimensionPixelOffset( + R.dimen.datepicker_month_list_item_header_height); + mDaySelectedCircleSize = res.getDimensionPixelSize( + R.dimen.datepicker_day_number_select_circle_radius); + + mRowHeight = (res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height) + - mMonthHeaderSize) / MAX_NUM_ROWS; + + // Set up accessibility components. + mTouchHelper = new MonthViewTouchHelper(this); + setAccessibilityDelegate(mTouchHelper); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + mLockAccessibilityDelegate = true; + + // Sets up any standard paints that will be used + initView(); + } + + void setTextColor(ColorStateList colors) { + final Resources res = getContext().getResources(); + + mNormalTextColor = colors.getColorForState(ENABLED_STATE_SET, + res.getColor(R.color.datepicker_default_normal_text_color_holo_light)); + mMonthTitlePaint.setColor(mNormalTextColor); + mMonthDayLabelPaint.setColor(mNormalTextColor); + + mDisabledTextColor = colors.getColorForState(EMPTY_STATE_SET, + res.getColor(R.color.datepicker_default_disabled_text_color_holo_light)); + mDayNumberDisabledPaint.setColor(mDisabledTextColor); + + mSelectedDayColor = colors.getColorForState(ENABLED_SELECTED_STATE_SET, + res.getColor(R.color.holo_blue_light)); + mDayNumberSelectedPaint.setColor(mSelectedDayColor); + mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); + } + + @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(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. + */ + private void initView() { + mMonthTitlePaint = new Paint(); + mMonthTitlePaint.setAntiAlias(true); + mMonthTitlePaint.setColor(mNormalTextColor); + mMonthTitlePaint.setTextSize(mMonthLabelTextSize); + mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD)); + mMonthTitlePaint.setTextAlign(Align.CENTER); + mMonthTitlePaint.setStyle(Style.FILL); + mMonthTitlePaint.setFakeBoldText(true); + + mMonthDayLabelPaint = new Paint(); + mMonthDayLabelPaint.setAntiAlias(true); + mMonthDayLabelPaint.setColor(mNormalTextColor); + mMonthDayLabelPaint.setTextSize(mMonthDayLabelTextSize); + mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL)); + mMonthDayLabelPaint.setTextAlign(Align.CENTER); + mMonthDayLabelPaint.setStyle(Style.FILL); + mMonthDayLabelPaint.setFakeBoldText(true); + + mDayNumberSelectedPaint = new Paint(); + mDayNumberSelectedPaint.setAntiAlias(true); + mDayNumberSelectedPaint.setColor(mSelectedDayColor); + mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); + mDayNumberSelectedPaint.setTextAlign(Align.CENTER); + mDayNumberSelectedPaint.setStyle(Style.FILL); + mDayNumberSelectedPaint.setFakeBoldText(true); + + mDayNumberPaint = new Paint(); + mDayNumberPaint.setAntiAlias(true); + mDayNumberPaint.setTextSize(mMiniDayNumberTextSize); + mDayNumberPaint.setTextAlign(Align.CENTER); + mDayNumberPaint.setStyle(Style.FILL); + mDayNumberPaint.setFakeBoldText(false); + + mDayNumberDisabledPaint = new Paint(); + mDayNumberDisabledPaint.setAntiAlias(true); + mDayNumberDisabledPaint.setColor(mDisabledTextColor); + mDayNumberDisabledPaint.setTextSize(mMiniDayNumberTextSize); + mDayNumberDisabledPaint.setTextAlign(Align.CENTER); + mDayNumberDisabledPaint.setStyle(Style.FILL); + mDayNumberDisabledPaint.setFakeBoldText(false); + } + + @Override + protected void onDraw(Canvas canvas) { + drawMonthTitle(canvas); + drawWeekDayLabels(canvas); + drawDays(canvas); + } + + /** + * 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} + */ + void setMonthParams(HashMap params) { + if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { + throw new InvalidParameterException( + "You must specify the 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(); + 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(); + } + + if (params.containsKey(VIEW_PARAMS_ENABLEDDAYRANGE_START)) { + mEnabledDayStart = params.get(VIEW_PARAMS_ENABLEDDAYRANGE_START); + } + if (params.containsKey(VIEW_PARAMS_ENABLEDDAYRANGE_END)) { + mEnabledDayEnd = params.get(VIEW_PARAMS_ENABLEDDAYRANGE_END); + } + + mNumCells = 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(); + } + + 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"); + } + } + + 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 + + mMonthHeaderSize); + } + + @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(); + return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, + Time.getCurrentTimezone()).toString(); + } + + private void drawMonthTitle(Canvas canvas) { + int x = (mWidth + 2 * mPadding) / 2; + int y = (mMonthHeaderSize - mMonthDayLabelTextSize) / 2 + (mMonthLabelTextSize / 3); + canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); + } + + private void drawWeekDayLabels(Canvas canvas) { + int y = mMonthHeaderSize - (mMonthDayLabelTextSize / 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 month days. + */ + private void drawDays(Canvas canvas) { + int y = (((mRowHeight + mMiniDayNumberTextSize) / 2) - DAY_SEPARATOR_WIDTH) + + mMonthHeaderSize; + int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); + int j = findDayOffset(); + for (int day = 1; day <= mNumCells; day++) { + int x = (2 * j + 1) * dayWidthHalf + mPadding; + if (mSelectedDay == day) { + canvas.drawCircle(x, y - (mMiniDayNumberTextSize / 3), mDaySelectedCircleSize, + mDayNumberSelectedPaint); + } + + if (mHasToday && mToday == day) { + mDayNumberPaint.setColor(mSelectedDayColor); + } else { + mDayNumberPaint.setColor(mNormalTextColor); + } + final Paint paint = (day < mEnabledDayStart || day > mEnabledDayEnd) ? + mDayNumberDisabledPaint : mDayNumberPaint; + canvas.drawText(String.format("%d", day), x, y, paint); + j++; + if (j == mNumDays) { + j = 0; + y += mRowHeight; + } + } + } + + 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 + */ + private 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 - mMonthHeaderSize) / 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) { + 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 The date that has accessibility focus, or {@code null} if no date + * has focus + */ + Calendar getAccessibilityFocus() { + final int day = mTouchHelper.getFocusedVirtualView(); + Calendar date = null; + if (day >= 0) { + date = Calendar.getInstance(); + date.set(mYear, mMonth, day); + } + return date; + } + + /** + * 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 + */ + boolean restoreAccessibilityFocus(Calendar day) { + if ((day.get(Calendar.YEAR) != mYear) || (day.get(Calendar.MONTH) != mMonth) || + (day.get(Calendar.DAY_OF_MONTH) > mNumCells)) { + return false; + } + mTouchHelper.setFocusedVirtualView(day.get(Calendar.DAY_OF_MONTH)); + 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(SimpleMonthView.this).performAction( + virtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + } + + public void clearFocusedVirtualView() { + final int focusedVirtualView = getFocusedVirtualView(); + if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { + getAccessibilityNodeProvider(SimpleMonthView.this).performAction( + focusedVirtualView, + AccessibilityNodeInfo.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 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, AccessibilityNodeInfo 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 = mMonthHeaderSize; + 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(SimpleMonthView view, Calendar day); + } +} -- cgit v1.1