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 
17 package android.widget;
18 
19 import android.app.Service;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.content.res.TypedArray;
23 import android.database.DataSetObserver;
24 import android.graphics.Canvas;
25 import android.graphics.Paint;
26 import android.graphics.Rect;
27 import android.graphics.drawable.Drawable;
28 import android.icu.util.Calendar;
29 import android.text.format.DateUtils;
30 import android.util.AttributeSet;
31 import android.util.DisplayMetrics;
32 import android.util.TypedValue;
33 import android.view.GestureDetector;
34 import android.view.LayoutInflater;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.ViewGroup;
38 
39 import com.android.internal.R;
40 
41 import libcore.icu.LocaleData;
42 
43 import java.util.Locale;
44 
45 /**
46  * A delegate implementing the legacy CalendarView
47  */
48 class 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 
CalendarViewLegacyDelegate(CalendarView delegator, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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
setShownWeekCount(int count)341     public void setShownWeekCount(int count) {
342         if (mShownWeekCount != count) {
343             mShownWeekCount = count;
344             mDelegator.invalidate();
345         }
346     }
347 
348     @Override
getShownWeekCount()349     public int getShownWeekCount() {
350         return mShownWeekCount;
351     }
352 
353     @Override
setSelectedWeekBackgroundColor(int color)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
getSelectedWeekBackgroundColor()368     public int getSelectedWeekBackgroundColor() {
369         return mSelectedWeekBackgroundColor;
370     }
371 
372     @Override
setFocusedMonthDateColor(int color)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
getFocusedMonthDateColor()387     public int getFocusedMonthDateColor() {
388         return mFocusedMonthDateColor;
389     }
390 
391     @Override
setUnfocusedMonthDateColor(int color)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
getUnfocusedMonthDateColor()406     public int getUnfocusedMonthDateColor() {
407         return mUnfocusedMonthDateColor;
408     }
409 
410     @Override
setWeekNumberColor(int color)411     public void setWeekNumberColor(int color) {
412         if (mWeekNumberColor != color) {
413             mWeekNumberColor = color;
414             if (mShowWeekNumber) {
415                 invalidateAllWeekViews();
416             }
417         }
418     }
419 
420     @Override
getWeekNumberColor()421     public int getWeekNumberColor() {
422         return mWeekNumberColor;
423     }
424 
425     @Override
setWeekSeparatorLineColor(int color)426     public void setWeekSeparatorLineColor(int color) {
427         if (mWeekSeparatorLineColor != color) {
428             mWeekSeparatorLineColor = color;
429             invalidateAllWeekViews();
430         }
431     }
432 
433     @Override
getWeekSeparatorLineColor()434     public int getWeekSeparatorLineColor() {
435         return mWeekSeparatorLineColor;
436     }
437 
438     @Override
setSelectedDateVerticalBar(int resourceId)439     public void setSelectedDateVerticalBar(int resourceId) {
440         Drawable drawable = mDelegator.getContext().getDrawable(resourceId);
441         setSelectedDateVerticalBar(drawable);
442     }
443 
444     @Override
setSelectedDateVerticalBar(Drawable drawable)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
getSelectedDateVerticalBar()459     public Drawable getSelectedDateVerticalBar() {
460         return mSelectedDateVerticalBar;
461     }
462 
463     @Override
setWeekDayTextAppearance(int resourceId)464     public void setWeekDayTextAppearance(int resourceId) {
465         if (mWeekDayTextAppearanceResId != resourceId) {
466             mWeekDayTextAppearanceResId = resourceId;
467             setUpHeader();
468         }
469     }
470 
471     @Override
getWeekDayTextAppearance()472     public int getWeekDayTextAppearance() {
473         return mWeekDayTextAppearanceResId;
474     }
475 
476     @Override
setDateTextAppearance(int resourceId)477     public void setDateTextAppearance(int resourceId) {
478         if (mDateTextAppearanceResId != resourceId) {
479             mDateTextAppearanceResId = resourceId;
480             updateDateTextSize();
481             invalidateAllWeekViews();
482         }
483     }
484 
485     @Override
getDateTextAppearance()486     public int getDateTextAppearance() {
487         return mDateTextAppearanceResId;
488     }
489 
490     @Override
setMinDate(long minDate)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
getMinDate()519     public long getMinDate() {
520         return mMinDate.getTimeInMillis();
521     }
522 
523     @Override
setMaxDate(long maxDate)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
getMaxDate()545     public long getMaxDate() {
546         return mMaxDate.getTimeInMillis();
547     }
548 
549     @Override
setShowWeekNumber(boolean showWeekNumber)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
getShowWeekNumber()560     public boolean getShowWeekNumber() {
561         return mShowWeekNumber;
562     }
563 
564     @Override
setFirstDayOfWeek(int firstDayOfWeek)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
getFirstDayOfWeek()576     public int getFirstDayOfWeek() {
577         return mFirstDayOfWeek;
578     }
579 
580     @Override
setDate(long date)581     public void setDate(long date) {
582         setDate(date, false, false);
583     }
584 
585     @Override
setDate(long date, boolean animate, boolean center)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
getDate()595     public long getDate() {
596         return mAdapter.mSelectedDate.getTimeInMillis();
597     }
598 
599     @Override
setOnDateChangeListener(CalendarView.OnDateChangeListener listener)600     public void setOnDateChangeListener(CalendarView.OnDateChangeListener listener) {
601         mOnDateChangeListener = listener;
602     }
603 
604     @Override
getBoundsForDate(long date, Rect outBounds)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
onConfigurationChanged(Configuration newConfig)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
setCurrentLocale(Locale locale)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     }
updateDateTextSize()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      */
invalidateAllWeekViews()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      */
getCalendarForLocale(Calendar oldCalendar, Locale locale)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      */
isSameDate(Calendar firstDate, Calendar secondDate)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      */
setUpAdapter()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      */
setUpHeader()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      */
setUpListView()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      */
goTo(Calendar date, boolean animate, boolean setSelected, boolean forceScroll)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      */
onScrollStateChanged(AbsListView view, int scrollState)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      */
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)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      */
setMonthDisplayed(Calendar calendar)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      */
getWeeksSinceMinDate(Calendar date)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          */
doScrollStateChange(AbsListView view, int scrollState)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 
run()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 
WeeksAdapter(Context context)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          */
init()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          */
setSelectedDay(Calendar selectedDay)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          */
getSelectedDay()1065         public Calendar getSelectedDay() {
1066             return mSelectedDate;
1067         }
1068 
1069         @Override
getCount()1070         public int getCount() {
1071             return mTotalWeekCount;
1072         }
1073 
1074         @Override
getItem(int position)1075         public Object getItem(int position) {
1076             return null;
1077         }
1078 
1079         @Override
getItemId(int position)1080         public long getItemId(int position) {
1081             return position;
1082         }
1083 
1084         @Override
getView(int position, View convertView, ViewGroup parent)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          */
setFocusMonth(int month)1111         public void setFocusMonth(int month) {
1112             if (mFocusedMonth == month) {
1113                 return;
1114             }
1115             mFocusedMonth = month;
1116             notifyDataSetChanged();
1117         }
1118 
1119         @Override
onTouch(View v, MotionEvent event)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          */
onDateTapped(Calendar day)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
onSingleTapUp(MotionEvent e)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 
WeekView(Context context)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          */
init(int weekNumber, int selectedWeekDay, int focusedMonth)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          */
initializePaints()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          */
getMonthOfFirstWeekDay()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          */
getMonthOfLastWeekDay()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          */
getFirstDay()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          */
getDayFromLocation(float x, Calendar outCalendar)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 
getBoundsForDate(Calendar date, Rect outBounds)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
onDraw(Canvas canvas)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          */
drawBackground(Canvas canvas)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          */
drawWeekNumbersAndDates(Canvas canvas)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          */
drawWeekSeparators(Canvas canvas)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          */
drawSelectedDateVerticalBars(Canvas canvas)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
onSizeChanged(int w, int h, int oldw, int oldh)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          */
updateSelectionPositions()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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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