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.view.accessibility.AccessibilityEvent;
43import android.view.accessibility.AccessibilityNodeInfo;
44import android.widget.AbsListView.OnScrollListener;
45
46import com.android.internal.R;
47
48import java.text.ParseException;
49import java.text.SimpleDateFormat;
50import java.util.Calendar;
51import java.util.Locale;
52import java.util.TimeZone;
53
54import libcore.icu.LocaleData;
55
56/**
57 * This class is a calendar widget for displaying and selecting dates. The range
58 * of dates supported by this calendar is configurable. A user can select a date
59 * by taping on it and can scroll and fling the calendar to a desired date.
60 *
61 * @attr ref android.R.styleable#CalendarView_showWeekNumber
62 * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
63 * @attr ref android.R.styleable#CalendarView_minDate
64 * @attr ref android.R.styleable#CalendarView_maxDate
65 * @attr ref android.R.styleable#CalendarView_shownWeekCount
66 * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
67 * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
68 * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
69 * @attr ref android.R.styleable#CalendarView_weekNumberColor
70 * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
71 * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
72 * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
73 * @attr ref android.R.styleable#CalendarView_dateTextAppearance
74 */
75@Widget
76public class CalendarView extends FrameLayout {
77
78    /**
79     * Tag for logging.
80     */
81    private static final String LOG_TAG = CalendarView.class.getSimpleName();
82
83    /**
84     * Default value whether to show week number.
85     */
86    private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true;
87
88    /**
89     * The number of milliseconds in a day.e
90     */
91    private static final long MILLIS_IN_DAY = 86400000L;
92
93    /**
94     * The number of day in a week.
95     */
96    private static final int DAYS_PER_WEEK = 7;
97
98    /**
99     * The number of milliseconds in a week.
100     */
101    private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY;
102
103    /**
104     * Affects when the month selection will change while scrolling upe
105     */
106    private static final int SCROLL_HYST_WEEKS = 2;
107
108    /**
109     * How long the GoTo fling animation should last.
110     */
111    private static final int GOTO_SCROLL_DURATION = 1000;
112
113    /**
114     * The duration of the adjustment upon a user scroll in milliseconds.
115     */
116    private static final int ADJUSTMENT_SCROLL_DURATION = 500;
117
118    /**
119     * How long to wait after receiving an onScrollStateChanged notification
120     * before acting on it.
121     */
122    private static final int SCROLL_CHANGE_DELAY = 40;
123
124    /**
125     * String for parsing dates.
126     */
127    private static final String DATE_FORMAT = "MM/dd/yyyy";
128
129    /**
130     * The default minimal date.
131     */
132    private static final String DEFAULT_MIN_DATE = "01/01/1900";
133
134    /**
135     * The default maximal date.
136     */
137    private static final String DEFAULT_MAX_DATE = "01/01/2100";
138
139    private static final int DEFAULT_SHOWN_WEEK_COUNT = 6;
140
141    private static final int DEFAULT_DATE_TEXT_SIZE = 14;
142
143    private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6;
144
145    private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12;
146
147    private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2;
148
149    private static final int UNSCALED_BOTTOM_BUFFER = 20;
150
151    private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1;
152
153    private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1;
154
155    private final int mWeekSeperatorLineWidth;
156
157    private int mDateTextSize;
158
159    private Drawable mSelectedDateVerticalBar;
160
161    private final int mSelectedDateVerticalBarWidth;
162
163    private int mSelectedWeekBackgroundColor;
164
165    private int mFocusedMonthDateColor;
166
167    private int mUnfocusedMonthDateColor;
168
169    private int mWeekSeparatorLineColor;
170
171    private int mWeekNumberColor;
172
173    private int mWeekDayTextAppearanceResId;
174
175    private int mDateTextAppearanceResId;
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        TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.CalendarView,
343                R.attr.calendarViewStyle, 0);
344        mShowWeekNumber = attributesArray.getBoolean(R.styleable.CalendarView_showWeekNumber,
345                DEFAULT_SHOW_WEEK_NUMBER);
346        mFirstDayOfWeek = attributesArray.getInt(R.styleable.CalendarView_firstDayOfWeek,
347                LocaleData.get(Locale.getDefault()).firstDayOfWeek);
348        String minDate = attributesArray.getString(R.styleable.CalendarView_minDate);
349        if (TextUtils.isEmpty(minDate) || !parseDate(minDate, mMinDate)) {
350            parseDate(DEFAULT_MIN_DATE, mMinDate);
351        }
352        String maxDate = attributesArray.getString(R.styleable.CalendarView_maxDate);
353        if (TextUtils.isEmpty(maxDate) || !parseDate(maxDate, mMaxDate)) {
354            parseDate(DEFAULT_MAX_DATE, mMaxDate);
355        }
356        if (mMaxDate.before(mMinDate)) {
357            throw new IllegalArgumentException("Max date cannot be before min date.");
358        }
359        mShownWeekCount = attributesArray.getInt(R.styleable.CalendarView_shownWeekCount,
360                DEFAULT_SHOWN_WEEK_COUNT);
361        mSelectedWeekBackgroundColor = attributesArray.getColor(
362                R.styleable.CalendarView_selectedWeekBackgroundColor, 0);
363        mFocusedMonthDateColor = attributesArray.getColor(
364                R.styleable.CalendarView_focusedMonthDateColor, 0);
365        mUnfocusedMonthDateColor = attributesArray.getColor(
366                R.styleable.CalendarView_unfocusedMonthDateColor, 0);
367        mWeekSeparatorLineColor = attributesArray.getColor(
368                R.styleable.CalendarView_weekSeparatorLineColor, 0);
369        mWeekNumberColor = attributesArray.getColor(R.styleable.CalendarView_weekNumberColor, 0);
370        mSelectedDateVerticalBar = attributesArray.getDrawable(
371                R.styleable.CalendarView_selectedDateVerticalBar);
372
373        mDateTextAppearanceResId = attributesArray.getResourceId(
374                R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small);
375        updateDateTextSize();
376
377        mWeekDayTextAppearanceResId = attributesArray.getResourceId(
378                R.styleable.CalendarView_weekDayTextAppearance,
379                DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
380        attributesArray.recycle();
381
382        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
383        mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
384                UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics);
385        mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
386                UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics);
387        mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
388                UNSCALED_BOTTOM_BUFFER, displayMetrics);
389        mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
390                UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics);
391        mWeekSeperatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
392                UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics);
393
394        LayoutInflater layoutInflater = (LayoutInflater) mContext
395                .getSystemService(Service.LAYOUT_INFLATER_SERVICE);
396        View content = layoutInflater.inflate(R.layout.calendar_view, null, false);
397        addView(content);
398
399        mListView = (ListView) findViewById(R.id.list);
400        mDayNamesHeader = (ViewGroup) content.findViewById(com.android.internal.R.id.day_names);
401        mMonthName = (TextView) content.findViewById(com.android.internal.R.id.month_name);
402
403        setUpHeader();
404        setUpListView();
405        setUpAdapter();
406
407        // go to today or whichever is close to today min or max date
408        mTempDate.setTimeInMillis(System.currentTimeMillis());
409        if (mTempDate.before(mMinDate)) {
410            goTo(mMinDate, false, true, true);
411        } else if (mMaxDate.before(mTempDate)) {
412            goTo(mMaxDate, false, true, true);
413        } else {
414            goTo(mTempDate, false, true, true);
415        }
416
417        invalidate();
418    }
419
420    /**
421     * Sets the number of weeks to be shown.
422     *
423     * @param count The shown week count.
424     *
425     * @attr ref android.R.styleable#CalendarView_shownWeekCount
426     */
427    public void setShownWeekCount(int count) {
428        if (mShownWeekCount != count) {
429            mShownWeekCount = count;
430            invalidate();
431        }
432    }
433
434    /**
435     * Gets the number of weeks to be shown.
436     *
437     * @return The shown week count.
438     *
439     * @attr ref android.R.styleable#CalendarView_shownWeekCount
440     */
441    public int getShownWeekCount() {
442        return mShownWeekCount;
443    }
444
445    /**
446     * Sets the background color for the selected week.
447     *
448     * @param color The week background color.
449     *
450     * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
451     */
452    public void setSelectedWeekBackgroundColor(int color) {
453        if (mSelectedWeekBackgroundColor != color) {
454            mSelectedWeekBackgroundColor = color;
455            final int childCount = mListView.getChildCount();
456            for (int i = 0; i < childCount; i++) {
457                WeekView weekView = (WeekView) mListView.getChildAt(i);
458                if (weekView.mHasSelectedDay) {
459                    weekView.invalidate();
460                }
461            }
462        }
463    }
464
465    /**
466     * Gets the background color for the selected week.
467     *
468     * @return The week background color.
469     *
470     * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
471     */
472    public int getSelectedWeekBackgroundColor() {
473        return mSelectedWeekBackgroundColor;
474    }
475
476    /**
477     * Sets the color for the dates of the focused month.
478     *
479     * @param color The focused month date color.
480     *
481     * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
482     */
483    public void setFocusedMonthDateColor(int color) {
484        if (mFocusedMonthDateColor != color) {
485            mFocusedMonthDateColor = color;
486            final int childCount = mListView.getChildCount();
487            for (int i = 0; i < childCount; i++) {
488                WeekView weekView = (WeekView) mListView.getChildAt(i);
489                if (weekView.mHasFocusedDay) {
490                    weekView.invalidate();
491                }
492            }
493        }
494    }
495
496    /**
497     * Gets the color for the dates in the focused month.
498     *
499     * @return The focused month date color.
500     *
501     * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
502     */
503    public int getFocusedMonthDateColor() {
504        return mFocusedMonthDateColor;
505    }
506
507    /**
508     * Sets the color for the dates of a not focused month.
509     *
510     * @param color A not focused month date color.
511     *
512     * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
513     */
514    public void setUnfocusedMonthDateColor(int color) {
515        if (mUnfocusedMonthDateColor != color) {
516            mUnfocusedMonthDateColor = color;
517            final int childCount = mListView.getChildCount();
518            for (int i = 0; i < childCount; i++) {
519                WeekView weekView = (WeekView) mListView.getChildAt(i);
520                if (weekView.mHasUnfocusedDay) {
521                    weekView.invalidate();
522                }
523            }
524        }
525    }
526
527    /**
528     * Gets the color for the dates in a not focused month.
529     *
530     * @return A not focused month date color.
531     *
532     * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
533     */
534    public int getUnfocusedMonthDateColor() {
535        return mFocusedMonthDateColor;
536    }
537
538    /**
539     * Sets the color for the week numbers.
540     *
541     * @param color The week number color.
542     *
543     * @attr ref android.R.styleable#CalendarView_weekNumberColor
544     */
545    public void setWeekNumberColor(int color) {
546        if (mWeekNumberColor != color) {
547            mWeekNumberColor = color;
548            if (mShowWeekNumber) {
549                invalidateAllWeekViews();
550            }
551        }
552    }
553
554    /**
555     * Gets the color for the week numbers.
556     *
557     * @return The week number color.
558     *
559     * @attr ref android.R.styleable#CalendarView_weekNumberColor
560     */
561    public int getWeekNumberColor() {
562        return mWeekNumberColor;
563    }
564
565    /**
566     * Sets the color for the separator line between weeks.
567     *
568     * @param color The week separator color.
569     *
570     * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
571     */
572    public void setWeekSeparatorLineColor(int color) {
573        if (mWeekSeparatorLineColor != color) {
574            mWeekSeparatorLineColor = color;
575            invalidateAllWeekViews();
576        }
577    }
578
579    /**
580     * Gets the color for the separator line between weeks.
581     *
582     * @return The week separator color.
583     *
584     * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
585     */
586    public int getWeekSeparatorLineColor() {
587        return mWeekSeparatorLineColor;
588    }
589
590    /**
591     * Sets the drawable for the vertical bar shown at the beginning and at
592     * the end of the selected date.
593     *
594     * @param resourceId The vertical bar drawable resource id.
595     *
596     * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
597     */
598    public void setSelectedDateVerticalBar(int resourceId) {
599        Drawable drawable = getResources().getDrawable(resourceId);
600        setSelectedDateVerticalBar(drawable);
601    }
602
603    /**
604     * Sets the drawable for the vertical bar shown at the beginning and at
605     * the end of the selected date.
606     *
607     * @param drawable The vertical bar drawable.
608     *
609     * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
610     */
611    public void setSelectedDateVerticalBar(Drawable drawable) {
612        if (mSelectedDateVerticalBar != drawable) {
613            mSelectedDateVerticalBar = drawable;
614            final int childCount = mListView.getChildCount();
615            for (int i = 0; i < childCount; i++) {
616                WeekView weekView = (WeekView) mListView.getChildAt(i);
617                if (weekView.mHasSelectedDay) {
618                    weekView.invalidate();
619                }
620            }
621        }
622    }
623
624    /**
625     * Gets the drawable for the vertical bar shown at the beginning and at
626     * the end of the selected date.
627     *
628     * @return The vertical bar drawable.
629     */
630    public Drawable getSelectedDateVerticalBar() {
631        return mSelectedDateVerticalBar;
632    }
633
634    /**
635     * Sets the text appearance for the week day abbreviation of the calendar header.
636     *
637     * @param resourceId The text appearance resource id.
638     *
639     * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
640     */
641    public void setWeekDayTextAppearance(int resourceId) {
642        if (mWeekDayTextAppearanceResId != resourceId) {
643            mWeekDayTextAppearanceResId = resourceId;
644            setUpHeader();
645        }
646    }
647
648    /**
649     * Gets the text appearance for the week day abbreviation of the calendar header.
650     *
651     * @return The text appearance resource id.
652     *
653     * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
654     */
655    public int getWeekDayTextAppearance() {
656        return mWeekDayTextAppearanceResId;
657    }
658
659    /**
660     * Sets the text appearance for the calendar dates.
661     *
662     * @param resourceId The text appearance resource id.
663     *
664     * @attr ref android.R.styleable#CalendarView_dateTextAppearance
665     */
666    public void setDateTextAppearance(int resourceId) {
667        if (mDateTextAppearanceResId != resourceId) {
668            mDateTextAppearanceResId = resourceId;
669            updateDateTextSize();
670            invalidateAllWeekViews();
671        }
672    }
673
674    /**
675     * Gets the text appearance for the calendar dates.
676     *
677     * @return The text appearance resource id.
678     *
679     * @attr ref android.R.styleable#CalendarView_dateTextAppearance
680     */
681    public int getDateTextAppearance() {
682        return mDateTextAppearanceResId;
683    }
684
685    @Override
686    public void setEnabled(boolean enabled) {
687        mListView.setEnabled(enabled);
688    }
689
690    @Override
691    public boolean isEnabled() {
692        return mListView.isEnabled();
693    }
694
695    @Override
696    protected void onConfigurationChanged(Configuration newConfig) {
697        super.onConfigurationChanged(newConfig);
698        setCurrentLocale(newConfig.locale);
699    }
700
701    @Override
702    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
703        super.onInitializeAccessibilityEvent(event);
704        event.setClassName(CalendarView.class.getName());
705    }
706
707    @Override
708    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
709        super.onInitializeAccessibilityNodeInfo(info);
710        info.setClassName(CalendarView.class.getName());
711    }
712
713    /**
714     * Gets the minimal date supported by this {@link CalendarView} in milliseconds
715     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
716     * zone.
717     * <p>
718     * Note: The default minimal date is 01/01/1900.
719     * <p>
720     *
721     * @return The minimal supported date.
722     *
723     * @attr ref android.R.styleable#CalendarView_minDate
724     */
725    public long getMinDate() {
726        return mMinDate.getTimeInMillis();
727    }
728
729    /**
730     * Sets the minimal date supported by this {@link CalendarView} in milliseconds
731     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
732     * zone.
733     *
734     * @param minDate The minimal supported date.
735     *
736     * @attr ref android.R.styleable#CalendarView_minDate
737     */
738    public void setMinDate(long minDate) {
739        mTempDate.setTimeInMillis(minDate);
740        if (isSameDate(mTempDate, mMinDate)) {
741            return;
742        }
743        mMinDate.setTimeInMillis(minDate);
744        // make sure the current date is not earlier than
745        // the new min date since the latter is used for
746        // calculating the indices in the adapter thus
747        // avoiding out of bounds error
748        Calendar date = mAdapter.mSelectedDate;
749        if (date.before(mMinDate)) {
750            mAdapter.setSelectedDay(mMinDate);
751        }
752        // reinitialize the adapter since its range depends on min date
753        mAdapter.init();
754        if (date.before(mMinDate)) {
755            setDate(mTempDate.getTimeInMillis());
756        } else {
757            // we go to the current date to force the ListView to query its
758            // adapter for the shown views since we have changed the adapter
759            // range and the base from which the later calculates item indices
760            // note that calling setDate will not work since the date is the same
761            goTo(date, false, true, false);
762        }
763    }
764
765    /**
766     * Gets the maximal date supported by this {@link CalendarView} in milliseconds
767     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
768     * zone.
769     * <p>
770     * Note: The default maximal date is 01/01/2100.
771     * <p>
772     *
773     * @return The maximal supported date.
774     *
775     * @attr ref android.R.styleable#CalendarView_maxDate
776     */
777    public long getMaxDate() {
778        return mMaxDate.getTimeInMillis();
779    }
780
781    /**
782     * Sets the maximal date supported by this {@link CalendarView} in milliseconds
783     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
784     * zone.
785     *
786     * @param maxDate The maximal supported date.
787     *
788     * @attr ref android.R.styleable#CalendarView_maxDate
789     */
790    public void setMaxDate(long maxDate) {
791        mTempDate.setTimeInMillis(maxDate);
792        if (isSameDate(mTempDate, mMaxDate)) {
793            return;
794        }
795        mMaxDate.setTimeInMillis(maxDate);
796        // reinitialize the adapter since its range depends on max date
797        mAdapter.init();
798        Calendar date = mAdapter.mSelectedDate;
799        if (date.after(mMaxDate)) {
800            setDate(mMaxDate.getTimeInMillis());
801        } else {
802            // we go to the current date to force the ListView to query its
803            // adapter for the shown views since we have changed the adapter
804            // range and the base from which the later calculates item indices
805            // note that calling setDate will not work since the date is the same
806            goTo(date, false, true, false);
807        }
808    }
809
810    /**
811     * Sets whether to show the week number.
812     *
813     * @param showWeekNumber True to show the week number.
814     *
815     * @attr ref android.R.styleable#CalendarView_showWeekNumber
816     */
817    public void setShowWeekNumber(boolean showWeekNumber) {
818        if (mShowWeekNumber == showWeekNumber) {
819            return;
820        }
821        mShowWeekNumber = showWeekNumber;
822        mAdapter.notifyDataSetChanged();
823        setUpHeader();
824    }
825
826    /**
827     * Gets whether to show the week number.
828     *
829     * @return True if showing the week number.
830     *
831     * @attr ref android.R.styleable#CalendarView_showWeekNumber
832     */
833    public boolean getShowWeekNumber() {
834        return mShowWeekNumber;
835    }
836
837    /**
838     * Gets the first day of week.
839     *
840     * @return The first day of the week conforming to the {@link CalendarView}
841     *         APIs.
842     * @see Calendar#MONDAY
843     * @see Calendar#TUESDAY
844     * @see Calendar#WEDNESDAY
845     * @see Calendar#THURSDAY
846     * @see Calendar#FRIDAY
847     * @see Calendar#SATURDAY
848     * @see Calendar#SUNDAY
849     *
850     * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
851     */
852    public int getFirstDayOfWeek() {
853        return mFirstDayOfWeek;
854    }
855
856    /**
857     * Sets the first day of week.
858     *
859     * @param firstDayOfWeek The first day of the week conforming to the
860     *            {@link CalendarView} APIs.
861     * @see Calendar#MONDAY
862     * @see Calendar#TUESDAY
863     * @see Calendar#WEDNESDAY
864     * @see Calendar#THURSDAY
865     * @see Calendar#FRIDAY
866     * @see Calendar#SATURDAY
867     * @see Calendar#SUNDAY
868     *
869     * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
870     */
871    public void setFirstDayOfWeek(int firstDayOfWeek) {
872        if (mFirstDayOfWeek == firstDayOfWeek) {
873            return;
874        }
875        mFirstDayOfWeek = firstDayOfWeek;
876        mAdapter.init();
877        mAdapter.notifyDataSetChanged();
878        setUpHeader();
879    }
880
881    /**
882     * Sets the listener to be notified upon selected date change.
883     *
884     * @param listener The listener to be notified.
885     */
886    public void setOnDateChangeListener(OnDateChangeListener listener) {
887        mOnDateChangeListener = listener;
888    }
889
890    /**
891     * Gets the selected date in milliseconds since January 1, 1970 00:00:00 in
892     * {@link TimeZone#getDefault()} time zone.
893     *
894     * @return The selected date.
895     */
896    public long getDate() {
897        return mAdapter.mSelectedDate.getTimeInMillis();
898    }
899
900    /**
901     * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
902     * {@link TimeZone#getDefault()} time zone.
903     *
904     * @param date The selected date.
905     *
906     * @throws IllegalArgumentException of the provided date is before the
907     *        minimal or after the maximal date.
908     *
909     * @see #setDate(long, boolean, boolean)
910     * @see #setMinDate(long)
911     * @see #setMaxDate(long)
912     */
913    public void setDate(long date) {
914        setDate(date, false, false);
915    }
916
917    /**
918     * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
919     * {@link TimeZone#getDefault()} time zone.
920     *
921     * @param date The date.
922     * @param animate Whether to animate the scroll to the current date.
923     * @param center Whether to center the current date even if it is already visible.
924     *
925     * @throws IllegalArgumentException of the provided date is before the
926     *        minimal or after the maximal date.
927     *
928     * @see #setMinDate(long)
929     * @see #setMaxDate(long)
930     */
931    public void setDate(long date, boolean animate, boolean center) {
932        mTempDate.setTimeInMillis(date);
933        if (isSameDate(mTempDate, mAdapter.mSelectedDate)) {
934            return;
935        }
936        goTo(mTempDate, animate, true, center);
937    }
938
939    private void updateDateTextSize() {
940        TypedArray dateTextAppearance = getContext().obtainStyledAttributes(
941                mDateTextAppearanceResId, R.styleable.TextAppearance);
942        mDateTextSize = dateTextAppearance.getDimensionPixelSize(
943                R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE);
944        dateTextAppearance.recycle();
945    }
946
947    /**
948     * Invalidates all week views.
949     */
950    private void invalidateAllWeekViews() {
951        final int childCount = mListView.getChildCount();
952        for (int i = 0; i < childCount; i++) {
953            View view = mListView.getChildAt(i);
954            view.invalidate();
955        }
956    }
957
958    /**
959     * Sets the current locale.
960     *
961     * @param locale The current locale.
962     */
963    private void setCurrentLocale(Locale locale) {
964        if (locale.equals(mCurrentLocale)) {
965            return;
966        }
967
968        mCurrentLocale = locale;
969
970        mTempDate = getCalendarForLocale(mTempDate, locale);
971        mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale);
972        mMinDate = getCalendarForLocale(mMinDate, locale);
973        mMaxDate = getCalendarForLocale(mMaxDate, locale);
974    }
975
976    /**
977     * Gets a calendar for locale bootstrapped with the value of a given calendar.
978     *
979     * @param oldCalendar The old calendar.
980     * @param locale The locale.
981     */
982    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
983        if (oldCalendar == null) {
984            return Calendar.getInstance(locale);
985        } else {
986            final long currentTimeMillis = oldCalendar.getTimeInMillis();
987            Calendar newCalendar = Calendar.getInstance(locale);
988            newCalendar.setTimeInMillis(currentTimeMillis);
989            return newCalendar;
990        }
991    }
992
993    /**
994     * @return True if the <code>firstDate</code> is the same as the <code>
995     * secondDate</code>.
996     */
997    private boolean isSameDate(Calendar firstDate, Calendar secondDate) {
998        return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR)
999                && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR));
1000    }
1001
1002    /**
1003     * Creates a new adapter if necessary and sets up its parameters.
1004     */
1005    private void setUpAdapter() {
1006        if (mAdapter == null) {
1007            mAdapter = new WeeksAdapter(getContext());
1008            mAdapter.registerDataSetObserver(new DataSetObserver() {
1009                @Override
1010                public void onChanged() {
1011                    if (mOnDateChangeListener != null) {
1012                        Calendar selectedDay = mAdapter.getSelectedDay();
1013                        mOnDateChangeListener.onSelectedDayChange(CalendarView.this,
1014                                selectedDay.get(Calendar.YEAR),
1015                                selectedDay.get(Calendar.MONTH),
1016                                selectedDay.get(Calendar.DAY_OF_MONTH));
1017                    }
1018                }
1019            });
1020            mListView.setAdapter(mAdapter);
1021        }
1022
1023        // refresh the view with the new parameters
1024        mAdapter.notifyDataSetChanged();
1025    }
1026
1027    /**
1028     * Sets up the strings to be used by the header.
1029     */
1030    private void setUpHeader() {
1031        mDayLabels = new String[mDaysPerWeek];
1032        for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) {
1033            int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i;
1034            mDayLabels[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay,
1035                    DateUtils.LENGTH_SHORTEST);
1036        }
1037
1038        TextView label = (TextView) mDayNamesHeader.getChildAt(0);
1039        if (mShowWeekNumber) {
1040            label.setVisibility(View.VISIBLE);
1041        } else {
1042            label.setVisibility(View.GONE);
1043        }
1044        for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) {
1045            label = (TextView) mDayNamesHeader.getChildAt(i);
1046            if (mWeekDayTextAppearanceResId > -1) {
1047                label.setTextAppearance(mContext, mWeekDayTextAppearanceResId);
1048            }
1049            if (i < mDaysPerWeek + 1) {
1050                label.setText(mDayLabels[i - 1]);
1051                label.setVisibility(View.VISIBLE);
1052            } else {
1053                label.setVisibility(View.GONE);
1054            }
1055        }
1056        mDayNamesHeader.invalidate();
1057    }
1058
1059    /**
1060     * Sets all the required fields for the list view.
1061     */
1062    private void setUpListView() {
1063        // Configure the listview
1064        mListView.setDivider(null);
1065        mListView.setItemsCanFocus(true);
1066        mListView.setVerticalScrollBarEnabled(false);
1067        mListView.setOnScrollListener(new OnScrollListener() {
1068            public void onScrollStateChanged(AbsListView view, int scrollState) {
1069                CalendarView.this.onScrollStateChanged(view, scrollState);
1070            }
1071
1072            public void onScroll(
1073                    AbsListView view, int firstVisibleItem, int visibleItemCount,
1074                    int totalItemCount) {
1075                CalendarView.this.onScroll(view, firstVisibleItem, visibleItemCount,
1076                        totalItemCount);
1077            }
1078        });
1079        // Make the scrolling behavior nicer
1080        mListView.setFriction(mFriction);
1081        mListView.setVelocityScale(mVelocityScale);
1082    }
1083
1084    /**
1085     * This moves to the specified time in the view. If the time is not already
1086     * in range it will move the list so that the first of the month containing
1087     * the time is at the top of the view. If the new time is already in view
1088     * the list will not be scrolled unless forceScroll is true. This time may
1089     * optionally be highlighted as selected as well.
1090     *
1091     * @param date The time to move to.
1092     * @param animate Whether to scroll to the given time or just redraw at the
1093     *            new location.
1094     * @param setSelected Whether to set the given time as selected.
1095     * @param forceScroll Whether to recenter even if the time is already
1096     *            visible.
1097     *
1098     * @throws IllegalArgumentException of the provided date is before the
1099     *        range start of after the range end.
1100     */
1101    private void goTo(Calendar date, boolean animate, boolean setSelected, boolean forceScroll) {
1102        if (date.before(mMinDate) || date.after(mMaxDate)) {
1103            throw new IllegalArgumentException("Time not between " + mMinDate.getTime()
1104                    + " and " + mMaxDate.getTime());
1105        }
1106        // Find the first and last entirely visible weeks
1107        int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
1108        View firstChild = mListView.getChildAt(0);
1109        if (firstChild != null && firstChild.getTop() < 0) {
1110            firstFullyVisiblePosition++;
1111        }
1112        int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1;
1113        if (firstChild != null && firstChild.getTop() > mBottomBuffer) {
1114            lastFullyVisiblePosition--;
1115        }
1116        if (setSelected) {
1117            mAdapter.setSelectedDay(date);
1118        }
1119        // Get the week we're going to
1120        int position = getWeeksSinceMinDate(date);
1121
1122        // Check if the selected day is now outside of our visible range
1123        // and if so scroll to the month that contains it
1124        if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition
1125                || forceScroll) {
1126            mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis());
1127            mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1);
1128
1129            setMonthDisplayed(mFirstDayOfMonth);
1130
1131            // the earliest time we can scroll to is the min date
1132            if (mFirstDayOfMonth.before(mMinDate)) {
1133                position = 0;
1134            } else {
1135                position = getWeeksSinceMinDate(mFirstDayOfMonth);
1136            }
1137
1138            mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
1139            if (animate) {
1140                mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset,
1141                        GOTO_SCROLL_DURATION);
1142            } else {
1143                mListView.setSelectionFromTop(position, mListScrollTopOffset);
1144                // Perform any after scroll operations that are needed
1145                onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE);
1146            }
1147        } else if (setSelected) {
1148            // Otherwise just set the selection
1149            setMonthDisplayed(date);
1150        }
1151    }
1152
1153    /**
1154     * Parses the given <code>date</code> and in case of success sets
1155     * the result to the <code>outDate</code>.
1156     *
1157     * @return True if the date was parsed.
1158     */
1159    private boolean parseDate(String date, Calendar outDate) {
1160        try {
1161            outDate.setTime(mDateFormat.parse(date));
1162            return true;
1163        } catch (ParseException e) {
1164            Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
1165            return false;
1166        }
1167    }
1168
1169    /**
1170     * Called when a <code>view</code> transitions to a new <code>scrollState
1171     * </code>.
1172     */
1173    private void onScrollStateChanged(AbsListView view, int scrollState) {
1174        mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
1175    }
1176
1177    /**
1178     * Updates the title and selected month if the <code>view</code> has moved to a new
1179     * month.
1180     */
1181    private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1182            int totalItemCount) {
1183        WeekView child = (WeekView) view.getChildAt(0);
1184        if (child == null) {
1185            return;
1186        }
1187
1188        // Figure out where we are
1189        long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
1190
1191        // If we have moved since our last call update the direction
1192        if (currScroll < mPreviousScrollPosition) {
1193            mIsScrollingUp = true;
1194        } else if (currScroll > mPreviousScrollPosition) {
1195            mIsScrollingUp = false;
1196        } else {
1197            return;
1198        }
1199
1200        // Use some hysteresis for checking which month to highlight. This
1201        // causes the month to transition when two full weeks of a month are
1202        // visible when scrolling up, and when the first day in a month reaches
1203        // the top of the screen when scrolling down.
1204        int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0;
1205        if (mIsScrollingUp) {
1206            child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset);
1207        } else if (offset != 0) {
1208            child = (WeekView) view.getChildAt(offset);
1209        }
1210
1211        // Find out which month we're moving into
1212        int month;
1213        if (mIsScrollingUp) {
1214            month = child.getMonthOfFirstWeekDay();
1215        } else {
1216            month = child.getMonthOfLastWeekDay();
1217        }
1218
1219        // And how it relates to our current highlighted month
1220        int monthDiff;
1221        if (mCurrentMonthDisplayed == 11 && month == 0) {
1222            monthDiff = 1;
1223        } else if (mCurrentMonthDisplayed == 0 && month == 11) {
1224            monthDiff = -1;
1225        } else {
1226            monthDiff = month - mCurrentMonthDisplayed;
1227        }
1228
1229        // Only switch months if we're scrolling away from the currently
1230        // selected month
1231        if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) {
1232            Calendar firstDay = child.getFirstDay();
1233            if (mIsScrollingUp) {
1234                firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK);
1235            } else {
1236                firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK);
1237            }
1238            setMonthDisplayed(firstDay);
1239        }
1240        mPreviousScrollPosition = currScroll;
1241        mPreviousScrollState = mCurrentScrollState;
1242    }
1243
1244    /**
1245     * Sets the month displayed at the top of this view based on time. Override
1246     * to add custom events when the title is changed.
1247     *
1248     * @param calendar A day in the new focus month.
1249     */
1250    private void setMonthDisplayed(Calendar calendar) {
1251        final int newMonthDisplayed = calendar.get(Calendar.MONTH);
1252        if (mCurrentMonthDisplayed != newMonthDisplayed) {
1253            mCurrentMonthDisplayed = newMonthDisplayed;
1254            mAdapter.setFocusMonth(mCurrentMonthDisplayed);
1255            final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
1256                    | DateUtils.FORMAT_SHOW_YEAR;
1257            final long millis = calendar.getTimeInMillis();
1258            String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags);
1259            mMonthName.setText(newMonthName);
1260            mMonthName.invalidate();
1261        }
1262    }
1263
1264    /**
1265     * @return Returns the number of weeks between the current <code>date</code>
1266     *         and the <code>mMinDate</code>.
1267     */
1268    private int getWeeksSinceMinDate(Calendar date) {
1269        if (date.before(mMinDate)) {
1270            throw new IllegalArgumentException("fromDate: " + mMinDate.getTime()
1271                    + " does not precede toDate: " + date.getTime());
1272        }
1273        long endTimeMillis = date.getTimeInMillis()
1274                + date.getTimeZone().getOffset(date.getTimeInMillis());
1275        long startTimeMillis = mMinDate.getTimeInMillis()
1276                + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis());
1277        long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek)
1278                * MILLIS_IN_DAY;
1279        return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK);
1280    }
1281
1282    /**
1283     * Command responsible for acting upon scroll state changes.
1284     */
1285    private class ScrollStateRunnable implements Runnable {
1286        private AbsListView mView;
1287
1288        private int mNewState;
1289
1290        /**
1291         * Sets up the runnable with a short delay in case the scroll state
1292         * immediately changes again.
1293         *
1294         * @param view The list view that changed state
1295         * @param scrollState The new state it changed to
1296         */
1297        public void doScrollStateChange(AbsListView view, int scrollState) {
1298            mView = view;
1299            mNewState = scrollState;
1300            removeCallbacks(this);
1301            postDelayed(this, SCROLL_CHANGE_DELAY);
1302        }
1303
1304        public void run() {
1305            mCurrentScrollState = mNewState;
1306            // Fix the position after a scroll or a fling ends
1307            if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
1308                    && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
1309                View child = mView.getChildAt(0);
1310                if (child == null) {
1311                    // The view is no longer visible, just return
1312                    return;
1313                }
1314                int dist = child.getBottom() - mListScrollTopOffset;
1315                if (dist > mListScrollTopOffset) {
1316                    if (mIsScrollingUp) {
1317                        mView.smoothScrollBy(dist - child.getHeight(), ADJUSTMENT_SCROLL_DURATION);
1318                    } else {
1319                        mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION);
1320                    }
1321                }
1322            }
1323            mPreviousScrollState = mNewState;
1324        }
1325    }
1326
1327    /**
1328     * <p>
1329     * This is a specialized adapter for creating a list of weeks with
1330     * selectable days. It can be configured to display the week number, start
1331     * the week on a given day, show a reduced number of days, or display an
1332     * arbitrary number of weeks at a time.
1333     * </p>
1334     */
1335    private class WeeksAdapter extends BaseAdapter implements OnTouchListener {
1336
1337        private int mSelectedWeek;
1338
1339        private GestureDetector mGestureDetector;
1340
1341        private int mFocusedMonth;
1342
1343        private final Calendar mSelectedDate = Calendar.getInstance();
1344
1345        private int mTotalWeekCount;
1346
1347        public WeeksAdapter(Context context) {
1348            mContext = context;
1349            mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener());
1350            init();
1351        }
1352
1353        /**
1354         * Set up the gesture detector and selected time
1355         */
1356        private void init() {
1357            mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
1358            mTotalWeekCount = getWeeksSinceMinDate(mMaxDate);
1359            if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek
1360                || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) {
1361                mTotalWeekCount++;
1362            }
1363        }
1364
1365        /**
1366         * Updates the selected day and related parameters.
1367         *
1368         * @param selectedDay The time to highlight
1369         */
1370        public void setSelectedDay(Calendar selectedDay) {
1371            if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR)
1372                    && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) {
1373                return;
1374            }
1375            mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis());
1376            mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
1377            mFocusedMonth = mSelectedDate.get(Calendar.MONTH);
1378            notifyDataSetChanged();
1379        }
1380
1381        /**
1382         * @return The selected day of month.
1383         */
1384        public Calendar getSelectedDay() {
1385            return mSelectedDate;
1386        }
1387
1388        @Override
1389        public int getCount() {
1390            return mTotalWeekCount;
1391        }
1392
1393        @Override
1394        public Object getItem(int position) {
1395            return null;
1396        }
1397
1398        @Override
1399        public long getItemId(int position) {
1400            return position;
1401        }
1402
1403        @Override
1404        public View getView(int position, View convertView, ViewGroup parent) {
1405            WeekView weekView = null;
1406            if (convertView != null) {
1407                weekView = (WeekView) convertView;
1408            } else {
1409                weekView = new WeekView(mContext);
1410                android.widget.AbsListView.LayoutParams params =
1411                    new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT,
1412                            LayoutParams.WRAP_CONTENT);
1413                weekView.setLayoutParams(params);
1414                weekView.setClickable(true);
1415                weekView.setOnTouchListener(this);
1416            }
1417
1418            int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get(
1419                    Calendar.DAY_OF_WEEK) : -1;
1420            weekView.init(position, selectedWeekDay, mFocusedMonth);
1421
1422            return weekView;
1423        }
1424
1425        /**
1426         * Changes which month is in focus and updates the view.
1427         *
1428         * @param month The month to show as in focus [0-11]
1429         */
1430        public void setFocusMonth(int month) {
1431            if (mFocusedMonth == month) {
1432                return;
1433            }
1434            mFocusedMonth = month;
1435            notifyDataSetChanged();
1436        }
1437
1438        @Override
1439        public boolean onTouch(View v, MotionEvent event) {
1440            if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) {
1441                WeekView weekView = (WeekView) v;
1442                // if we cannot find a day for the given location we are done
1443                if (!weekView.getDayFromLocation(event.getX(), mTempDate)) {
1444                    return true;
1445                }
1446                // it is possible that the touched day is outside the valid range
1447                // we draw whole weeks but range end can fall not on the week end
1448                if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
1449                    return true;
1450                }
1451                onDateTapped(mTempDate);
1452                return true;
1453            }
1454            return false;
1455        }
1456
1457        /**
1458         * Maintains the same hour/min/sec but moves the day to the tapped day.
1459         *
1460         * @param day The day that was tapped
1461         */
1462        private void onDateTapped(Calendar day) {
1463            setSelectedDay(day);
1464            setMonthDisplayed(day);
1465        }
1466
1467        /**
1468         * This is here so we can identify single tap events and set the
1469         * selected day correctly
1470         */
1471        class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
1472            @Override
1473            public boolean onSingleTapUp(MotionEvent e) {
1474                return true;
1475            }
1476        }
1477    }
1478
1479    /**
1480     * <p>
1481     * This is a dynamic view for drawing a single week. It can be configured to
1482     * display the week number, start the week on a given day, or show a reduced
1483     * number of days. It is intended for use as a single view within a
1484     * ListView. See {@link WeeksAdapter} for usage.
1485     * </p>
1486     */
1487    private class WeekView extends View {
1488
1489        private final Rect mTempRect = new Rect();
1490
1491        private final Paint mDrawPaint = new Paint();
1492
1493        private final Paint mMonthNumDrawPaint = new Paint();
1494
1495        // Cache the number strings so we don't have to recompute them each time
1496        private String[] mDayNumbers;
1497
1498        // Quick lookup for checking which days are in the focus month
1499        private boolean[] mFocusDay;
1500
1501        // Whether this view has a focused day.
1502        private boolean mHasFocusedDay;
1503
1504        // Whether this view has only focused days.
1505        private boolean mHasUnfocusedDay;
1506
1507        // The first day displayed by this item
1508        private Calendar mFirstDay;
1509
1510        // The month of the first day in this week
1511        private int mMonthOfFirstWeekDay = -1;
1512
1513        // The month of the last day in this week
1514        private int mLastWeekDayMonth = -1;
1515
1516        // The position of this week, equivalent to weeks since the week of Jan
1517        // 1st, 1900
1518        private int mWeek = -1;
1519
1520        // Quick reference to the width of this view, matches parent
1521        private int mWidth;
1522
1523        // The height this view should draw at in pixels, set by height param
1524        private int mHeight;
1525
1526        // If this view contains the selected day
1527        private boolean mHasSelectedDay = false;
1528
1529        // Which day is selected [0-6] or -1 if no day is selected
1530        private int mSelectedDay = -1;
1531
1532        // The number of days + a spot for week number if it is displayed
1533        private int mNumCells;
1534
1535        // The left edge of the selected day
1536        private int mSelectedLeft = -1;
1537
1538        // The right edge of the selected day
1539        private int mSelectedRight = -1;
1540
1541        public WeekView(Context context) {
1542            super(context);
1543
1544            // Sets up any standard paints that will be used
1545            initilaizePaints();
1546        }
1547
1548        /**
1549         * Initializes this week view.
1550         *
1551         * @param weekNumber The number of the week this view represents. The
1552         *            week number is a zero based index of the weeks since
1553         *            {@link CalendarView#getMinDate()}.
1554         * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no
1555         *            selected day.
1556         * @param focusedMonth The month that is currently in focus i.e.
1557         *            highlighted.
1558         */
1559        public void init(int weekNumber, int selectedWeekDay, int focusedMonth) {
1560            mSelectedDay = selectedWeekDay;
1561            mHasSelectedDay = mSelectedDay != -1;
1562            mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek;
1563            mWeek = weekNumber;
1564            mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());
1565
1566            mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek);
1567            mTempDate.setFirstDayOfWeek(mFirstDayOfWeek);
1568
1569            // Allocate space for caching the day numbers and focus values
1570            mDayNumbers = new String[mNumCells];
1571            mFocusDay = new boolean[mNumCells];
1572
1573            // If we're showing the week number calculate it based on Monday
1574            int i = 0;
1575            if (mShowWeekNumber) {
1576                mDayNumbers[0] = String.format(Locale.getDefault(), "%d",
1577                        mTempDate.get(Calendar.WEEK_OF_YEAR));
1578                i++;
1579            }
1580
1581            // Now adjust our starting day based on the start day of the week
1582            int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK);
1583            mTempDate.add(Calendar.DAY_OF_MONTH, diff);
1584
1585            mFirstDay = (Calendar) mTempDate.clone();
1586            mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH);
1587
1588            mHasUnfocusedDay = true;
1589            for (; i < mNumCells; i++) {
1590                final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth);
1591                mFocusDay[i] = isFocusedDay;
1592                mHasFocusedDay |= isFocusedDay;
1593                mHasUnfocusedDay &= !isFocusedDay;
1594                // do not draw dates outside the valid range to avoid user confusion
1595                if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
1596                    mDayNumbers[i] = "";
1597                } else {
1598                    mDayNumbers[i] = String.format(Locale.getDefault(), "%d",
1599                            mTempDate.get(Calendar.DAY_OF_MONTH));
1600                }
1601                mTempDate.add(Calendar.DAY_OF_MONTH, 1);
1602            }
1603            // We do one extra add at the end of the loop, if that pushed us to
1604            // new month undo it
1605            if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) {
1606                mTempDate.add(Calendar.DAY_OF_MONTH, -1);
1607            }
1608            mLastWeekDayMonth = mTempDate.get(Calendar.MONTH);
1609
1610            updateSelectionPositions();
1611        }
1612
1613        /**
1614         * Initialize the paint instances.
1615         */
1616        private void initilaizePaints() {
1617            mDrawPaint.setFakeBoldText(false);
1618            mDrawPaint.setAntiAlias(true);
1619            mDrawPaint.setStyle(Style.FILL);
1620
1621            mMonthNumDrawPaint.setFakeBoldText(true);
1622            mMonthNumDrawPaint.setAntiAlias(true);
1623            mMonthNumDrawPaint.setStyle(Style.FILL);
1624            mMonthNumDrawPaint.setTextAlign(Align.CENTER);
1625            mMonthNumDrawPaint.setTextSize(mDateTextSize);
1626        }
1627
1628        /**
1629         * Returns the month of the first day in this week.
1630         *
1631         * @return The month the first day of this view is in.
1632         */
1633        public int getMonthOfFirstWeekDay() {
1634            return mMonthOfFirstWeekDay;
1635        }
1636
1637        /**
1638         * Returns the month of the last day in this week
1639         *
1640         * @return The month the last day of this view is in
1641         */
1642        public int getMonthOfLastWeekDay() {
1643            return mLastWeekDayMonth;
1644        }
1645
1646        /**
1647         * Returns the first day in this view.
1648         *
1649         * @return The first day in the view.
1650         */
1651        public Calendar getFirstDay() {
1652            return mFirstDay;
1653        }
1654
1655        /**
1656         * Calculates the day that the given x position is in, accounting for
1657         * week number.
1658         *
1659         * @param x The x position of the touch event.
1660         * @return True if a day was found for the given location.
1661         */
1662        public boolean getDayFromLocation(float x, Calendar outCalendar) {
1663            final boolean isLayoutRtl = isLayoutRtl();
1664
1665            int start;
1666            int end;
1667
1668            if (isLayoutRtl) {
1669                start = 0;
1670                end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
1671            } else {
1672                start = mShowWeekNumber ? mWidth / mNumCells : 0;
1673                end = mWidth;
1674            }
1675
1676            if (x < start || x > end) {
1677                outCalendar.clear();
1678                return false;
1679            }
1680
1681            // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels
1682            int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start));
1683
1684            if (isLayoutRtl) {
1685                dayPosition = mDaysPerWeek - 1 - dayPosition;
1686            }
1687
1688            outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis());
1689            outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition);
1690
1691            return true;
1692        }
1693
1694        @Override
1695        protected void onDraw(Canvas canvas) {
1696            drawBackground(canvas);
1697            drawWeekNumbersAndDates(canvas);
1698            drawWeekSeparators(canvas);
1699            drawSelectedDateVerticalBars(canvas);
1700        }
1701
1702        /**
1703         * This draws the selection highlight if a day is selected in this week.
1704         *
1705         * @param canvas The canvas to draw on
1706         */
1707        private void drawBackground(Canvas canvas) {
1708            if (!mHasSelectedDay) {
1709                return;
1710            }
1711            mDrawPaint.setColor(mSelectedWeekBackgroundColor);
1712
1713            mTempRect.top = mWeekSeperatorLineWidth;
1714            mTempRect.bottom = mHeight;
1715
1716            final boolean isLayoutRtl = isLayoutRtl();
1717
1718            if (isLayoutRtl) {
1719                mTempRect.left = 0;
1720                mTempRect.right = mSelectedLeft - 2;
1721            } else {
1722                mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0;
1723                mTempRect.right = mSelectedLeft - 2;
1724            }
1725            canvas.drawRect(mTempRect, mDrawPaint);
1726
1727            if (isLayoutRtl) {
1728                mTempRect.left = mSelectedRight + 3;
1729                mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
1730            } else {
1731                mTempRect.left = mSelectedRight + 3;
1732                mTempRect.right = mWidth;
1733            }
1734            canvas.drawRect(mTempRect, mDrawPaint);
1735        }
1736
1737        /**
1738         * Draws the week and month day numbers for this week.
1739         *
1740         * @param canvas The canvas to draw on
1741         */
1742        private void drawWeekNumbersAndDates(Canvas canvas) {
1743            final float textHeight = mDrawPaint.getTextSize();
1744            final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth;
1745            final int nDays = mNumCells;
1746            final int divisor = 2 * nDays;
1747
1748            mDrawPaint.setTextAlign(Align.CENTER);
1749            mDrawPaint.setTextSize(mDateTextSize);
1750
1751            int i = 0;
1752
1753            if (isLayoutRtl()) {
1754                for (; i < nDays - 1; i++) {
1755                    mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
1756                            : mUnfocusedMonthDateColor);
1757                    int x = (2 * i + 1) * mWidth / divisor;
1758                    canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint);
1759                }
1760                if (mShowWeekNumber) {
1761                    mDrawPaint.setColor(mWeekNumberColor);
1762                    int x = mWidth - mWidth / divisor;
1763                    canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
1764                }
1765            } else {
1766                if (mShowWeekNumber) {
1767                    mDrawPaint.setColor(mWeekNumberColor);
1768                    int x = mWidth / divisor;
1769                    canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
1770                    i++;
1771                }
1772                for (; i < nDays; i++) {
1773                    mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
1774                            : mUnfocusedMonthDateColor);
1775                    int x = (2 * i + 1) * mWidth / divisor;
1776                    canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint);
1777                }
1778            }
1779        }
1780
1781        /**
1782         * Draws a horizontal line for separating the weeks.
1783         *
1784         * @param canvas The canvas to draw on.
1785         */
1786        private void drawWeekSeparators(Canvas canvas) {
1787            // If it is the topmost fully visible child do not draw separator line
1788            int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
1789            if (mListView.getChildAt(0).getTop() < 0) {
1790                firstFullyVisiblePosition++;
1791            }
1792            if (firstFullyVisiblePosition == mWeek) {
1793                return;
1794            }
1795            mDrawPaint.setColor(mWeekSeparatorLineColor);
1796            mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth);
1797            float startX;
1798            float stopX;
1799            if (isLayoutRtl()) {
1800                startX = 0;
1801                stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
1802            } else {
1803                startX = mShowWeekNumber ? mWidth / mNumCells : 0;
1804                stopX = mWidth;
1805            }
1806            canvas.drawLine(startX, 0, stopX, 0, mDrawPaint);
1807        }
1808
1809        /**
1810         * Draws the selected date bars if this week has a selected day.
1811         *
1812         * @param canvas The canvas to draw on
1813         */
1814        private void drawSelectedDateVerticalBars(Canvas canvas) {
1815            if (!mHasSelectedDay) {
1816                return;
1817            }
1818            mSelectedDateVerticalBar.setBounds(mSelectedLeft - mSelectedDateVerticalBarWidth / 2,
1819                    mWeekSeperatorLineWidth,
1820                    mSelectedLeft + mSelectedDateVerticalBarWidth / 2, mHeight);
1821            mSelectedDateVerticalBar.draw(canvas);
1822            mSelectedDateVerticalBar.setBounds(mSelectedRight - mSelectedDateVerticalBarWidth / 2,
1823                    mWeekSeperatorLineWidth,
1824                    mSelectedRight + mSelectedDateVerticalBarWidth / 2, mHeight);
1825            mSelectedDateVerticalBar.draw(canvas);
1826        }
1827
1828        @Override
1829        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1830            mWidth = w;
1831            updateSelectionPositions();
1832        }
1833
1834        /**
1835         * This calculates the positions for the selected day lines.
1836         */
1837        private void updateSelectionPositions() {
1838            if (mHasSelectedDay) {
1839                final boolean isLayoutRtl = isLayoutRtl();
1840                int selectedPosition = mSelectedDay - mFirstDayOfWeek;
1841                if (selectedPosition < 0) {
1842                    selectedPosition += 7;
1843                }
1844                if (mShowWeekNumber && !isLayoutRtl) {
1845                    selectedPosition++;
1846                }
1847                if (isLayoutRtl) {
1848                    mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells;
1849
1850                } else {
1851                    mSelectedLeft = selectedPosition * mWidth / mNumCells;
1852                }
1853                mSelectedRight = mSelectedLeft + mWidth / mNumCells;
1854            }
1855        }
1856
1857        @Override
1858        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1859            mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView
1860                    .getPaddingBottom()) / mShownWeekCount;
1861            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight);
1862        }
1863    }
1864}
1865