Home | History | Annotate | Download | only in widget
      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 = -1;
    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          */
    325         public void onSelectedDayChange(CalendarView view, int year, int month, int dayOfMonth);
    326     }
    327 
    328     public CalendarView(Context context) {
    329         this(context, null);
    330     }
    331 
    332     public CalendarView(Context context, AttributeSet attrs) {
    333         this(context, attrs, 0);
    334     }
    335 
    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) context
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    681     public int getDateTextAppearance() {
    682         return mDateTextAppearanceResId;
    683     }
    684 
    685     @Override
    686     public void setEnabled(boolean enabled) {
    687         mListView.setEnabled(enabled);
    688     }
    689 
    690     @Override
    691     public boolean isEnabled() {
    692         return mListView.isEnabled();
    693     }
    694 
    695     @Override
    696     protected void onConfigurationChanged(Configuration newConfig) {
    697         super.onConfigurationChanged(newConfig);
    698         setCurrentLocale(newConfig.locale);
    699     }
    700 
    701     @Override
    702     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    703         super.onInitializeAccessibilityEvent(event);
    704         event.setClassName(CalendarView.class.getName());
    705     }
    706 
    707     @Override
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    871     public void setFirstDayOfWeek(int firstDayOfWeek) {
    872         if (mFirstDayOfWeek == firstDayOfWeek) {
    873             return;
    874         }
    875         mFirstDayOfWeek = firstDayOfWeek;
    876         mAdapter.init();
    877         setUpHeader();
    878     }
    879 
    880     /**
    881      * Sets the listener to be notified upon selected date change.
    882      *
    883      * @param listener The listener to be notified.
    884      */
    885     public void setOnDateChangeListener(OnDateChangeListener listener) {
    886         mOnDateChangeListener = listener;
    887     }
    888 
    889     /**
    890      * Gets the selected date in milliseconds since January 1, 1970 00:00:00 in
    891      * {@link TimeZone#getDefault()} time zone.
    892      *
    893      * @return The selected date.
    894      */
    895     public long getDate() {
    896         return mAdapter.mSelectedDate.getTimeInMillis();
    897     }
    898 
    899     /**
    900      * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
    901      * {@link TimeZone#getDefault()} time zone.
    902      *
    903      * @param date The selected date.
    904      *
    905      * @throws IllegalArgumentException of the provided date is before the
    906      *        minimal or after the maximal date.
    907      *
    908      * @see #setDate(long, boolean, boolean)
    909      * @see #setMinDate(long)
    910      * @see #setMaxDate(long)
    911      */
    912     public void setDate(long date) {
    913         setDate(date, false, false);
    914     }
    915 
    916     /**
    917      * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
    918      * {@link TimeZone#getDefault()} time zone.
    919      *
    920      * @param date The date.
    921      * @param animate Whether to animate the scroll to the current date.
    922      * @param center Whether to center the current date even if it is already visible.
    923      *
    924      * @throws IllegalArgumentException of the provided date is before the
    925      *        minimal or after the maximal date.
    926      *
    927      * @see #setMinDate(long)
    928      * @see #setMaxDate(long)
    929      */
    930     public void setDate(long date, boolean animate, boolean center) {
    931         mTempDate.setTimeInMillis(date);
    932         if (isSameDate(mTempDate, mAdapter.mSelectedDate)) {
    933             return;
    934         }
    935         goTo(mTempDate, animate, true, center);
    936     }
    937 
    938     private void updateDateTextSize() {
    939         TypedArray dateTextAppearance = mContext.obtainStyledAttributes(
    940                 mDateTextAppearanceResId, R.styleable.TextAppearance);
    941         mDateTextSize = dateTextAppearance.getDimensionPixelSize(
    942                 R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE);
    943         dateTextAppearance.recycle();
    944     }
    945 
    946     /**
    947      * Invalidates all week views.
    948      */
    949     private void invalidateAllWeekViews() {
    950         final int childCount = mListView.getChildCount();
    951         for (int i = 0; i < childCount; i++) {
    952             View view = mListView.getChildAt(i);
    953             view.invalidate();
    954         }
    955     }
    956 
    957     /**
    958      * Sets the current locale.
    959      *
    960      * @param locale The current locale.
    961      */
    962     private void setCurrentLocale(Locale locale) {
    963         if (locale.equals(mCurrentLocale)) {
    964             return;
    965         }
    966 
    967         mCurrentLocale = locale;
    968 
    969         mTempDate = getCalendarForLocale(mTempDate, locale);
    970         mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale);
    971         mMinDate = getCalendarForLocale(mMinDate, locale);
    972         mMaxDate = getCalendarForLocale(mMaxDate, locale);
    973     }
    974 
    975     /**
    976      * Gets a calendar for locale bootstrapped with the value of a given calendar.
    977      *
    978      * @param oldCalendar The old calendar.
    979      * @param locale The locale.
    980      */
    981     private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
    982         if (oldCalendar == null) {
    983             return Calendar.getInstance(locale);
    984         } else {
    985             final long currentTimeMillis = oldCalendar.getTimeInMillis();
    986             Calendar newCalendar = Calendar.getInstance(locale);
    987             newCalendar.setTimeInMillis(currentTimeMillis);
    988             return newCalendar;
    989         }
    990     }
    991 
    992     /**
    993      * @return True if the <code>firstDate</code> is the same as the <code>
    994      * secondDate</code>.
    995      */
    996     private boolean isSameDate(Calendar firstDate, Calendar secondDate) {
    997         return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR)
    998                 && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR));
    999     }
   1000 
   1001     /**
   1002      * Creates a new adapter if necessary and sets up its parameters.
   1003      */
   1004     private void setUpAdapter() {
   1005         if (mAdapter == null) {
   1006             mAdapter = new WeeksAdapter();
   1007             mAdapter.registerDataSetObserver(new DataSetObserver() {
   1008                 @Override
   1009                 public void onChanged() {
   1010                     if (mOnDateChangeListener != null) {
   1011                         Calendar selectedDay = mAdapter.getSelectedDay();
   1012                         mOnDateChangeListener.onSelectedDayChange(CalendarView.this,
   1013                                 selectedDay.get(Calendar.YEAR),
   1014                                 selectedDay.get(Calendar.MONTH),
   1015                                 selectedDay.get(Calendar.DAY_OF_MONTH));
   1016                     }
   1017                 }
   1018             });
   1019             mListView.setAdapter(mAdapter);
   1020         }
   1021 
   1022         // refresh the view with the new parameters
   1023         mAdapter.notifyDataSetChanged();
   1024     }
   1025 
   1026     /**
   1027      * Sets up the strings to be used by the header.
   1028      */
   1029     private void setUpHeader() {
   1030         final String[] tinyWeekdayNames = LocaleData.get(Locale.getDefault()).tinyWeekdayNames;
   1031         mDayLabels = new String[mDaysPerWeek];
   1032         for (int i = 0; i < mDaysPerWeek; i++) {
   1033             final int j = i + mFirstDayOfWeek;
   1034             final int calendarDay = (j > Calendar.SATURDAY) ? j - Calendar.SATURDAY : j;
   1035             mDayLabels[i] = tinyWeekdayNames[calendarDay];
   1036         }
   1037         // Deal with week number
   1038         TextView label = (TextView) mDayNamesHeader.getChildAt(0);
   1039         if (mShowWeekNumber) {
   1040             label.setVisibility(View.VISIBLE);
   1041         } else {
   1042             label.setVisibility(View.GONE);
   1043         }
   1044         // Deal with day labels
   1045         final int count = mDayNamesHeader.getChildCount();
   1046         for (int i = 0; i < count - 1; i++) {
   1047             label = (TextView) mDayNamesHeader.getChildAt(i + 1);
   1048             if (mWeekDayTextAppearanceResId > -1) {
   1049                 label.setTextAppearance(mContext, mWeekDayTextAppearanceResId);
   1050             }
   1051             if (i < mDaysPerWeek) {
   1052                 label.setText(mDayLabels[i]);
   1053                 label.setVisibility(View.VISIBLE);
   1054             } else {
   1055                 label.setVisibility(View.GONE);
   1056             }
   1057         }
   1058         mDayNamesHeader.invalidate();
   1059     }
   1060 
   1061     /**
   1062      * Sets all the required fields for the list view.
   1063      */
   1064     private void setUpListView() {
   1065         // Configure the listview
   1066         mListView.setDivider(null);
   1067         mListView.setItemsCanFocus(true);
   1068         mListView.setVerticalScrollBarEnabled(false);
   1069         mListView.setOnScrollListener(new OnScrollListener() {
   1070             public void onScrollStateChanged(AbsListView view, int scrollState) {
   1071                 CalendarView.this.onScrollStateChanged(view, scrollState);
   1072             }
   1073 
   1074             public void onScroll(
   1075                     AbsListView view, int firstVisibleItem, int visibleItemCount,
   1076                     int totalItemCount) {
   1077                 CalendarView.this.onScroll(view, firstVisibleItem, visibleItemCount,
   1078                         totalItemCount);
   1079             }
   1080         });
   1081         // Make the scrolling behavior nicer
   1082         mListView.setFriction(mFriction);
   1083         mListView.setVelocityScale(mVelocityScale);
   1084     }
   1085 
   1086     /**
   1087      * This moves to the specified time in the view. If the time is not already
   1088      * in range it will move the list so that the first of the month containing
   1089      * the time is at the top of the view. If the new time is already in view
   1090      * the list will not be scrolled unless forceScroll is true. This time may
   1091      * optionally be highlighted as selected as well.
   1092      *
   1093      * @param date The time to move to.
   1094      * @param animate Whether to scroll to the given time or just redraw at the
   1095      *            new location.
   1096      * @param setSelected Whether to set the given time as selected.
   1097      * @param forceScroll Whether to recenter even if the time is already
   1098      *            visible.
   1099      *
   1100      * @throws IllegalArgumentException of the provided date is before the
   1101      *        range start of after the range end.
   1102      */
   1103     private void goTo(Calendar date, boolean animate, boolean setSelected, boolean forceScroll) {
   1104         if (date.before(mMinDate) || date.after(mMaxDate)) {
   1105             throw new IllegalArgumentException("Time not between " + mMinDate.getTime()
   1106                     + " and " + mMaxDate.getTime());
   1107         }
   1108         // Find the first and last entirely visible weeks
   1109         int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
   1110         View firstChild = mListView.getChildAt(0);
   1111         if (firstChild != null && firstChild.getTop() < 0) {
   1112             firstFullyVisiblePosition++;
   1113         }
   1114         int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1;
   1115         if (firstChild != null && firstChild.getTop() > mBottomBuffer) {
   1116             lastFullyVisiblePosition--;
   1117         }
   1118         if (setSelected) {
   1119             mAdapter.setSelectedDay(date);
   1120         }
   1121         // Get the week we're going to
   1122         int position = getWeeksSinceMinDate(date);
   1123 
   1124         // Check if the selected day is now outside of our visible range
   1125         // and if so scroll to the month that contains it
   1126         if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition
   1127                 || forceScroll) {
   1128             mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis());
   1129             mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1);
   1130 
   1131             setMonthDisplayed(mFirstDayOfMonth);
   1132 
   1133             // the earliest time we can scroll to is the min date
   1134             if (mFirstDayOfMonth.before(mMinDate)) {
   1135                 position = 0;
   1136             } else {
   1137                 position = getWeeksSinceMinDate(mFirstDayOfMonth);
   1138             }
   1139 
   1140             mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
   1141             if (animate) {
   1142                 mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset,
   1143                         GOTO_SCROLL_DURATION);
   1144             } else {
   1145                 mListView.setSelectionFromTop(position, mListScrollTopOffset);
   1146                 // Perform any after scroll operations that are needed
   1147                 onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE);
   1148             }
   1149         } else if (setSelected) {
   1150             // Otherwise just set the selection
   1151             setMonthDisplayed(date);
   1152         }
   1153     }
   1154 
   1155     /**
   1156      * Parses the given <code>date</code> and in case of success sets
   1157      * the result to the <code>outDate</code>.
   1158      *
   1159      * @return True if the date was parsed.
   1160      */
   1161     private boolean parseDate(String date, Calendar outDate) {
   1162         try {
   1163             outDate.setTime(mDateFormat.parse(date));
   1164             return true;
   1165         } catch (ParseException e) {
   1166             Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
   1167             return false;
   1168         }
   1169     }
   1170 
   1171     /**
   1172      * Called when a <code>view</code> transitions to a new <code>scrollState
   1173      * </code>.
   1174      */
   1175     private void onScrollStateChanged(AbsListView view, int scrollState) {
   1176         mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
   1177     }
   1178 
   1179     /**
   1180      * Updates the title and selected month if the <code>view</code> has moved to a new
   1181      * month.
   1182      */
   1183     private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
   1184             int totalItemCount) {
   1185         WeekView child = (WeekView) view.getChildAt(0);
   1186         if (child == null) {
   1187             return;
   1188         }
   1189 
   1190         // Figure out where we are
   1191         long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
   1192 
   1193         // If we have moved since our last call update the direction
   1194         if (currScroll < mPreviousScrollPosition) {
   1195             mIsScrollingUp = true;
   1196         } else if (currScroll > mPreviousScrollPosition) {
   1197             mIsScrollingUp = false;
   1198         } else {
   1199             return;
   1200         }
   1201 
   1202         // Use some hysteresis for checking which month to highlight. This
   1203         // causes the month to transition when two full weeks of a month are
   1204         // visible when scrolling up, and when the first day in a month reaches
   1205         // the top of the screen when scrolling down.
   1206         int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0;
   1207         if (mIsScrollingUp) {
   1208             child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset);
   1209         } else if (offset != 0) {
   1210             child = (WeekView) view.getChildAt(offset);
   1211         }
   1212 
   1213         if (child != null) {
   1214             // Find out which month we're moving into
   1215             int month;
   1216             if (mIsScrollingUp) {
   1217                 month = child.getMonthOfFirstWeekDay();
   1218             } else {
   1219                 month = child.getMonthOfLastWeekDay();
   1220             }
   1221 
   1222             // And how it relates to our current highlighted month
   1223             int monthDiff;
   1224             if (mCurrentMonthDisplayed == 11 && month == 0) {
   1225                 monthDiff = 1;
   1226             } else if (mCurrentMonthDisplayed == 0 && month == 11) {
   1227                 monthDiff = -1;
   1228             } else {
   1229                 monthDiff = month - mCurrentMonthDisplayed;
   1230             }
   1231 
   1232             // Only switch months if we're scrolling away from the currently
   1233             // selected month
   1234             if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) {
   1235                 Calendar firstDay = child.getFirstDay();
   1236                 if (mIsScrollingUp) {
   1237                     firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK);
   1238                 } else {
   1239                     firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK);
   1240                 }
   1241                 setMonthDisplayed(firstDay);
   1242             }
   1243         }
   1244 
   1245         mPreviousScrollPosition = currScroll;
   1246         mPreviousScrollState = mCurrentScrollState;
   1247     }
   1248 
   1249     /**
   1250      * Sets the month displayed at the top of this view based on time. Override
   1251      * to add custom events when the title is changed.
   1252      *
   1253      * @param calendar A day in the new focus month.
   1254      */
   1255     private void setMonthDisplayed(Calendar calendar) {
   1256         mCurrentMonthDisplayed = calendar.get(Calendar.MONTH);
   1257         mAdapter.setFocusMonth(mCurrentMonthDisplayed);
   1258         final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
   1259                 | DateUtils.FORMAT_SHOW_YEAR;
   1260         final long millis = calendar.getTimeInMillis();
   1261         String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags);
   1262         mMonthName.setText(newMonthName);
   1263         mMonthName.invalidate();
   1264     }
   1265 
   1266     /**
   1267      * @return Returns the number of weeks between the current <code>date</code>
   1268      *         and the <code>mMinDate</code>.
   1269      */
   1270     private int getWeeksSinceMinDate(Calendar date) {
   1271         if (date.before(mMinDate)) {
   1272             throw new IllegalArgumentException("fromDate: " + mMinDate.getTime()
   1273                     + " does not precede toDate: " + date.getTime());
   1274         }
   1275         long endTimeMillis = date.getTimeInMillis()
   1276                 + date.getTimeZone().getOffset(date.getTimeInMillis());
   1277         long startTimeMillis = mMinDate.getTimeInMillis()
   1278                 + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis());
   1279         long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek)
   1280                 * MILLIS_IN_DAY;
   1281         return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK);
   1282     }
   1283 
   1284     /**
   1285      * Command responsible for acting upon scroll state changes.
   1286      */
   1287     private class ScrollStateRunnable implements Runnable {
   1288         private AbsListView mView;
   1289 
   1290         private int mNewState;
   1291 
   1292         /**
   1293          * Sets up the runnable with a short delay in case the scroll state
   1294          * immediately changes again.
   1295          *
   1296          * @param view The list view that changed state
   1297          * @param scrollState The new state it changed to
   1298          */
   1299         public void doScrollStateChange(AbsListView view, int scrollState) {
   1300             mView = view;
   1301             mNewState = scrollState;
   1302             removeCallbacks(this);
   1303             postDelayed(this, SCROLL_CHANGE_DELAY);
   1304         }
   1305 
   1306         public void run() {
   1307             mCurrentScrollState = mNewState;
   1308             // Fix the position after a scroll or a fling ends
   1309             if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
   1310                     && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
   1311                 View child = mView.getChildAt(0);
   1312                 if (child == null) {
   1313                     // The view is no longer visible, just return
   1314                     return;
   1315                 }
   1316                 int dist = child.getBottom() - mListScrollTopOffset;
   1317                 if (dist > mListScrollTopOffset) {
   1318                     if (mIsScrollingUp) {
   1319                         mView.smoothScrollBy(dist - child.getHeight(), ADJUSTMENT_SCROLL_DURATION);
   1320                     } else {
   1321                         mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION);
   1322                     }
   1323                 }
   1324             }
   1325             mPreviousScrollState = mNewState;
   1326         }
   1327     }
   1328 
   1329     /**
   1330      * <p>
   1331      * This is a specialized adapter for creating a list of weeks with
   1332      * selectable days. It can be configured to display the week number, start
   1333      * the week on a given day, show a reduced number of days, or display an
   1334      * arbitrary number of weeks at a time.
   1335      * </p>
   1336      */
   1337     private class WeeksAdapter extends BaseAdapter implements OnTouchListener {
   1338         private final Calendar mSelectedDate = Calendar.getInstance();
   1339         private final GestureDetector mGestureDetector;
   1340 
   1341         private int mSelectedWeek;
   1342 
   1343         private int mFocusedMonth;
   1344 
   1345         private int mTotalWeekCount;
   1346 
   1347         public WeeksAdapter() {
   1348             mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener());
   1349             init();
   1350         }
   1351 
   1352         /**
   1353          * Set up the gesture detector and selected time
   1354          */
   1355         private void init() {
   1356             mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
   1357             mTotalWeekCount = getWeeksSinceMinDate(mMaxDate);
   1358             if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek
   1359                 || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) {
   1360                 mTotalWeekCount++;
   1361             }
   1362             notifyDataSetChanged();
   1363         }
   1364 
   1365         /**
   1366          * Updates the selected day and related parameters.
   1367          *
   1368          * @param selectedDay The time to highlight
   1369          */
   1370         public void setSelectedDay(Calendar selectedDay) {
   1371             if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR)
   1372                     && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) {
   1373                 return;
   1374             }
   1375             mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis());
   1376             mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
   1377             mFocusedMonth = mSelectedDate.get(Calendar.MONTH);
   1378             notifyDataSetChanged();
   1379         }
   1380 
   1381         /**
   1382          * @return The selected day of month.
   1383          */
   1384         public Calendar getSelectedDay() {
   1385             return mSelectedDate;
   1386         }
   1387 
   1388         @Override
   1389         public int getCount() {
   1390             return mTotalWeekCount;
   1391         }
   1392 
   1393         @Override
   1394         public Object getItem(int position) {
   1395             return null;
   1396         }
   1397 
   1398         @Override
   1399         public long getItemId(int position) {
   1400             return position;
   1401         }
   1402 
   1403         @Override
   1404         public View getView(int position, View convertView, ViewGroup parent) {
   1405             WeekView weekView = null;
   1406             if (convertView != null) {
   1407                 weekView = (WeekView) convertView;
   1408             } else {
   1409                 weekView = new WeekView(mContext);
   1410                 android.widget.AbsListView.LayoutParams params =
   1411                     new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT,
   1412                             LayoutParams.WRAP_CONTENT);
   1413                 weekView.setLayoutParams(params);
   1414                 weekView.setClickable(true);
   1415                 weekView.setOnTouchListener(this);
   1416             }
   1417 
   1418             int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get(
   1419                     Calendar.DAY_OF_WEEK) : -1;
   1420             weekView.init(position, selectedWeekDay, mFocusedMonth);
   1421 
   1422             return weekView;
   1423         }
   1424 
   1425         /**
   1426          * Changes which month is in focus and updates the view.
   1427          *
   1428          * @param month The month to show as in focus [0-11]
   1429          */
   1430         public void setFocusMonth(int month) {
   1431             if (mFocusedMonth == month) {
   1432                 return;
   1433             }
   1434             mFocusedMonth = month;
   1435             notifyDataSetChanged();
   1436         }
   1437 
   1438         @Override
   1439         public boolean onTouch(View v, MotionEvent event) {
   1440             if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) {
   1441                 WeekView weekView = (WeekView) v;
   1442                 // if we cannot find a day for the given location we are done
   1443                 if (!weekView.getDayFromLocation(event.getX(), mTempDate)) {
   1444                     return true;
   1445                 }
   1446                 // it is possible that the touched day is outside the valid range
   1447                 // we draw whole weeks but range end can fall not on the week end
   1448                 if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
   1449                     return true;
   1450                 }
   1451                 onDateTapped(mTempDate);
   1452                 return true;
   1453             }
   1454             return false;
   1455         }
   1456 
   1457         /**
   1458          * Maintains the same hour/min/sec but moves the day to the tapped day.
   1459          *
   1460          * @param day The day that was tapped
   1461          */
   1462         private void onDateTapped(Calendar day) {
   1463             setSelectedDay(day);
   1464             setMonthDisplayed(day);
   1465         }
   1466 
   1467         /**
   1468          * This is here so we can identify single tap events and set the
   1469          * selected day correctly
   1470          */
   1471         class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
   1472             @Override
   1473             public boolean onSingleTapUp(MotionEvent e) {
   1474                 return true;
   1475             }
   1476         }
   1477     }
   1478 
   1479     /**
   1480      * <p>
   1481      * This is a dynamic view for drawing a single week. It can be configured to
   1482      * display the week number, start the week on a given day, or show a reduced
   1483      * number of days. It is intended for use as a single view within a
   1484      * ListView. See {@link WeeksAdapter} for usage.
   1485      * </p>
   1486      */
   1487     private class WeekView extends View {
   1488 
   1489         private final Rect mTempRect = new Rect();
   1490 
   1491         private final Paint mDrawPaint = new Paint();
   1492 
   1493         private final Paint mMonthNumDrawPaint = new Paint();
   1494 
   1495         // Cache the number strings so we don't have to recompute them each time
   1496         private String[] mDayNumbers;
   1497 
   1498         // Quick lookup for checking which days are in the focus month
   1499         private boolean[] mFocusDay;
   1500 
   1501         // Whether this view has a focused day.
   1502         private boolean mHasFocusedDay;
   1503 
   1504         // Whether this view has only focused days.
   1505         private boolean mHasUnfocusedDay;
   1506 
   1507         // The first day displayed by this item
   1508         private Calendar mFirstDay;
   1509 
   1510         // The month of the first day in this week
   1511         private int mMonthOfFirstWeekDay = -1;
   1512 
   1513         // The month of the last day in this week
   1514         private int mLastWeekDayMonth = -1;
   1515 
   1516         // The position of this week, equivalent to weeks since the week of Jan
   1517         // 1st, 1900
   1518         private int mWeek = -1;
   1519 
   1520         // Quick reference to the width of this view, matches parent
   1521         private int mWidth;
   1522 
   1523         // The height this view should draw at in pixels, set by height param
   1524         private int mHeight;
   1525 
   1526         // If this view contains the selected day
   1527         private boolean mHasSelectedDay = false;
   1528 
   1529         // Which day is selected [0-6] or -1 if no day is selected
   1530         private int mSelectedDay = -1;
   1531 
   1532         // The number of days + a spot for week number if it is displayed
   1533         private int mNumCells;
   1534 
   1535         // The left edge of the selected day
   1536         private int mSelectedLeft = -1;
   1537 
   1538         // The right edge of the selected day
   1539         private int mSelectedRight = -1;
   1540 
   1541         public WeekView(Context context) {
   1542             super(context);
   1543 
   1544             // Sets up any standard paints that will be used
   1545             initilaizePaints();
   1546         }
   1547 
   1548         /**
   1549          * Initializes this week view.
   1550          *
   1551          * @param weekNumber The number of the week this view represents. The
   1552          *            week number is a zero based index of the weeks since
   1553          *            {@link CalendarView#getMinDate()}.
   1554          * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no
   1555          *            selected day.
   1556          * @param focusedMonth The month that is currently in focus i.e.
   1557          *            highlighted.
   1558          */
   1559         public void init(int weekNumber, int selectedWeekDay, int focusedMonth) {
   1560             mSelectedDay = selectedWeekDay;
   1561             mHasSelectedDay = mSelectedDay != -1;
   1562             mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek;
   1563             mWeek = weekNumber;
   1564             mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());
   1565 
   1566             mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek);
   1567             mTempDate.setFirstDayOfWeek(mFirstDayOfWeek);
   1568 
   1569             // Allocate space for caching the day numbers and focus values
   1570             mDayNumbers = new String[mNumCells];
   1571             mFocusDay = new boolean[mNumCells];
   1572 
   1573             // If we're showing the week number calculate it based on Monday
   1574             int i = 0;
   1575             if (mShowWeekNumber) {
   1576                 mDayNumbers[0] = String.format(Locale.getDefault(), "%d",
   1577                         mTempDate.get(Calendar.WEEK_OF_YEAR));
   1578                 i++;
   1579             }
   1580 
   1581             // Now adjust our starting day based on the start day of the week
   1582             int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK);
   1583             mTempDate.add(Calendar.DAY_OF_MONTH, diff);
   1584 
   1585             mFirstDay = (Calendar) mTempDate.clone();
   1586             mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH);
   1587 
   1588             mHasUnfocusedDay = true;
   1589             for (; i < mNumCells; i++) {
   1590                 final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth);
   1591                 mFocusDay[i] = isFocusedDay;
   1592                 mHasFocusedDay |= isFocusedDay;
   1593                 mHasUnfocusedDay &= !isFocusedDay;
   1594                 // do not draw dates outside the valid range to avoid user confusion
   1595                 if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
   1596                     mDayNumbers[i] = "";
   1597                 } else {
   1598                     mDayNumbers[i] = String.format(Locale.getDefault(), "%d",
   1599                             mTempDate.get(Calendar.DAY_OF_MONTH));
   1600                 }
   1601                 mTempDate.add(Calendar.DAY_OF_MONTH, 1);
   1602             }
   1603             // We do one extra add at the end of the loop, if that pushed us to
   1604             // new month undo it
   1605             if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) {
   1606                 mTempDate.add(Calendar.DAY_OF_MONTH, -1);
   1607             }
   1608             mLastWeekDayMonth = mTempDate.get(Calendar.MONTH);
   1609 
   1610             updateSelectionPositions();
   1611         }
   1612 
   1613         /**
   1614          * Initialize the paint instances.
   1615          */
   1616         private void initilaizePaints() {
   1617             mDrawPaint.setFakeBoldText(false);
   1618             mDrawPaint.setAntiAlias(true);
   1619             mDrawPaint.setStyle(Style.FILL);
   1620 
   1621             mMonthNumDrawPaint.setFakeBoldText(true);
   1622             mMonthNumDrawPaint.setAntiAlias(true);
   1623             mMonthNumDrawPaint.setStyle(Style.FILL);
   1624             mMonthNumDrawPaint.setTextAlign(Align.CENTER);
   1625             mMonthNumDrawPaint.setTextSize(mDateTextSize);
   1626         }
   1627 
   1628         /**
   1629          * Returns the month of the first day in this week.
   1630          *
   1631          * @return The month the first day of this view is in.
   1632          */
   1633         public int getMonthOfFirstWeekDay() {
   1634             return mMonthOfFirstWeekDay;
   1635         }
   1636 
   1637         /**
   1638          * Returns the month of the last day in this week
   1639          *
   1640          * @return The month the last day of this view is in
   1641          */
   1642         public int getMonthOfLastWeekDay() {
   1643             return mLastWeekDayMonth;
   1644         }
   1645 
   1646         /**
   1647          * Returns the first day in this view.
   1648          *
   1649          * @return The first day in the view.
   1650          */
   1651         public Calendar getFirstDay() {
   1652             return mFirstDay;
   1653         }
   1654 
   1655         /**
   1656          * Calculates the day that the given x position is in, accounting for
   1657          * week number.
   1658          *
   1659          * @param x The x position of the touch event.
   1660          * @return True if a day was found for the given location.
   1661          */
   1662         public boolean getDayFromLocation(float x, Calendar outCalendar) {
   1663             final boolean isLayoutRtl = isLayoutRtl();
   1664 
   1665             int start;
   1666             int end;
   1667 
   1668             if (isLayoutRtl) {
   1669                 start = 0;
   1670                 end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
   1671             } else {
   1672                 start = mShowWeekNumber ? mWidth / mNumCells : 0;
   1673                 end = mWidth;
   1674             }
   1675 
   1676             if (x < start || x > end) {
   1677                 outCalendar.clear();
   1678                 return false;
   1679             }
   1680 
   1681             // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels
   1682             int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start));
   1683 
   1684             if (isLayoutRtl) {
   1685                 dayPosition = mDaysPerWeek - 1 - dayPosition;
   1686             }
   1687 
   1688             outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis());
   1689             outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition);
   1690 
   1691             return true;
   1692         }
   1693 
   1694         @Override
   1695         protected void onDraw(Canvas canvas) {
   1696             drawBackground(canvas);
   1697             drawWeekNumbersAndDates(canvas);
   1698             drawWeekSeparators(canvas);
   1699             drawSelectedDateVerticalBars(canvas);
   1700         }
   1701 
   1702         /**
   1703          * This draws the selection highlight if a day is selected in this week.
   1704          *
   1705          * @param canvas The canvas to draw on
   1706          */
   1707         private void drawBackground(Canvas canvas) {
   1708             if (!mHasSelectedDay) {
   1709                 return;
   1710             }
   1711             mDrawPaint.setColor(mSelectedWeekBackgroundColor);
   1712 
   1713             mTempRect.top = mWeekSeperatorLineWidth;
   1714             mTempRect.bottom = mHeight;
   1715 
   1716             final boolean isLayoutRtl = isLayoutRtl();
   1717 
   1718             if (isLayoutRtl) {
   1719                 mTempRect.left = 0;
   1720                 mTempRect.right = mSelectedLeft - 2;
   1721             } else {
   1722                 mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0;
   1723                 mTempRect.right = mSelectedLeft - 2;
   1724             }
   1725             canvas.drawRect(mTempRect, mDrawPaint);
   1726 
   1727             if (isLayoutRtl) {
   1728                 mTempRect.left = mSelectedRight + 3;
   1729                 mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
   1730             } else {
   1731                 mTempRect.left = mSelectedRight + 3;
   1732                 mTempRect.right = mWidth;
   1733             }
   1734             canvas.drawRect(mTempRect, mDrawPaint);
   1735         }
   1736 
   1737         /**
   1738          * Draws the week and month day numbers for this week.
   1739          *
   1740          * @param canvas The canvas to draw on
   1741          */
   1742         private void drawWeekNumbersAndDates(Canvas canvas) {
   1743             final float textHeight = mDrawPaint.getTextSize();
   1744             final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth;
   1745             final int nDays = mNumCells;
   1746             final int divisor = 2 * nDays;
   1747 
   1748             mDrawPaint.setTextAlign(Align.CENTER);
   1749             mDrawPaint.setTextSize(mDateTextSize);
   1750 
   1751             int i = 0;
   1752 
   1753             if (isLayoutRtl()) {
   1754                 for (; i < nDays - 1; i++) {
   1755                     mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
   1756                             : mUnfocusedMonthDateColor);
   1757                     int x = (2 * i + 1) * mWidth / divisor;
   1758                     canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint);
   1759                 }
   1760                 if (mShowWeekNumber) {
   1761                     mDrawPaint.setColor(mWeekNumberColor);
   1762                     int x = mWidth - mWidth / divisor;
   1763                     canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
   1764                 }
   1765             } else {
   1766                 if (mShowWeekNumber) {
   1767                     mDrawPaint.setColor(mWeekNumberColor);
   1768                     int x = mWidth / divisor;
   1769                     canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
   1770                     i++;
   1771                 }
   1772                 for (; i < nDays; i++) {
   1773                     mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
   1774                             : mUnfocusedMonthDateColor);
   1775                     int x = (2 * i + 1) * mWidth / divisor;
   1776                     canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint);
   1777                 }
   1778             }
   1779         }
   1780 
   1781         /**
   1782          * Draws a horizontal line for separating the weeks.
   1783          *
   1784          * @param canvas The canvas to draw on.
   1785          */
   1786         private void drawWeekSeparators(Canvas canvas) {
   1787             // If it is the topmost fully visible child do not draw separator line
   1788             int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
   1789             if (mListView.getChildAt(0).getTop() < 0) {
   1790                 firstFullyVisiblePosition++;
   1791             }
   1792             if (firstFullyVisiblePosition == mWeek) {
   1793                 return;
   1794             }
   1795             mDrawPaint.setColor(mWeekSeparatorLineColor);
   1796             mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth);
   1797             float startX;
   1798             float stopX;
   1799             if (isLayoutRtl()) {
   1800                 startX = 0;
   1801                 stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
   1802             } else {
   1803                 startX = mShowWeekNumber ? mWidth / mNumCells : 0;
   1804                 stopX = mWidth;
   1805             }
   1806             canvas.drawLine(startX, 0, stopX, 0, mDrawPaint);
   1807         }
   1808 
   1809         /**
   1810          * Draws the selected date bars if this week has a selected day.
   1811          *
   1812          * @param canvas The canvas to draw on
   1813          */
   1814         private void drawSelectedDateVerticalBars(Canvas canvas) {
   1815             if (!mHasSelectedDay) {
   1816                 return;
   1817             }
   1818             mSelectedDateVerticalBar.setBounds(mSelectedLeft - mSelectedDateVerticalBarWidth / 2,
   1819                     mWeekSeperatorLineWidth,
   1820                     mSelectedLeft + mSelectedDateVerticalBarWidth / 2, mHeight);
   1821             mSelectedDateVerticalBar.draw(canvas);
   1822             mSelectedDateVerticalBar.setBounds(mSelectedRight - mSelectedDateVerticalBarWidth / 2,
   1823                     mWeekSeperatorLineWidth,
   1824                     mSelectedRight + mSelectedDateVerticalBarWidth / 2, mHeight);
   1825             mSelectedDateVerticalBar.draw(canvas);
   1826         }
   1827 
   1828         @Override
   1829         protected void onSizeChanged(int w, int h, int oldw, int oldh) {
   1830             mWidth = w;
   1831             updateSelectionPositions();
   1832         }
   1833 
   1834         /**
   1835          * This calculates the positions for the selected day lines.
   1836          */
   1837         private void updateSelectionPositions() {
   1838             if (mHasSelectedDay) {
   1839                 final boolean isLayoutRtl = isLayoutRtl();
   1840                 int selectedPosition = mSelectedDay - mFirstDayOfWeek;
   1841                 if (selectedPosition < 0) {
   1842                     selectedPosition += 7;
   1843                 }
   1844                 if (mShowWeekNumber && !isLayoutRtl) {
   1845                     selectedPosition++;
   1846                 }
   1847                 if (isLayoutRtl) {
   1848                     mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells;
   1849 
   1850                 } else {
   1851                     mSelectedLeft = selectedPosition * mWidth / mNumCells;
   1852                 }
   1853                 mSelectedRight = mSelectedLeft + mWidth / mNumCells;
   1854             }
   1855         }
   1856 
   1857         @Override
   1858         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1859             mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView
   1860                     .getPaddingBottom()) / mShownWeekCount;
   1861             setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight);
   1862         }
   1863     }
   1864 }
   1865