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