CalendarView.java revision 76559a65ad9d644f10beacf8895ceb217fdd0aeb
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import com.android.internal.R;
20
21import android.annotation.Widget;
22import android.app.Service;
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.TypedArray;
26import android.database.DataSetObserver;
27import android.graphics.Canvas;
28import android.graphics.Paint;
29import android.graphics.Paint.Align;
30import android.graphics.Paint.Style;
31import android.graphics.Rect;
32import android.graphics.drawable.Drawable;
33import android.text.TextUtils;
34import android.text.format.DateFormat;
35import android.text.format.DateUtils;
36import android.util.AttributeSet;
37import android.util.DisplayMetrics;
38import android.util.Log;
39import android.util.TypedValue;
40import android.view.GestureDetector;
41import android.view.LayoutInflater;
42import android.view.MotionEvent;
43import android.view.View;
44import android.view.ViewGroup;
45import android.widget.AbsListView.OnScrollListener;
46
47import java.text.ParseException;
48import java.text.SimpleDateFormat;
49import java.util.Calendar;
50import java.util.Locale;
51import java.util.TimeZone;
52
53import libcore.icu.LocaleData;
54
55/**
56 * This class is a calendar widget for displaying and selecting dates. The range
57 * of dates supported by this calendar is configurable. A user can select a date
58 * by taping on it and can scroll and fling the calendar to a desired date.
59 *
60 * @attr ref android.R.styleable#CalendarView_showWeekNumber
61 * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
62 * @attr ref android.R.styleable#CalendarView_minDate
63 * @attr ref android.R.styleable#CalendarView_maxDate
64 * @attr ref android.R.styleable#CalendarView_shownWeekCount
65 * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
66 * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
67 * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
68 * @attr ref android.R.styleable#CalendarView_weekNumberColor
69 * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
70 * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
71 * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
72 * @attr ref android.R.styleable#CalendarView_dateTextAppearance
73 */
74@Widget
75public class CalendarView extends FrameLayout {
76
77    /**
78     * Tag for logging.
79     */
80    private static final String LOG_TAG = CalendarView.class.getSimpleName();
81
82    /**
83     * Default value whether to show week number.
84     */
85    private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true;
86
87    /**
88     * The number of milliseconds in a day.e
89     */
90    private static final long MILLIS_IN_DAY = 86400000L;
91
92    /**
93     * The number of day in a week.
94     */
95    private static final int DAYS_PER_WEEK = 7;
96
97    /**
98     * The number of milliseconds in a week.
99     */
100    private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY;
101
102    /**
103     * Affects when the month selection will change while scrolling upe
104     */
105    private static final int SCROLL_HYST_WEEKS = 2;
106
107    /**
108     * How long the GoTo fling animation should last.
109     */
110    private static final int GOTO_SCROLL_DURATION = 1000;
111
112    /**
113     * The duration of the adjustment upon a user scroll in milliseconds.
114     */
115    private static final int ADJUSTMENT_SCROLL_DURATION = 500;
116
117    /**
118     * How long to wait after receiving an onScrollStateChanged notification
119     * before acting on it.
120     */
121    private static final int SCROLL_CHANGE_DELAY = 40;
122
123    /**
124     * String for formatting the month name in the title text view.
125     */
126    private static final String FORMAT_MONTH_NAME = "MMMM, yyyy";
127
128    /**
129     * String for parsing dates.
130     */
131    private static final String DATE_FORMAT = "MM/dd/yyyy";
132
133    /**
134     * The default minimal date.
135     */
136    private static final String DEFAULT_MIN_DATE = "01/01/1900";
137
138    /**
139     * The default maximal date.
140     */
141    private static final String DEFAULT_MAX_DATE = "01/01/2100";
142
143    private static final int DEFAULT_SHOWN_WEEK_COUNT = 6;
144
145    private static final int DEFAULT_DATE_TEXT_SIZE = 14;
146
147    private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6;
148
149    private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12;
150
151    private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2;
152
153    private static final int UNSCALED_BOTTOM_BUFFER = 20;
154
155    private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1;
156
157    private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1;
158
159    private final int mWeekSeperatorLineWidth;
160
161    private final int mDateTextSize;
162
163    private final Drawable mSelectedDateVerticalBar;
164
165    private final int mSelectedDateVerticalBarWidth;
166
167    private final int mSelectedWeekBackgroundColor;
168
169    private final int mFocusedMonthDateColor;
170
171    private final int mUnfocusedMonthDateColor;
172
173    private final int mWeekSeparatorLineColor;
174
175    private final int mWeekNumberColor;
176
177    /**
178     * The top offset of the weeks list.
179     */
180    private int mListScrollTopOffset = 2;
181
182    /**
183     * The visible height of a week view.
184     */
185    private int mWeekMinVisibleHeight = 12;
186
187    /**
188     * The visible height of a week view.
189     */
190    private int mBottomBuffer = 20;
191
192    /**
193     * The number of shown weeks.
194     */
195    private int mShownWeekCount;
196
197    /**
198     * Flag whether to show the week number.
199     */
200    private boolean mShowWeekNumber;
201
202    /**
203     * The number of day per week to be shown.
204     */
205    private int mDaysPerWeek = 7;
206
207    /**
208     * The friction of the week list while flinging.
209     */
210    private float mFriction = .05f;
211
212    /**
213     * Scale for adjusting velocity of the week list while flinging.
214     */
215    private float mVelocityScale = 0.333f;
216
217    /**
218     * The adapter for the weeks list.
219     */
220    private WeeksAdapter mAdapter;
221
222    /**
223     * The weeks list.
224     */
225    private ListView mListView;
226
227    /**
228     * The name of the month to display.
229     */
230    private TextView mMonthName;
231
232    /**
233     * The header with week day names.
234     */
235    private ViewGroup mDayNamesHeader;
236
237    /**
238     * Cached labels for the week names header.
239     */
240    private String[] mDayLabels;
241
242    /**
243     * The first day of the week.
244     */
245    private int mFirstDayOfWeek;
246
247    /**
248     * Which month should be displayed/highlighted [0-11].
249     */
250    private int mCurrentMonthDisplayed;
251
252    /**
253     * Used for tracking during a scroll.
254     */
255    private long mPreviousScrollPosition;
256
257    /**
258     * Used for tracking which direction the view is scrolling.
259     */
260    private boolean mIsScrollingUp = false;
261
262    /**
263     * The previous scroll state of the weeks ListView.
264     */
265    private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;
266
267    /**
268     * The current scroll state of the weeks ListView.
269     */
270    private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;
271
272    /**
273     * Listener for changes in the selected day.
274     */
275    private OnDateChangeListener mOnDateChangeListener;
276
277    /**
278     * Command for adjusting the position after a scroll/fling.
279     */
280    private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();
281
282    /**
283     * Temporary instance to avoid multiple instantiations.
284     */
285    private Calendar mTempDate;
286
287    /**
288     * The first day of the focused month.
289     */
290    private Calendar mFirstDayOfMonth;
291
292    /**
293     * The start date of the range supported by this picker.
294     */
295    private Calendar mMinDate;
296
297    /**
298     * The end date of the range supported by this picker.
299     */
300    private Calendar mMaxDate;
301
302    /**
303     * Date format for parsing dates.
304     */
305    private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
306
307    /**
308     * The current locale.
309     */
310    private Locale mCurrentLocale;
311
312    /**
313     * The callback used to indicate the user changes the date.
314     */
315    public interface OnDateChangeListener {
316
317        /**
318         * Called upon change of the selected day.
319         *
320         * @param view The view associated with this listener.
321         * @param year The year that was set.
322         * @param month The month that was set [0-11].
323         * @param dayOfMonth The day of the month that was set.
324         */
325        public void onSelectedDayChange(CalendarView view, int year, int month, int dayOfMonth);
326    }
327
328    public CalendarView(Context context) {
329        this(context, null);
330    }
331
332    public CalendarView(Context context, AttributeSet attrs) {
333        this(context, attrs, 0);
334    }
335
336    public CalendarView(Context context, AttributeSet attrs, int defStyle) {
337        super(context, attrs, 0);
338
339        // initialization based on locale
340        setCurrentLocale(Locale.getDefault());
341
342        TypedValue calendarViewStyle = new TypedValue();
343        context.getTheme().resolveAttribute(R.attr.calendarViewStyle, calendarViewStyle, true);
344        TypedArray attributesArray = context.obtainStyledAttributes(calendarViewStyle.resourceId,
345                R.styleable.CalendarView);
346        mShowWeekNumber = attributesArray.getBoolean(R.styleable.CalendarView_showWeekNumber,
347                DEFAULT_SHOW_WEEK_NUMBER);
348        mFirstDayOfWeek = attributesArray.getInt(R.styleable.CalendarView_firstDayOfWeek,
349                LocaleData.get(Locale.getDefault()).firstDayOfWeek);
350        String minDate = attributesArray.getString(R.styleable.CalendarView_minDate);
351        if (TextUtils.isEmpty(minDate) || !parseDate(minDate, mMinDate)) {
352            parseDate(DEFAULT_MIN_DATE, mMinDate);
353        }
354        String maxDate = attributesArray.getString(R.styleable.CalendarView_maxDate);
355        if (TextUtils.isEmpty(maxDate) || !parseDate(maxDate, mMaxDate)) {
356            parseDate(DEFAULT_MAX_DATE, mMaxDate);
357        }
358        mShownWeekCount = attributesArray.getInt(R.styleable.CalendarView_shownWeekCount,
359                DEFAULT_SHOWN_WEEK_COUNT);
360        mSelectedWeekBackgroundColor = attributesArray.getColor(
361                R.styleable.CalendarView_selectedWeekBackgroundColor, 0);
362        mFocusedMonthDateColor = attributesArray.getColor(
363                R.styleable.CalendarView_focusedMonthDateColor, 0);
364        mUnfocusedMonthDateColor = attributesArray.getColor(
365                R.styleable.CalendarView_unfocusedMonthDateColor, 0);
366        mWeekSeparatorLineColor = attributesArray.getColor(
367                R.styleable.CalendarView_weekSeparatorLineColor, 0);
368        mWeekNumberColor = attributesArray.getColor(R.styleable.CalendarView_weekNumberColor, 0);
369        mSelectedDateVerticalBar = attributesArray.getDrawable(
370                R.styleable.CalendarView_selectedDateVerticalBar);
371
372        int dateTextAppearanceResId= attributesArray.getResourceId(
373                R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small);
374        TypedArray dateTextAppearance = context.obtainStyledAttributes(dateTextAppearanceResId,
375                com.android.internal.R.styleable.TextAppearance);
376        mDateTextSize = dateTextAppearance.getDimensionPixelSize(
377                R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE);
378        dateTextAppearance.recycle();
379
380        int weekDayTextAppearanceResId = attributesArray.getResourceId(
381                R.styleable.CalendarView_weekDayTextAppearance,
382                DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
383        attributesArray.recycle();
384
385        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
386        mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
387                UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics);
388        mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
389                UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics);
390        mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
391                UNSCALED_BOTTOM_BUFFER, displayMetrics);
392        mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
393                UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics);
394        mWeekSeperatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
395                UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics);
396
397        LayoutInflater layoutInflater = (LayoutInflater) mContext
398                .getSystemService(Service.LAYOUT_INFLATER_SERVICE);
399        View content = layoutInflater.inflate(R.layout.calendar_view, null, false);
400        addView(content);
401
402        mListView = (ListView) findViewById(R.id.list);
403        mDayNamesHeader = (ViewGroup) content.findViewById(com.android.internal.R.id.day_names);
404        mMonthName = (TextView) content.findViewById(com.android.internal.R.id.month_name);
405
406        setUpHeader(weekDayTextAppearanceResId);
407        setUpListView();
408        setUpAdapter();
409
410        // go to today now
411        mTempDate.setTimeInMillis(System.currentTimeMillis());
412        goTo(mTempDate, false, true, true);
413        invalidate();
414    }
415
416    @Override
417    public void setEnabled(boolean enabled) {
418        mListView.setEnabled(enabled);
419    }
420
421    @Override
422    public boolean isEnabled() {
423        return mListView.isEnabled();
424    }
425
426    @Override
427    protected void onConfigurationChanged(Configuration newConfig) {
428        super.onConfigurationChanged(newConfig);
429        setCurrentLocale(newConfig.locale);
430    }
431
432    /**
433     * Gets the minimal date supported by this {@link CalendarView} in milliseconds
434     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
435     * zone.
436     * <p>
437     * Note: The default minimal date is 01/01/1900.
438     * <p>
439     *
440     * @return The minimal supported date.
441     */
442    public long getMinDate() {
443        return mMinDate.getTimeInMillis();
444    }
445
446    /**
447     * Sets the minimal date supported by this {@link CalendarView} in milliseconds
448     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
449     * zone.
450     *
451     * @param minDate The minimal supported date.
452     */
453    public void setMinDate(long minDate) {
454        mTempDate.setTimeInMillis(minDate);
455        if (isSameDate(mTempDate, mMinDate)) {
456            return;
457        }
458        mMinDate.setTimeInMillis(minDate);
459        // make sure the current date is not earlier than
460        // the new min date since the latter is used for
461        // calculating the indices in the adapter thus
462        // avoiding out of bounds error
463        Calendar date = mAdapter.mSelectedDate;
464        if (date.before(mMinDate)) {
465            mAdapter.setSelectedDay(mMinDate);
466        }
467        // reinitialize the adapter since its range depends on min date
468        mAdapter.init();
469        if (date.before(mMinDate)) {
470            setDate(mTempDate.getTimeInMillis());
471        } else {
472            // we go to the current date to force the ListView to query its
473            // adapter for the shown views since we have changed the adapter
474            // range and the base from which the later calculates item indices
475            // note that calling setDate will not work since the date is the same
476            goTo(date, false, true, false);
477        }
478    }
479
480    /**
481     * Gets the maximal date supported by this {@link CalendarView} in milliseconds
482     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
483     * zone.
484     * <p>
485     * Note: The default maximal date is 01/01/2100.
486     * <p>
487     *
488     * @return The maximal supported date.
489     */
490    public long getMaxDate() {
491        return mMaxDate.getTimeInMillis();
492    }
493
494    /**
495     * Sets the maximal date supported by this {@link CalendarView} in milliseconds
496     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
497     * zone.
498     *
499     * @param maxDate The maximal supported date.
500     */
501    public void setMaxDate(long maxDate) {
502        mTempDate.setTimeInMillis(maxDate);
503        if (isSameDate(mTempDate, mMaxDate)) {
504            return;
505        }
506        mMaxDate.setTimeInMillis(maxDate);
507        // reinitialize the adapter since its range depends on max date
508        mAdapter.init();
509        Calendar date = mAdapter.mSelectedDate;
510        if (date.after(mMaxDate)) {
511            setDate(mMaxDate.getTimeInMillis());
512        } else {
513            // we go to the current date to force the ListView to query its
514            // adapter for the shown views since we have changed the adapter
515            // range and the base from which the later calculates item indices
516            // note that calling setDate will not work since the date is the same
517            goTo(date, false, true, false);
518        }
519    }
520
521    /**
522     * Sets whether to show the week number.
523     *
524     * @param showWeekNumber True to show the week number.
525     */
526    public void setShowWeekNumber(boolean showWeekNumber) {
527        if (mShowWeekNumber == showWeekNumber) {
528            return;
529        }
530        mShowWeekNumber = showWeekNumber;
531        mAdapter.notifyDataSetChanged();
532        setUpHeader(DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
533    }
534
535    /**
536     * Gets whether to show the week number.
537     *
538     * @return True if showing the week number.
539     */
540    public boolean getShowWeekNumber() {
541        return mShowWeekNumber;
542    }
543
544    /**
545     * Gets the first day of week.
546     *
547     * @return The first day of the week conforming to the {@link CalendarView}
548     *         APIs.
549     * @see Calendar#MONDAY
550     * @see Calendar#TUESDAY
551     * @see Calendar#WEDNESDAY
552     * @see Calendar#THURSDAY
553     * @see Calendar#FRIDAY
554     * @see Calendar#SATURDAY
555     * @see Calendar#SUNDAY
556     */
557    public int getFirstDayOfWeek() {
558        return mFirstDayOfWeek;
559    }
560
561    /**
562     * Sets the first day of week.
563     *
564     * @param firstDayOfWeek The first day of the week conforming to the
565     *            {@link CalendarView} APIs.
566     * @see Calendar#MONDAY
567     * @see Calendar#TUESDAY
568     * @see Calendar#WEDNESDAY
569     * @see Calendar#THURSDAY
570     * @see Calendar#FRIDAY
571     * @see Calendar#SATURDAY
572     * @see Calendar#SUNDAY
573     */
574    public void setFirstDayOfWeek(int firstDayOfWeek) {
575        if (mFirstDayOfWeek == firstDayOfWeek) {
576            return;
577        }
578        mFirstDayOfWeek = firstDayOfWeek;
579        mAdapter.init();
580        mAdapter.notifyDataSetChanged();
581        setUpHeader(DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
582    }
583
584    /**
585     * Sets the listener to be notified upon selected date change.
586     *
587     * @param listener The listener to be notified.
588     */
589    public void setOnDateChangeListener(OnDateChangeListener listener) {
590        mOnDateChangeListener = listener;
591    }
592
593    /**
594     * Gets the selected date in milliseconds since January 1, 1970 00:00:00 in
595     * {@link TimeZone#getDefault()} time zone.
596     *
597     * @return The selected date.
598     */
599    public long getDate() {
600        return mAdapter.mSelectedDate.getTimeInMillis();
601    }
602
603    /**
604     * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
605     * {@link TimeZone#getDefault()} time zone.
606     *
607     * @param date The selected date.
608     *
609     * @throws IllegalArgumentException of the provided date is before the
610     *        minimal or after the maximal date.
611     *
612     * @see #setDate(long, boolean, boolean)
613     * @see #setMinDate(long)
614     * @see #setMaxDate(long)
615     */
616    public void setDate(long date) {
617        setDate(date, false, false);
618    }
619
620    /**
621     * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
622     * {@link TimeZone#getDefault()} time zone.
623     *
624     * @param date The date.
625     * @param animate Whether to animate the scroll to the current date.
626     * @param center Whether to center the current date even if it is already visible.
627     *
628     * @throws IllegalArgumentException of the provided date is before the
629     *        minimal or after the maximal date.
630     *
631     * @see #setMinDate(long)
632     * @see #setMaxDate(long)
633     */
634    public void setDate(long date, boolean animate, boolean center) {
635        mTempDate.setTimeInMillis(date);
636        if (isSameDate(mTempDate, mAdapter.mSelectedDate)) {
637            return;
638        }
639        goTo(mTempDate, animate, true, center);
640    }
641
642    /**
643     * Sets the current locale.
644     *
645     * @param locale The current locale.
646     */
647    private void setCurrentLocale(Locale locale) {
648        if (locale.equals(mCurrentLocale)) {
649            return;
650        }
651
652        mCurrentLocale = locale;
653
654        mTempDate = getCalendarForLocale(mTempDate, locale);
655        mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale);
656        mMinDate = getCalendarForLocale(mMinDate, locale);
657        mMaxDate = getCalendarForLocale(mMaxDate, locale);
658    }
659
660    /**
661     * Gets a calendar for locale bootstrapped with the value of a given calendar.
662     *
663     * @param oldCalendar The old calendar.
664     * @param locale The locale.
665     */
666    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
667        if (oldCalendar == null) {
668            return Calendar.getInstance(locale);
669        } else {
670            final long currentTimeMillis = oldCalendar.getTimeInMillis();
671            Calendar newCalendar = Calendar.getInstance(locale);
672            newCalendar.setTimeInMillis(currentTimeMillis);
673            return newCalendar;
674        }
675    }
676
677    /**
678     * @return True if the <code>firstDate</code> is the same as the <code>
679     * secondDate</code>.
680     */
681    private boolean isSameDate(Calendar firstDate, Calendar secondDate) {
682        return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR)
683                && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR));
684    }
685
686    /**
687     * Creates a new adapter if necessary and sets up its parameters.
688     */
689    private void setUpAdapter() {
690        if (mAdapter == null) {
691            mAdapter = new WeeksAdapter(getContext());
692            mAdapter.registerDataSetObserver(new DataSetObserver() {
693                @Override
694                public void onChanged() {
695                    if (mOnDateChangeListener != null) {
696                        Calendar selectedDay = mAdapter.getSelectedDay();
697                        mOnDateChangeListener.onSelectedDayChange(CalendarView.this,
698                                selectedDay.get(Calendar.YEAR),
699                                selectedDay.get(Calendar.MONTH),
700                                selectedDay.get(Calendar.DAY_OF_MONTH));
701                    }
702                }
703            });
704            mListView.setAdapter(mAdapter);
705        }
706
707        // refresh the view with the new parameters
708        mAdapter.notifyDataSetChanged();
709    }
710
711    /**
712     * Sets up the strings to be used by the header.
713     */
714    private void setUpHeader(int weekDayTextAppearanceResId) {
715        mDayLabels = new String[mDaysPerWeek];
716        for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) {
717            int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i;
718            mDayLabels[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay,
719                    DateUtils.LENGTH_SHORTEST);
720        }
721
722        TextView label = (TextView) mDayNamesHeader.getChildAt(0);
723        if (mShowWeekNumber) {
724            label.setVisibility(View.VISIBLE);
725        } else {
726            label.setVisibility(View.GONE);
727        }
728        for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) {
729            label = (TextView) mDayNamesHeader.getChildAt(i);
730            if (weekDayTextAppearanceResId > -1) {
731                label.setTextAppearance(mContext, weekDayTextAppearanceResId);
732            }
733            if (i < mDaysPerWeek + 1) {
734                label.setText(mDayLabels[i - 1]);
735                label.setVisibility(View.VISIBLE);
736            } else {
737                label.setVisibility(View.GONE);
738            }
739        }
740        mDayNamesHeader.invalidate();
741    }
742
743    /**
744     * Sets all the required fields for the list view.
745     */
746    private void setUpListView() {
747        // Configure the listview
748        mListView.setDivider(null);
749        mListView.setItemsCanFocus(true);
750        mListView.setVerticalScrollBarEnabled(false);
751        mListView.setOnScrollListener(new OnScrollListener() {
752            public void onScrollStateChanged(AbsListView view, int scrollState) {
753                CalendarView.this.onScrollStateChanged(view, scrollState);
754            }
755
756            public void onScroll(
757                    AbsListView view, int firstVisibleItem, int visibleItemCount,
758                    int totalItemCount) {
759                CalendarView.this.onScroll(view, firstVisibleItem, visibleItemCount,
760                        totalItemCount);
761            }
762        });
763        // Make the scrolling behavior nicer
764        mListView.setFriction(mFriction);
765        mListView.setVelocityScale(mVelocityScale);
766    }
767
768    /**
769     * This moves to the specified time in the view. If the time is not already
770     * in range it will move the list so that the first of the month containing
771     * the time is at the top of the view. If the new time is already in view
772     * the list will not be scrolled unless forceScroll is true. This time may
773     * optionally be highlighted as selected as well.
774     *
775     * @param date The time to move to.
776     * @param animate Whether to scroll to the given time or just redraw at the
777     *            new location.
778     * @param setSelected Whether to set the given time as selected.
779     * @param forceScroll Whether to recenter even if the time is already
780     *            visible.
781     *
782     * @throws IllegalArgumentException of the provided date is before the
783     *        range start of after the range end.
784     */
785    private void goTo(Calendar date, boolean animate, boolean setSelected, boolean forceScroll) {
786        if (date.before(mMinDate) || date.after(mMaxDate)) {
787            throw new IllegalArgumentException("Time not between " + mMinDate.getTime()
788                    + " and " + mMaxDate.getTime());
789        }
790        // Find the first and last entirely visible weeks
791        int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
792        View firstChild = mListView.getChildAt(0);
793        if (firstChild != null && firstChild.getTop() < 0) {
794            firstFullyVisiblePosition++;
795        }
796        int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1;
797        if (firstChild != null && firstChild.getTop() > mBottomBuffer) {
798            lastFullyVisiblePosition--;
799        }
800        if (setSelected) {
801            mAdapter.setSelectedDay(date);
802        }
803        // Get the week we're going to
804        int position = getWeeksSinceMinDate(date);
805
806        // Check if the selected day is now outside of our visible range
807        // and if so scroll to the month that contains it
808        if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition
809                || forceScroll) {
810            mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis());
811            mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1);
812
813            setMonthDisplayed(mFirstDayOfMonth);
814
815            // the earliest time we can scroll to is the min date
816            if (mFirstDayOfMonth.before(mMinDate)) {
817                position = 0;
818            } else {
819                position = getWeeksSinceMinDate(mFirstDayOfMonth);
820            }
821
822            mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
823            if (animate) {
824                mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset,
825                        GOTO_SCROLL_DURATION);
826            } else {
827                mListView.setSelectionFromTop(position, mListScrollTopOffset);
828                // Perform any after scroll operations that are needed
829                onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE);
830            }
831        } else if (setSelected) {
832            // Otherwise just set the selection
833            setMonthDisplayed(date);
834        }
835    }
836
837    /**
838     * Parses the given <code>date</code> and in case of success sets
839     * the result to the <code>outDate</code>.
840     *
841     * @return True if the date was parsed.
842     */
843    private boolean parseDate(String date, Calendar outDate) {
844        try {
845            outDate.setTime(mDateFormat.parse(date));
846            return true;
847        } catch (ParseException e) {
848            Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
849            return false;
850        }
851    }
852
853    /**
854     * Called when a <code>view</code> transitions to a new <code>scrollState
855     * </code>.
856     */
857    private void onScrollStateChanged(AbsListView view, int scrollState) {
858        mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
859    }
860
861    /**
862     * Updates the title and selected month if the <code>view</code> has moved to a new
863     * month.
864     */
865    private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
866            int totalItemCount) {
867        WeekView child = (WeekView) view.getChildAt(0);
868        if (child == null) {
869            return;
870        }
871
872        // Figure out where we are
873        long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
874
875        // If we have moved since our last call update the direction
876        if (currScroll < mPreviousScrollPosition) {
877            mIsScrollingUp = true;
878        } else if (currScroll > mPreviousScrollPosition) {
879            mIsScrollingUp = false;
880        } else {
881            return;
882        }
883
884        // Use some hysteresis for checking which month to highlight. This
885        // causes the month to transition when two full weeks of a month are
886        // visible when scrolling up, and when the first day in a month reaches
887        // the top of the screen when scrolling down.
888        int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0;
889        if (mIsScrollingUp) {
890            child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset);
891        } else if (offset != 0) {
892            child = (WeekView) view.getChildAt(offset);
893        }
894
895        // Find out which month we're moving into
896        int month;
897        if (mIsScrollingUp) {
898            month = child.getMonthOfFirstWeekDay();
899        } else {
900            month = child.getMonthOfLastWeekDay();
901        }
902
903        // And how it relates to our current highlighted month
904        int monthDiff;
905        if (mCurrentMonthDisplayed == 11 && month == 0) {
906            monthDiff = 1;
907        } else if (mCurrentMonthDisplayed == 0 && month == 11) {
908            monthDiff = -1;
909        } else {
910            monthDiff = month - mCurrentMonthDisplayed;
911        }
912
913        // Only switch months if we're scrolling away from the currently
914        // selected month
915        if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) {
916            Calendar firstDay = child.getFirstDay();
917            if (mIsScrollingUp) {
918                firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK);
919            } else {
920                firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK);
921            }
922            setMonthDisplayed(firstDay);
923        }
924        mPreviousScrollPosition = currScroll;
925        mPreviousScrollState = mCurrentScrollState;
926    }
927
928    /**
929     * Sets the month displayed at the top of this view based on time. Override
930     * to add custom events when the title is changed.
931     *
932     * @param calendar A day in the new focus month.
933     */
934    private void setMonthDisplayed(Calendar calendar) {
935        mMonthName.setText(DateFormat.format(FORMAT_MONTH_NAME, calendar));
936        mMonthName.invalidate();
937        mCurrentMonthDisplayed = calendar.get(Calendar.MONTH);
938        mAdapter.setFocusMonth(mCurrentMonthDisplayed);
939        // TODO Send Accessibility Event
940    }
941
942    /**
943     * @return Returns the number of weeks between the current <code>date</code>
944     *         and the <code>mMinDate</code>.
945     */
946    private int getWeeksSinceMinDate(Calendar date) {
947        if (date.before(mMinDate)) {
948            throw new IllegalArgumentException("fromDate: " + mMinDate.getTime()
949                    + " does not precede toDate: " + date.getTime());
950        }
951        long endTimeMillis = date.getTimeInMillis()
952                + date.getTimeZone().getOffset(date.getTimeInMillis());
953        long startTimeMillis = mMinDate.getTimeInMillis()
954                + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis());
955        long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek)
956                * MILLIS_IN_DAY;
957        return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK);
958    }
959
960    /**
961     * Command responsible for acting upon scroll state changes.
962     */
963    private class ScrollStateRunnable implements Runnable {
964        private AbsListView mView;
965
966        private int mNewState;
967
968        /**
969         * Sets up the runnable with a short delay in case the scroll state
970         * immediately changes again.
971         *
972         * @param view The list view that changed state
973         * @param scrollState The new state it changed to
974         */
975        public void doScrollStateChange(AbsListView view, int scrollState) {
976            mView = view;
977            mNewState = scrollState;
978            removeCallbacks(this);
979            postDelayed(this, SCROLL_CHANGE_DELAY);
980        }
981
982        public void run() {
983            mCurrentScrollState = mNewState;
984            // Fix the position after a scroll or a fling ends
985            if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
986                    && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
987                View child = mView.getChildAt(0);
988                if (child == null) {
989                    // The view is no longer visible, just return
990                    return;
991                }
992                int dist = child.getBottom() - mListScrollTopOffset;
993                if (dist > mListScrollTopOffset) {
994                    if (mIsScrollingUp) {
995                        mView.smoothScrollBy(dist - child.getHeight(), ADJUSTMENT_SCROLL_DURATION);
996                    } else {
997                        mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION);
998                    }
999                }
1000            }
1001            mPreviousScrollState = mNewState;
1002        }
1003    }
1004
1005    /**
1006     * <p>
1007     * This is a specialized adapter for creating a list of weeks with
1008     * selectable days. It can be configured to display the week number, start
1009     * the week on a given day, show a reduced number of days, or display an
1010     * arbitrary number of weeks at a time.
1011     * </p>
1012     */
1013    private class WeeksAdapter extends BaseAdapter implements OnTouchListener {
1014
1015        private int mSelectedWeek;
1016
1017        private GestureDetector mGestureDetector;
1018
1019        private int mFocusedMonth;
1020
1021        private final Calendar mSelectedDate = Calendar.getInstance();
1022
1023        private int mTotalWeekCount;
1024
1025        public WeeksAdapter(Context context) {
1026            mContext = context;
1027            mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener());
1028            init();
1029        }
1030
1031        /**
1032         * Set up the gesture detector and selected time
1033         */
1034        private void init() {
1035            mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
1036            mTotalWeekCount = getWeeksSinceMinDate(mMaxDate);
1037            if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek
1038                || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) {
1039                mTotalWeekCount++;
1040            }
1041        }
1042
1043        /**
1044         * Updates the selected day and related parameters.
1045         *
1046         * @param selectedDay The time to highlight
1047         */
1048        public void setSelectedDay(Calendar selectedDay) {
1049            if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR)
1050                    && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) {
1051                return;
1052            }
1053            mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis());
1054            mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
1055            mFocusedMonth = mSelectedDate.get(Calendar.MONTH);
1056            notifyDataSetChanged();
1057        }
1058
1059        /**
1060         * @return The selected day of month.
1061         */
1062        public Calendar getSelectedDay() {
1063            return mSelectedDate;
1064        }
1065
1066        @Override
1067        public int getCount() {
1068            return mTotalWeekCount;
1069        }
1070
1071        @Override
1072        public Object getItem(int position) {
1073            return null;
1074        }
1075
1076        @Override
1077        public long getItemId(int position) {
1078            return position;
1079        }
1080
1081        @Override
1082        public View getView(int position, View convertView, ViewGroup parent) {
1083            WeekView weekView = null;
1084            if (convertView != null) {
1085                weekView = (WeekView) convertView;
1086            } else {
1087                weekView = new WeekView(mContext);
1088                android.widget.AbsListView.LayoutParams params =
1089                    new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT,
1090                            LayoutParams.WRAP_CONTENT);
1091                weekView.setLayoutParams(params);
1092                weekView.setClickable(true);
1093                weekView.setOnTouchListener(this);
1094            }
1095
1096            int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get(
1097                    Calendar.DAY_OF_WEEK) : -1;
1098            weekView.init(position, selectedWeekDay, mFocusedMonth);
1099
1100            return weekView;
1101        }
1102
1103        /**
1104         * Changes which month is in focus and updates the view.
1105         *
1106         * @param month The month to show as in focus [0-11]
1107         */
1108        public void setFocusMonth(int month) {
1109            if (mFocusedMonth == month) {
1110                return;
1111            }
1112            mFocusedMonth = month;
1113            notifyDataSetChanged();
1114        }
1115
1116        @Override
1117        public boolean onTouch(View v, MotionEvent event) {
1118            if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) {
1119                WeekView weekView = (WeekView) v;
1120                // if we cannot find a day for the given location we are done
1121                if (!weekView.getDayFromLocation(event.getX(), mTempDate)) {
1122                    return true;
1123                }
1124                // it is possible that the touched day is outside the valid range
1125                // we draw whole weeks but range end can fall not on the week end
1126                if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
1127                    return true;
1128                }
1129                onDateTapped(mTempDate);
1130                return true;
1131            }
1132            return false;
1133        }
1134
1135        /**
1136         * Maintains the same hour/min/sec but moves the day to the tapped day.
1137         *
1138         * @param day The day that was tapped
1139         */
1140        private void onDateTapped(Calendar day) {
1141            setSelectedDay(day);
1142            setMonthDisplayed(day);
1143        }
1144
1145        /**
1146         * This is here so we can identify single tap events and set the
1147         * selected day correctly
1148         */
1149        class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
1150            @Override
1151            public boolean onSingleTapUp(MotionEvent e) {
1152                return true;
1153            }
1154        }
1155    }
1156
1157    /**
1158     * <p>
1159     * This is a dynamic view for drawing a single week. It can be configured to
1160     * display the week number, start the week on a given day, or show a reduced
1161     * number of days. It is intended for use as a single view within a
1162     * ListView. See {@link WeeksAdapter} for usage.
1163     * </p>
1164     */
1165    private class WeekView extends View {
1166
1167        private final Rect mTempRect = new Rect();
1168
1169        private final Paint mDrawPaint = new Paint();
1170
1171        private final Paint mMonthNumDrawPaint = new Paint();
1172
1173        // Cache the number strings so we don't have to recompute them each time
1174        private String[] mDayNumbers;
1175
1176        // Quick lookup for checking which days are in the focus month
1177        private boolean[] mFocusDay;
1178
1179        // The first day displayed by this item
1180        private Calendar mFirstDay;
1181
1182        // The month of the first day in this week
1183        private int mMonthOfFirstWeekDay = -1;
1184
1185        // The month of the last day in this week
1186        private int mLastWeekDayMonth = -1;
1187
1188        // The position of this week, equivalent to weeks since the week of Jan
1189        // 1st, 1900
1190        private int mWeek = -1;
1191
1192        // Quick reference to the width of this view, matches parent
1193        private int mWidth;
1194
1195        // The height this view should draw at in pixels, set by height param
1196        private int mHeight;
1197
1198        // If this view contains the selected day
1199        private boolean mHasSelectedDay = false;
1200
1201        // Which day is selected [0-6] or -1 if no day is selected
1202        private int mSelectedDay = -1;
1203
1204        // The number of days + a spot for week number if it is displayed
1205        private int mNumCells;
1206
1207        // The left edge of the selected day
1208        private int mSelectedLeft = -1;
1209
1210        // The right edge of the selected day
1211        private int mSelectedRight = -1;
1212
1213        public WeekView(Context context) {
1214            super(context);
1215
1216            mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView
1217                    .getPaddingBottom()) / mShownWeekCount;
1218
1219            // Sets up any standard paints that will be used
1220            setPaintProperties();
1221        }
1222
1223        /**
1224         * Initializes this week view.
1225         *
1226         * @param weekNumber The number of the week this view represents. The
1227         *            week number is a zero based index of the weeks since
1228         *            {@link CalendarView#getMinDate()}.
1229         * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no
1230         *            selected day.
1231         * @param focusedMonth The month that is currently in focus i.e.
1232         *            highlighted.
1233         */
1234        public void init(int weekNumber, int selectedWeekDay, int focusedMonth) {
1235            mSelectedDay = selectedWeekDay;
1236            mHasSelectedDay = mSelectedDay != -1;
1237            mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek;
1238            mWeek = weekNumber;
1239            mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());
1240
1241            mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek);
1242            mTempDate.setFirstDayOfWeek(mFirstDayOfWeek);
1243
1244            // Allocate space for caching the day numbers and focus values
1245            mDayNumbers = new String[mNumCells];
1246            mFocusDay = new boolean[mNumCells];
1247
1248            // If we're showing the week number calculate it based on Monday
1249            int i = 0;
1250            if (mShowWeekNumber) {
1251                mDayNumbers[0] = Integer.toString(mTempDate.get(Calendar.WEEK_OF_YEAR));
1252                i++;
1253            }
1254
1255            // Now adjust our starting day based on the start day of the week
1256            int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK);
1257            mTempDate.add(Calendar.DAY_OF_MONTH, diff);
1258
1259            mFirstDay = (Calendar) mTempDate.clone();
1260            mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH);
1261
1262            for (; i < mNumCells; i++) {
1263                mFocusDay[i] = (mTempDate.get(Calendar.MONTH) == focusedMonth);
1264                // do not draw dates outside the valid range to avoid user confusion
1265                if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
1266                    mDayNumbers[i] = "";
1267                } else {
1268                    mDayNumbers[i] = Integer.toString(mTempDate.get(Calendar.DAY_OF_MONTH));
1269                }
1270                mTempDate.add(Calendar.DAY_OF_MONTH, 1);
1271            }
1272            // We do one extra add at the end of the loop, if that pushed us to
1273            // new month undo it
1274            if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) {
1275                mTempDate.add(Calendar.DAY_OF_MONTH, -1);
1276            }
1277            mLastWeekDayMonth = mTempDate.get(Calendar.MONTH);
1278
1279            updateSelectionPositions();
1280        }
1281
1282        /**
1283         * Sets up the text and style properties for painting.
1284         */
1285        private void setPaintProperties() {
1286            mDrawPaint.setFakeBoldText(false);
1287            mDrawPaint.setAntiAlias(true);
1288            mDrawPaint.setTextSize(mDateTextSize);
1289            mDrawPaint.setStyle(Style.FILL);
1290
1291            mMonthNumDrawPaint.setFakeBoldText(true);
1292            mMonthNumDrawPaint.setAntiAlias(true);
1293            mMonthNumDrawPaint.setTextSize(mDateTextSize);
1294            mMonthNumDrawPaint.setColor(mFocusedMonthDateColor);
1295            mMonthNumDrawPaint.setStyle(Style.FILL);
1296            mMonthNumDrawPaint.setTextAlign(Align.CENTER);
1297        }
1298
1299        /**
1300         * Returns the month of the first day in this week.
1301         *
1302         * @return The month the first day of this view is in.
1303         */
1304        public int getMonthOfFirstWeekDay() {
1305            return mMonthOfFirstWeekDay;
1306        }
1307
1308        /**
1309         * Returns the month of the last day in this week
1310         *
1311         * @return The month the last day of this view is in
1312         */
1313        public int getMonthOfLastWeekDay() {
1314            return mLastWeekDayMonth;
1315        }
1316
1317        /**
1318         * Returns the first day in this view.
1319         *
1320         * @return The first day in the view.
1321         */
1322        public Calendar getFirstDay() {
1323            return mFirstDay;
1324        }
1325
1326        /**
1327         * Calculates the day that the given x position is in, accounting for
1328         * week number.
1329         *
1330         * @param x The x position of the touch event.
1331         * @return True if a day was found for the given location.
1332         */
1333        public boolean getDayFromLocation(float x, Calendar outCalendar) {
1334            int dayStart = mShowWeekNumber ? mWidth / mNumCells : 0;
1335            if (x < dayStart || x > mWidth) {
1336                outCalendar.clear();
1337                return false;
1338            }
1339            // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
1340            int dayPosition = (int) ((x - dayStart) * mDaysPerWeek
1341                    / (mWidth - dayStart));
1342            outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis());
1343            outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition);
1344            return true;
1345        }
1346
1347        @Override
1348        protected void onDraw(Canvas canvas) {
1349            drawBackground(canvas);
1350            drawWeekNumbers(canvas);
1351            drawWeekSeparators(canvas);
1352            drawSelectedDateVerticalBars(canvas);
1353        }
1354
1355        /**
1356         * This draws the selection highlight if a day is selected in this week.
1357         *
1358         * @param canvas The canvas to draw on
1359         */
1360        private void drawBackground(Canvas canvas) {
1361            if (!mHasSelectedDay) {
1362                return;
1363            }
1364            mDrawPaint.setColor(mSelectedWeekBackgroundColor);
1365
1366            mTempRect.top = mWeekSeperatorLineWidth;
1367            mTempRect.bottom = mHeight;
1368            mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0;
1369            mTempRect.right = mSelectedLeft - 2;
1370            canvas.drawRect(mTempRect, mDrawPaint);
1371
1372            mTempRect.left = mSelectedRight + 3;
1373            mTempRect.right = mWidth;
1374            canvas.drawRect(mTempRect, mDrawPaint);
1375        }
1376
1377        /**
1378         * Draws the week and month day numbers for this week.
1379         *
1380         * @param canvas The canvas to draw on
1381         */
1382        private void drawWeekNumbers(Canvas canvas) {
1383            float textHeight = mDrawPaint.getTextSize();
1384            int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth;
1385            int nDays = mNumCells;
1386
1387            mDrawPaint.setTextAlign(Align.CENTER);
1388            int i = 0;
1389            int divisor = 2 * nDays;
1390            if (mShowWeekNumber) {
1391                mDrawPaint.setColor(mWeekNumberColor);
1392                int x = mWidth / divisor;
1393                canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
1394                i++;
1395            }
1396            for (; i < nDays; i++) {
1397                mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
1398                        : mUnfocusedMonthDateColor);
1399                int x = (2 * i + 1) * mWidth / divisor;
1400                canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint);
1401            }
1402        }
1403
1404        /**
1405         * Draws a horizontal line for separating the weeks.
1406         *
1407         * @param canvas The canvas to draw on.
1408         */
1409        private void drawWeekSeparators(Canvas canvas) {
1410            // If it is the topmost fully visible child do not draw separator line
1411            int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
1412            if (mListView.getChildAt(0).getTop() < 0) {
1413                firstFullyVisiblePosition++;
1414            }
1415            if (firstFullyVisiblePosition == mWeek) {
1416                return;
1417            }
1418            mDrawPaint.setColor(mWeekSeparatorLineColor);
1419            mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth);
1420            float x = mShowWeekNumber ? mWidth / mNumCells : 0;
1421            canvas.drawLine(x, 0, mWidth, 0, mDrawPaint);
1422        }
1423
1424        /**
1425         * Draws the selected date bars if this week has a selected day.
1426         *
1427         * @param canvas The canvas to draw on
1428         */
1429        private void drawSelectedDateVerticalBars(Canvas canvas) {
1430            if (!mHasSelectedDay) {
1431                return;
1432            }
1433            mSelectedDateVerticalBar.setBounds(mSelectedLeft - mSelectedDateVerticalBarWidth / 2,
1434                    mWeekSeperatorLineWidth,
1435                    mSelectedLeft + mSelectedDateVerticalBarWidth / 2, mHeight);
1436            mSelectedDateVerticalBar.draw(canvas);
1437            mSelectedDateVerticalBar.setBounds(mSelectedRight - mSelectedDateVerticalBarWidth / 2,
1438                    mWeekSeperatorLineWidth,
1439                    mSelectedRight + mSelectedDateVerticalBarWidth / 2, mHeight);
1440            mSelectedDateVerticalBar.draw(canvas);
1441        }
1442
1443        @Override
1444        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1445            mWidth = w;
1446            updateSelectionPositions();
1447        }
1448
1449        /**
1450         * This calculates the positions for the selected day lines.
1451         */
1452        private void updateSelectionPositions() {
1453            if (mHasSelectedDay) {
1454                int selectedPosition = mSelectedDay - mFirstDayOfWeek;
1455                if (selectedPosition < 0) {
1456                    selectedPosition += 7;
1457                }
1458                if (mShowWeekNumber) {
1459                    selectedPosition++;
1460                }
1461                mSelectedLeft = selectedPosition * mWidth / mNumCells;
1462                mSelectedRight = (selectedPosition + 1) * mWidth / mNumCells;
1463            }
1464        }
1465
1466        @Override
1467        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1468            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight);
1469        }
1470    }
1471}
1472