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