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