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