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