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