diff options
author | James Kung <kingkung@google.com> | 2013-10-04 19:37:26 -0700 |
---|---|---|
committer | James Kung <kingkung@google.com> | 2013-10-06 10:08:31 -0700 |
commit | e668d6b1b77ac4b127f961150e0d0a8a088143d9 (patch) | |
tree | 96866e9d5483aedf172a8081b9e4fd4a84412524 /src/com/android | |
parent | 16b291818825d611ef14770187616911560dedb4 (diff) | |
download | datetimepicker-e668d6b1b77ac4b127f961150e0d0a8a088143d9.tar.gz |
Abstracting date picker classes to allow customization
Change-Id: Iaf4a61fa2c0c975ffabe21658e654939f0f29782
Diffstat (limited to 'src/com/android')
8 files changed, 989 insertions, 851 deletions
diff --git a/src/com/android/datetimepicker/date/DatePickerController.java b/src/com/android/datetimepicker/date/DatePickerController.java index a4986cc..f89580e 100644 --- a/src/com/android/datetimepicker/date/DatePickerController.java +++ b/src/com/android/datetimepicker/date/DatePickerController.java @@ -17,7 +17,7 @@ package com.android.datetimepicker.date; import com.android.datetimepicker.date.DatePickerDialog.OnDateChangedListener; -import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay; +import com.android.datetimepicker.date.MonthAdapter.CalendarDay; /** * Controller class to communicate among the various components of the date picker dialog. diff --git a/src/com/android/datetimepicker/date/DatePickerDialog.java b/src/com/android/datetimepicker/date/DatePickerDialog.java index a9aff7f..14c6b6d 100644 --- a/src/com/android/datetimepicker/date/DatePickerDialog.java +++ b/src/com/android/datetimepicker/date/DatePickerDialog.java @@ -38,7 +38,7 @@ import android.widget.TextView; import com.android.datetimepicker.HapticFeedbackController; import com.android.datetimepicker.R; import com.android.datetimepicker.Utils; -import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay; +import com.android.datetimepicker.date.MonthAdapter.CalendarDay; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -219,7 +219,7 @@ public class DatePickerDialog extends DialogFragment implements } final Activity activity = getActivity(); - mDayPickerView = new DayPickerView(activity, this); + mDayPickerView = new SimpleDayPickerView(activity, this); mYearPickerView = new YearPickerView(activity, this); Resources res = getResources(); diff --git a/src/com/android/datetimepicker/date/DayPickerView.java b/src/com/android/datetimepicker/date/DayPickerView.java index e57a7fa..b1838da 100644 --- a/src/com/android/datetimepicker/date/DayPickerView.java +++ b/src/com/android/datetimepicker/date/DayPickerView.java @@ -33,7 +33,7 @@ import android.widget.ListView; import com.android.datetimepicker.Utils; import com.android.datetimepicker.date.DatePickerDialog.OnDateChangedListener; -import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay; +import com.android.datetimepicker.date.MonthAdapter.CalendarDay; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -42,7 +42,8 @@ import java.util.Locale; /** * This displays a list of months in a calendar format with selectable days. */ -public class DayPickerView extends ListView implements OnScrollListener, OnDateChangedListener { +public abstract class DayPickerView extends ListView implements OnScrollListener, + OnDateChangedListener { private static final String TAG = "MonthFragment"; @@ -71,11 +72,10 @@ public class DayPickerView extends ListView implements OnScrollListener, OnDateC // highlighted time protected CalendarDay mSelectedDay = new CalendarDay(); - protected SimpleMonthAdapter mAdapter; + protected MonthAdapter mAdapter; protected CalendarDay mTempDay = new CalendarDay(); - private static float mScale = 0; // When the week starts; numbered like Time.<WEEKDAY> (e.g. SUNDAY=0). protected int mFirstDayOfWeek; // The last name announced by accessibility @@ -106,8 +106,7 @@ public class DayPickerView extends ListView implements OnScrollListener, OnDateC public void setController(DatePickerController controller) { mController = controller; mController.registerOnDateChangedListener(this); - setUpAdapter(); - setAdapter(mAdapter); + refreshAdapter(); onDateChanged(); } @@ -121,25 +120,26 @@ public class DayPickerView extends ListView implements OnScrollListener, OnDateC } public void onChange() { - setUpAdapter(); - setAdapter(mAdapter); + refreshAdapter(); } /** * Creates a new adapter if necessary and sets up its parameters. Override * this method to provide a custom adapter. */ - protected void setUpAdapter() { + protected void refreshAdapter() { if (mAdapter == null) { - mAdapter = new SimpleMonthAdapter(getContext(), mController); + mAdapter = createMonthAdapter(getContext(), mController); } else { mAdapter.setSelectedDay(mSelectedDay); - mAdapter.notifyDataSetChanged(); } // refresh the view with the new parameters - mAdapter.notifyDataSetChanged(); + setAdapter(mAdapter); } + public abstract MonthAdapter createMonthAdapter(Context context, + DatePickerController controller); + /* * Sets all the required fields for the list view. Override this method to * set a different list view behavior. @@ -184,7 +184,7 @@ public class DayPickerView extends ListView implements OnScrollListener, OnDateC mTempDay.set(day); final int position = (day.year - mController.getMinYear()) - * SimpleMonthAdapter.MONTHS_IN_YEAR + day.month; + * MonthAdapter.MONTHS_IN_YEAR + day.month; View child; int i = 0; @@ -253,7 +253,7 @@ public class DayPickerView extends ListView implements OnScrollListener, OnDateC @Override public void onScroll( AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - SimpleMonthView child = (SimpleMonthView) view.getChildAt(0); + MonthView child = (MonthView) view.getChildAt(0); if (child == null) { return; } @@ -380,12 +380,12 @@ public class DayPickerView extends ListView implements OnScrollListener, OnDateC final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); - if (child instanceof SimpleMonthView) { - final CalendarDay focus = ((SimpleMonthView) child).getAccessibilityFocus(); + if (child instanceof MonthView) { + final CalendarDay focus = ((MonthView) child).getAccessibilityFocus(); if (focus != null) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { // Clear focus to avoid ListView bug in Jelly Bean MR1. - ((SimpleMonthView) child).clearAccessibilityFocus(); + ((MonthView) child).clearAccessibilityFocus(); } return focus; } @@ -410,8 +410,8 @@ public class DayPickerView extends ListView implements OnScrollListener, OnDateC final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); - if (child instanceof SimpleMonthView) { - if (((SimpleMonthView) child).restoreAccessibilityFocus(day)) { + if (child instanceof MonthView) { + if (((MonthView) child).restoreAccessibilityFocus(day)) { return true; } } diff --git a/src/com/android/datetimepicker/date/MonthAdapter.java b/src/com/android/datetimepicker/date/MonthAdapter.java new file mode 100644 index 0000000..098bb9e --- /dev/null +++ b/src/com/android/datetimepicker/date/MonthAdapter.java @@ -0,0 +1,224 @@ +/* + * 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.android.datetimepicker.date; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.format.Time; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView.LayoutParams; +import android.widget.BaseAdapter; + +import com.android.datetimepicker.date.MonthView.OnDayClickListener; + +import java.util.Calendar; +import java.util.HashMap; + +/** + * An adapter for a list of {@link MonthView} items. + */ +public abstract class MonthAdapter extends BaseAdapter implements OnDayClickListener { + + private static final String TAG = "SimpleMonthAdapter"; + + private final Context mContext; + private final DatePickerController mController; + + private CalendarDay mSelectedDay; + + protected static int WEEK_7_OVERHANG_HEIGHT = 7; + protected static final int MONTHS_IN_YEAR = 12; + + /** + * A convenience class to represent a specific date. + */ + public static class CalendarDay { + private Calendar calendar; + private Time time; + int year; + int month; + int day; + + public CalendarDay() { + setTime(System.currentTimeMillis()); + } + + public CalendarDay(long timeInMillis) { + setTime(timeInMillis); + } + + public CalendarDay(Calendar calendar) { + year = calendar.get(Calendar.YEAR); + month = calendar.get(Calendar.MONTH); + day = calendar.get(Calendar.DAY_OF_MONTH); + } + + public CalendarDay(int year, int month, int day) { + setDay(year, month, day); + } + + public void set(CalendarDay date) { + year = date.year; + month = date.month; + day = date.day; + } + + public void setDay(int year, int month, int day) { + this.year = year; + this.month = month; + this.day = day; + } + + public synchronized void setJulianDay(int julianDay) { + if (time == null) { + time = new Time(); + } + time.setJulianDay(julianDay); + setTime(time.toMillis(false)); + } + + private void setTime(long timeInMillis) { + if (calendar == null) { + calendar = Calendar.getInstance(); + } + calendar.setTimeInMillis(timeInMillis); + month = calendar.get(Calendar.MONTH); + year = calendar.get(Calendar.YEAR); + day = calendar.get(Calendar.DAY_OF_MONTH); + } + } + + public MonthAdapter(Context context, + DatePickerController controller) { + mContext = context; + mController = controller; + init(); + setSelectedDay(mController.getSelectedDay()); + } + + /** + * Updates the selected day and related parameters. + * + * @param day The day to highlight + */ + public void setSelectedDay(CalendarDay day) { + mSelectedDay = day; + notifyDataSetChanged(); + } + + public CalendarDay getSelectedDay() { + return mSelectedDay; + } + + /** + * Set up the gesture detector and selected time + */ + protected void init() { + mSelectedDay = new CalendarDay(System.currentTimeMillis()); + } + + @Override + public int getCount() { + return ((mController.getMaxYear() - mController.getMinYear()) + 1) * MONTHS_IN_YEAR; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @SuppressLint("NewApi") + @SuppressWarnings("unchecked") + @Override + public View getView(int position, View convertView, ViewGroup parent) { + MonthView v; + HashMap<String, Integer> drawingParams = null; + if (convertView != null) { + v = (MonthView) convertView; + // We store the drawing parameters in the view so it can be recycled + drawingParams = (HashMap<String, Integer>) v.getTag(); + } else { + v = createMonthView(mContext); + // Set up the new view + LayoutParams params = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + v.setLayoutParams(params); + v.setClickable(true); + v.setOnDayClickListener(this); + } + if (drawingParams == null) { + drawingParams = new HashMap<String, Integer>(); + } + drawingParams.clear(); + + final int month = position % MONTHS_IN_YEAR; + final int year = position / MONTHS_IN_YEAR + mController.getMinYear(); + + int selectedDay = -1; + if (isSelectedDayInMonth(year, month)) { + selectedDay = mSelectedDay.day; + } + + // Invokes requestLayout() to ensure that the recycled view is set with the appropriate + // height/number of weeks before being displayed. + v.reuse(); + + drawingParams.put(MonthView.VIEW_PARAMS_SELECTED_DAY, selectedDay); + drawingParams.put(MonthView.VIEW_PARAMS_YEAR, year); + drawingParams.put(MonthView.VIEW_PARAMS_MONTH, month); + drawingParams.put(MonthView.VIEW_PARAMS_WEEK_START, mController.getFirstDayOfWeek()); + v.setMonthParams(drawingParams); + v.invalidate(); + return v; + } + + public abstract MonthView createMonthView(Context context); + + private boolean isSelectedDayInMonth(int year, int month) { + return mSelectedDay.year == year && mSelectedDay.month == month; + } + + + @Override + public void onDayClick(MonthView view, CalendarDay day) { + if (day != null) { + onDayTapped(day); + } + } + + /** + * Maintains the same hour/min/sec but moves the day to the tapped day. + * + * @param day The day that was tapped + */ + protected void onDayTapped(CalendarDay day) { + mController.tryVibrate(); + mController.onDayOfMonthSelected(day.year, day.month, day.day); + setSelectedDay(day); + } +} diff --git a/src/com/android/datetimepicker/date/MonthView.java b/src/com/android/datetimepicker/date/MonthView.java new file mode 100644 index 0000000..05e754c --- /dev/null +++ b/src/com/android/datetimepicker/date/MonthView.java @@ -0,0 +1,688 @@ +/* + * 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.android.datetimepicker.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.Bundle; +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.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.datetimepicker.R; +import com.android.datetimepicker.Utils; +import com.android.datetimepicker.date.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 Time#SUNDAY} + * through {@link Time#SATURDAY}. + */ + public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; + /** + * Which day the week should start on. {@link Time#SUNDAY} through + * {@link 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 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"; + + 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; + + 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 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; + 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 mDayTextColor; + protected int mTodayNumberColor; + protected int mMonthTitleColor; + protected int mMonthTitleBGColor; + + 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); + + mDayTextColor = res.getColor(R.color.date_picker_text_normal); + mTodayNumberColor = res.getColor(R.color.blue); + mMonthTitleColor = res.getColor(R.color.white); + mMonthTitleBGColor = res.getColor(R.color.circle_background); + + 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(); + } + + @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. 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); + + mMonthTitleBGPaint = new Paint(); + mMonthTitleBGPaint.setFakeBoldText(true); + mMonthTitleBGPaint.setAntiAlias(true); + mMonthTitleBGPaint.setColor(mMonthTitleBGColor); + 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); + + mMonthDayLabelPaint = new Paint(); + mMonthDayLabelPaint.setAntiAlias(true); + mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); + mMonthDayLabelPaint.setColor(mDayTextColor); + 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); + } + + // 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 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(); + return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, + Time.getCurrentTimezone()).toString(); + } + + 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; + + 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); + + 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); + } +} diff --git a/src/com/android/datetimepicker/date/SimpleDayPickerView.java b/src/com/android/datetimepicker/date/SimpleDayPickerView.java new file mode 100644 index 0000000..658c8a2 --- /dev/null +++ b/src/com/android/datetimepicker/date/SimpleDayPickerView.java @@ -0,0 +1,40 @@ +/* + * 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.android.datetimepicker.date; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * A DayPickerView customized for {@link SimpleMonthAdapter} + */ +public class SimpleDayPickerView extends DayPickerView { + + public SimpleDayPickerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SimpleDayPickerView(Context context, DatePickerController controller) { + super(context, controller); + } + + @Override + public MonthAdapter createMonthAdapter(Context context, DatePickerController controller) { + return new SimpleMonthAdapter(context, controller); + } + +} diff --git a/src/com/android/datetimepicker/date/SimpleMonthAdapter.java b/src/com/android/datetimepicker/date/SimpleMonthAdapter.java index b8d13d8..b49b135 100644 --- a/src/com/android/datetimepicker/date/SimpleMonthAdapter.java +++ b/src/com/android/datetimepicker/date/SimpleMonthAdapter.java @@ -16,209 +16,19 @@ package com.android.datetimepicker.date; -import android.annotation.SuppressLint; import android.content.Context; -import android.text.format.Time; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView.LayoutParams; -import android.widget.BaseAdapter; - -import com.android.datetimepicker.date.SimpleMonthView.OnDayClickListener; - -import java.util.Calendar; -import java.util.HashMap; /** * An adapter for a list of {@link SimpleMonthView} items. */ -public class SimpleMonthAdapter extends BaseAdapter implements OnDayClickListener { - - private static final String TAG = "SimpleMonthAdapter"; - - private final Context mContext; - private final DatePickerController mController; - - private CalendarDay mSelectedDay; - - protected static int WEEK_7_OVERHANG_HEIGHT = 7; - protected static final int MONTHS_IN_YEAR = 12; - - /** - * A convenience class to represent a specific date. - */ - public static class CalendarDay { - private Calendar calendar; - private Time time; - int year; - int month; - int day; - - public CalendarDay() { - setTime(System.currentTimeMillis()); - } - - public CalendarDay(long timeInMillis) { - setTime(timeInMillis); - } - - public CalendarDay(Calendar calendar) { - year = calendar.get(Calendar.YEAR); - month = calendar.get(Calendar.MONTH); - day = calendar.get(Calendar.DAY_OF_MONTH); - } - - public CalendarDay(int year, int month, int day) { - setDay(year, month, day); - } - - public void set(CalendarDay date) { - year = date.year; - month = date.month; - day = date.day; - } - - public void setDay(int year, int month, int day) { - this.year = year; - this.month = month; - this.day = day; - } - - public void setJulianDay(int julianDay) { - if (time == null) { - time = new Time(); - } - time.setJulianDay(julianDay); - setTime(time.toMillis(false)); - } - - private void setTime(long timeInMillis) { - if (calendar == null) { - calendar = Calendar.getInstance(); - } - calendar.setTimeInMillis(timeInMillis); - month = calendar.get(Calendar.MONTH); - year = calendar.get(Calendar.YEAR); - day = calendar.get(Calendar.DAY_OF_MONTH); - } - } - - public SimpleMonthAdapter(Context context, - DatePickerController controller) { - mContext = context; - mController = controller; - init(); - setSelectedDay(mController.getSelectedDay()); - } - - /** - * Updates the selected day and related parameters. - * - * @param day The day to highlight - */ - public void setSelectedDay(CalendarDay day) { - mSelectedDay = day; - notifyDataSetChanged(); - } - - public CalendarDay getSelectedDay() { - return mSelectedDay; - } - - /** - * Set up the gesture detector and selected time - */ - protected void init() { - mSelectedDay = new CalendarDay(System.currentTimeMillis()); - } - - @Override - public int getCount() { - return ((mController.getMaxYear() - mController.getMinYear()) + 1) * MONTHS_IN_YEAR; - } - - @Override - public Object getItem(int position) { - return null; - } - - @Override - public long getItemId(int position) { - return position; - } +public class SimpleMonthAdapter extends MonthAdapter { - @Override - public boolean hasStableIds() { - return true; + public SimpleMonthAdapter(Context context, DatePickerController controller) { + super(context, controller); } - @SuppressLint("NewApi") - @SuppressWarnings("unchecked") @Override - public View getView(int position, View convertView, ViewGroup parent) { - SimpleMonthView v; - HashMap<String, Integer> drawingParams = null; - if (convertView != null) { - v = (SimpleMonthView) convertView; - // We store the drawing parameters in the view so it can be recycled - drawingParams = (HashMap<String, Integer>) v.getTag(); - } else { - v = new SimpleMonthView(mContext); - // Set up the new view - LayoutParams params = new LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - v.setLayoutParams(params); - v.setClickable(true); - v.setOnDayClickListener(this); - } - if (drawingParams == null) { - drawingParams = new HashMap<String, Integer>(); - } - drawingParams.clear(); - - final int month = position % MONTHS_IN_YEAR; - final int year = position / MONTHS_IN_YEAR + mController.getMinYear(); - Log.d(TAG, "Year: " + year + ", Month: " + month); - - int selectedDay = -1; - if (isSelectedDayInMonth(year, month)) { - selectedDay = mSelectedDay.day; - } - - // Invokes requestLayout() to ensure that the recycled view is set with the appropriate - // height/number of weeks before being displayed. - v.reuse(); - - drawingParams.put(SimpleMonthView.VIEW_PARAMS_SELECTED_DAY, selectedDay); - drawingParams.put(SimpleMonthView.VIEW_PARAMS_YEAR, year); - drawingParams.put(SimpleMonthView.VIEW_PARAMS_MONTH, month); - drawingParams.put(SimpleMonthView.VIEW_PARAMS_WEEK_START, mController.getFirstDayOfWeek()); - v.setMonthParams(drawingParams); - v.invalidate(); - return v; - } - - private boolean isSelectedDayInMonth(int year, int month) { - return mSelectedDay.year == year && mSelectedDay.month == month; - } - - - @Override - public void onDayClick(SimpleMonthView view, CalendarDay day) { - if (day != null) { - onDayTapped(day); - } - } - - /** - * Maintains the same hour/min/sec but moves the day to the tapped day. - * - * @param day The day that was tapped - */ - protected void onDayTapped(CalendarDay day) { - mController.tryVibrate(); - mController.onDayOfMonthSelected(day.year, day.month, day.day); - setSelectedDay(day); + public MonthView createMonthView(Context context) { + return new SimpleMonthView(context); } } diff --git a/src/com/android/datetimepicker/date/SimpleMonthView.java b/src/com/android/datetimepicker/date/SimpleMonthView.java index 9d88117..6939fa8 100644 --- a/src/com/android/datetimepicker/date/SimpleMonthView.java +++ b/src/com/android/datetimepicker/date/SimpleMonthView.java @@ -17,651 +17,27 @@ package com.android.datetimepicker.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.Bundle; -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.view.MotionEvent; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import com.android.datetimepicker.R; -import com.android.datetimepicker.Utils; -import com.android.datetimepicker.date.SimpleMonthAdapter.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 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 - */ - 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 Time#SUNDAY} - * through {@link Time#SATURDAY}. - */ - public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; - /** - * Which day the week should start on. {@link Time#SUNDAY} through - * {@link 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 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"; - - 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; - - 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 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; - 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 mDayTextColor; - protected int mTodayNumberColor; - protected int mMonthTitleColor; - protected int mMonthTitleBGColor; +public class SimpleMonthView extends MonthView { public SimpleMonthView(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); - - mDayTextColor = res.getColor(R.color.date_picker_text_normal); - mTodayNumberColor = res.getColor(R.color.blue); - mMonthTitleColor = res.getColor(R.color.white); - mMonthTitleBGColor = res.getColor(R.color.circle_background); - - 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(); - } - - @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; + public void drawMonthDay(Canvas canvas, int year, int month, int day, + int x, int y, int startX, int stopX, int startY, int stopY) { + if (mSelectedDay == day) { + canvas.drawCircle(x , y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE, + mSelectedCirclePaint); } - 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(mDayTextColor); - mMonthTitlePaint.setTextAlign(Align.CENTER); - mMonthTitlePaint.setStyle(Style.FILL); - - mMonthTitleBGPaint = new Paint(); - mMonthTitleBGPaint.setFakeBoldText(true); - mMonthTitleBGPaint.setAntiAlias(true); - mMonthTitleBGPaint.setColor(mMonthTitleBGColor); - 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); - - mMonthDayLabelPaint = new Paint(); - mMonthDayLabelPaint.setAntiAlias(true); - mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); - mMonthDayLabelPaint.setColor(mDayTextColor); - 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 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); + if (mHasToday && mToday == day) { + mMonthNumPaint.setColor(mTodayNumberColor); } 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 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(); - return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, - Time.getCurrentTimezone()).toString(); - } - - 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; - if (mSelectedDay == dayNumber) { - canvas.drawCircle(x, y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE, - mSelectedCirclePaint); - } - - if (mHasToday && mToday == dayNumber) { - mMonthNumPaint.setColor(mTodayNumberColor); - } else { - mMonthNumPaint.setColor(mDayTextColor); - } - canvas.drawText(String.format("%d", dayNumber), x, y, mMonthNumPaint); - 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 - */ - 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(SimpleMonthView.this).performAction( - virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); - } - - public void clearFocusedVirtualView() { - final int focusedVirtualView = getFocusedVirtualView(); - if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { - getAccessibilityNodeProvider(SimpleMonthView.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)); + mMonthNumPaint.setColor(mDayTextColor); } - - @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(SimpleMonthView view, CalendarDay day); + canvas.drawText(String.format("%d", day), x, y, mMonthNumPaint); } } |