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         // Find out which month we're moving into
   1214         int month;
   1215         if (mIsScrollingUp) {
   1216             month = child.getMonthOfFirstWeekDay();
   1217         } else {
   1218             month = child.getMonthOfLastWeekDay();
   1219         }
   1220 
   1221         // And how it relates to our current highlighted month
   1222         int monthDiff;
   1223         if (mCurrentMonthDisplayed == 11 && month == 0) {
   1224             monthDiff = 1;
   1225         } else if (mCurrentMonthDisplayed == 0 && month == 11) {
   1226             monthDiff = -1;
   1227         } else {
   1228             monthDiff = month - mCurrentMonthDisplayed;
   1229         }
   1230 
   1231         // Only switch months if we're scrolling away from the currently
   1232         // selected month
   1233         if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) {
   1234             Calendar firstDay = child.getFirstDay();
   1235             if (mIsScrollingUp) {
   1236                 firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK);
   1237             } else {
   1238                 firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK);
   1239             }
   1240             setMonthDisplayed(firstDay);
   1241         }
   1242         mPreviousScrollPosition = currScroll;
   1243         mPreviousScrollState = mCurrentScrollState;
   1244     }
   1245 
   1246     /**
   1247      * Sets the month displayed at the top of this view based on time. Override
   1248      * to add custom events when the title is changed.
   1249      *
   1250      * @param calendar A day in the new focus month.
   1251      */
   1252     private void setMonthDisplayed(Calendar calendar) {
   1253         mCurrentMonthDisplayed = calendar.get(Calendar.MONTH);
   1254         mAdapter.setFocusMonth(mCurrentMonthDisplayed);
   1255         final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
   1256                 | DateUtils.FORMAT_SHOW_YEAR;
   1257         final long millis = calendar.getTimeInMillis();
   1258         String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags);
   1259         mMonthName.setText(newMonthName);
   1260         mMonthName.invalidate();
   1261     }
   1262 
   1263     /**
   1264      * @return Returns the number of weeks between the current <code>date</code>
   1265      *         and the <code>mMinDate</code>.
   1266      */
   1267     private int getWeeksSinceMinDate(Calendar date) {
   1268         if (date.before(mMinDate)) {
   1269             throw new IllegalArgumentException("fromDate: " + mMinDate.getTime()
   1270                     + " does not precede toDate: " + date.getTime());
   1271         }
   1272         long endTimeMillis = date.getTimeInMillis()
   1273                 + date.getTimeZone().getOffset(date.getTimeInMillis());
   1274         long startTimeMillis = mMinDate.getTimeInMillis()
   1275                 + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis());
   1276         long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek)
   1277                 * MILLIS_IN_DAY;
   1278         return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK);
   1279     }
   1280 
   1281     /**
   1282      * Command responsible for acting upon scroll state changes.
   1283      */
   1284     private class ScrollStateRunnable implements Runnable {
   1285         private AbsListView mView;
   1286 
   1287         private int mNewState;
   1288 
   1289         /**
   1290          * Sets up the runnable with a short delay in case the scroll state
   1291          * immediately changes again.
   1292          *
   1293          * @param view The list view that changed state
   1294          * @param scrollState The new state it changed to
   1295          */
   1296         public void doScrollStateChange(AbsListView view, int scrollState) {
   1297             mView = view;
   1298             mNewState = scrollState;
   1299             removeCallbacks(this);
   1300             postDelayed(this, SCROLL_CHANGE_DELAY);
   1301         }
   1302 
   1303         public void run() {
   1304             mCurrentScrollState = mNewState;
   1305             // Fix the position after a scroll or a fling ends
   1306             if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
   1307                     && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
   1308                 View child = mView.getChildAt(0);
   1309                 if (child == null) {
   1310                     // The view is no longer visible, just return
   1311                     return;
   1312                 }
   1313                 int dist = child.getBottom() - mListScrollTopOffset;
   1314                 if (dist > mListScrollTopOffset) {
   1315                     if (mIsScrollingUp) {
   1316                         mView.smoothScrollBy(dist - child.getHeight(), ADJUSTMENT_SCROLL_DURATION);
   1317                     } else {
   1318                         mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION);
   1319                     }
   1320                 }
   1321             }
   1322             mPreviousScrollState = mNewState;
   1323         }
   1324     }
   1325 
   1326     /**
   1327      * <p>
   1328      * This is a specialized adapter for creating a list of weeks with
   1329      * selectable days. It can be configured to display the week number, start
   1330      * the week on a given day, show a reduced number of days, or display an
   1331      * arbitrary number of weeks at a time.
   1332      * </p>
   1333      */
   1334     private class WeeksAdapter extends BaseAdapter implements OnTouchListener {
   1335         private final Calendar mSelectedDate = Calendar.getInstance();
   1336         private final GestureDetector mGestureDetector;
   1337 
   1338         private int mSelectedWeek;
   1339 
   1340         private int mFocusedMonth;
   1341 
   1342         private int mTotalWeekCount;
   1343 
   1344         public WeeksAdapter() {
   1345             mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener());
   1346             init();
   1347         }
   1348 
   1349         /**
   1350          * Set up the gesture detector and selected time
   1351          */
   1352         private void init() {
   1353             mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
   1354             mTotalWeekCount = getWeeksSinceMinDate(mMaxDate);
   1355             if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek
   1356                 || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) {
   1357                 mTotalWeekCount++;
   1358             }
   1359             notifyDataSetChanged();
   1360         }
   1361 
   1362         /**
   1363          * Updates the selected day and related parameters.
   1364          *
   1365          * @param selectedDay The time to highlight
   1366          */
   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          */
   1381         public Calendar getSelectedDay() {
   1382             return mSelectedDate;
   1383         }
   1384 
   1385         @Override
   1386         public int getCount() {
   1387             return mTotalWeekCount;
   1388         }
   1389 
   1390         @Override
   1391         public Object getItem(int position) {
   1392             return null;
   1393         }
   1394 
   1395         @Override
   1396         public long getItemId(int position) {
   1397             return position;
   1398         }
   1399 
   1400         @Override
   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          */
   1427         public void setFocusMonth(int month) {
   1428             if (mFocusedMonth == month) {
   1429                 return;
   1430             }
   1431             mFocusedMonth = month;
   1432             notifyDataSetChanged();
   1433         }
   1434 
   1435         @Override
   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          */
   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
   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 
   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          */
   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          */
   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          */
   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          */
   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          */
   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          */
   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
   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          */
   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          */
   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          */
   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          */
   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
   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          */
   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
   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