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