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