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