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.widget.AbsListView.OnScrollListener;
     43 
     44 import com.android.internal.R;
     45 
     46 import java.text.ParseException;
     47 import java.text.SimpleDateFormat;
     48 import java.util.Calendar;
     49 import java.util.Locale;
     50 import java.util.TimeZone;
     51 
     52 import libcore.icu.LocaleData;
     53 
     54 /**
     55  * This class is a calendar widget for displaying and selecting dates. The range
     56  * of dates supported by this calendar is configurable. A user can select a date
     57  * by taping on it and can scroll and fling the calendar to a desired date.
     58  *
     59  * @attr ref android.R.styleable#CalendarView_showWeekNumber
     60  * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
     61  * @attr ref android.R.styleable#CalendarView_minDate
     62  * @attr ref android.R.styleable#CalendarView_maxDate
     63  * @attr ref android.R.styleable#CalendarView_shownWeekCount
     64  * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
     65  * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
     66  * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
     67  * @attr ref android.R.styleable#CalendarView_weekNumberColor
     68  * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
     69  * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
     70  * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
     71  * @attr ref android.R.styleable#CalendarView_dateTextAppearance
     72  */
     73 @Widget
     74 public class CalendarView extends FrameLayout {
     75 
     76     /**
     77      * Tag for logging.
     78      */
     79     private static final String LOG_TAG = CalendarView.class.getSimpleName();
     80 
     81     /**
     82      * Default value whether to show week number.
     83      */
     84     private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true;
     85 
     86     /**
     87      * The number of milliseconds in a day.e
     88      */
     89     private static final long MILLIS_IN_DAY = 86400000L;
     90 
     91     /**
     92      * The number of day in a week.
     93      */
     94     private static final int DAYS_PER_WEEK = 7;
     95 
     96     /**
     97      * The number of milliseconds in a week.
     98      */
     99     private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY;
    100 
    101     /**
    102      * Affects when the month selection will change while scrolling upe
    103      */
    104     private static final int SCROLL_HYST_WEEKS = 2;
    105 
    106     /**
    107      * How long the GoTo fling animation should last.
    108      */
    109     private static final int GOTO_SCROLL_DURATION = 1000;
    110 
    111     /**
    112      * The duration of the adjustment upon a user scroll in milliseconds.
    113      */
    114     private static final int ADJUSTMENT_SCROLL_DURATION = 500;
    115 
    116     /**
    117      * How long to wait after receiving an onScrollStateChanged notification
    118      * before acting on it.
    119      */
    120     private static final int SCROLL_CHANGE_DELAY = 40;
    121 
    122     /**
    123      * String for parsing dates.
    124      */
    125     private static final String DATE_FORMAT = "MM/dd/yyyy";
    126 
    127     /**
    128      * The default minimal date.
    129      */
    130     private static final String DEFAULT_MIN_DATE = "01/01/1900";
    131 
    132     /**
    133      * The default maximal date.
    134      */
    135     private static final String DEFAULT_MAX_DATE = "01/01/2100";
    136 
    137     private static final int DEFAULT_SHOWN_WEEK_COUNT = 6;
    138 
    139     private static final int DEFAULT_DATE_TEXT_SIZE = 14;
    140 
    141     private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6;
    142 
    143     private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12;
    144 
    145     private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2;
    146 
    147     private static final int UNSCALED_BOTTOM_BUFFER = 20;
    148 
    149     private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1;
    150 
    151     private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1;
    152 
    153     private final int mWeekSeperatorLineWidth;
    154 
    155     private final int mDateTextSize;
    156 
    157     private final Drawable mSelectedDateVerticalBar;
    158 
    159     private final int mSelectedDateVerticalBarWidth;
    160 
    161     private final int mSelectedWeekBackgroundColor;
    162 
    163     private final int mFocusedMonthDateColor;
    164 
    165     private final int mUnfocusedMonthDateColor;
    166 
    167     private final int mWeekSeparatorLineColor;
    168 
    169     private final int mWeekNumberColor;
    170 
    171     /**
    172      * The top offset of the weeks list.
    173      */
    174     private int mListScrollTopOffset = 2;
    175 
    176     /**
    177      * The visible height of a week view.
    178      */
    179     private int mWeekMinVisibleHeight = 12;
    180 
    181     /**
    182      * The visible height of a week view.
    183      */
    184     private int mBottomBuffer = 20;
    185 
    186     /**
    187      * The number of shown weeks.
    188      */
    189     private int mShownWeekCount;
    190 
    191     /**
    192      * Flag whether to show the week number.
    193      */
    194     private boolean mShowWeekNumber;
    195 
    196     /**
    197      * The number of day per week to be shown.
    198      */
    199     private int mDaysPerWeek = 7;
    200 
    201     /**
    202      * The friction of the week list while flinging.
    203      */
    204     private float mFriction = .05f;
    205 
    206     /**
    207      * Scale for adjusting velocity of the week list while flinging.
    208      */
    209     private float mVelocityScale = 0.333f;
    210 
    211     /**
    212      * The adapter for the weeks list.
    213      */
    214     private WeeksAdapter mAdapter;
    215 
    216     /**
    217      * The weeks list.
    218      */
    219     private ListView mListView;
    220 
    221     /**
    222      * The name of the month to display.
    223      */
    224     private TextView mMonthName;
    225 
    226     /**
    227      * The header with week day names.
    228      */
    229     private ViewGroup mDayNamesHeader;
    230 
    231     /**
    232      * Cached labels for the week names header.
    233      */
    234     private String[] mDayLabels;
    235 
    236     /**
    237      * The first day of the week.
    238      */
    239     private int mFirstDayOfWeek;
    240 
    241     /**
    242      * Which month should be displayed/highlighted [0-11].
    243      */
    244     private int mCurrentMonthDisplayed;
    245 
    246     /**
    247      * Used for tracking during a scroll.
    248      */
    249     private long mPreviousScrollPosition;
    250 
    251     /**
    252      * Used for tracking which direction the view is scrolling.
    253      */
    254     private boolean mIsScrollingUp = false;
    255 
    256     /**
    257      * The previous scroll state of the weeks ListView.
    258      */
    259     private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;
    260 
    261     /**
    262      * The current scroll state of the weeks ListView.
    263      */
    264     private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;
    265 
    266     /**
    267      * Listener for changes in the selected day.
    268      */
    269     private OnDateChangeListener mOnDateChangeListener;
    270 
    271     /**
    272      * Command for adjusting the position after a scroll/fling.
    273      */
    274     private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();
    275 
    276     /**
    277      * Temporary instance to avoid multiple instantiations.
    278      */
    279     private Calendar mTempDate;
    280 
    281     /**
    282      * The first day of the focused month.
    283      */
    284     private Calendar mFirstDayOfMonth;
    285 
    286     /**
    287      * The start date of the range supported by this picker.
    288      */
    289     private Calendar mMinDate;
    290 
    291     /**
    292      * The end date of the range supported by this picker.
    293      */
    294     private Calendar mMaxDate;
    295 
    296     /**
    297      * Date format for parsing dates.
    298      */
    299     private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
    300 
    301     /**
    302      * The current locale.
    303      */
    304     private Locale mCurrentLocale;
    305 
    306     /**
    307      * The callback used to indicate the user changes the date.
    308      */
    309     public interface OnDateChangeListener {
    310 
    311         /**
    312          * Called upon change of the selected day.
    313          *
    314          * @param view The view associated with this listener.
    315          * @param year The year that was set.
    316          * @param month The month that was set [0-11].
    317          * @param dayOfMonth The day of the month that was set.
    318          */
    319         public void onSelectedDayChange(CalendarView view, int year, int month, int dayOfMonth);
    320     }
    321 
    322     public CalendarView(Context context) {
    323         this(context, null);
    324     }
    325 
    326     public CalendarView(Context context, AttributeSet attrs) {
    327         this(context, attrs, 0);
    328     }
    329 
    330     public CalendarView(Context context, AttributeSet attrs, int defStyle) {
    331         super(context, attrs, 0);
    332 
    333         // initialization based on locale
    334         setCurrentLocale(Locale.getDefault());
    335 
    336         TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.CalendarView,
    337                 R.attr.calendarViewStyle, 0);
    338         mShowWeekNumber = attributesArray.getBoolean(R.styleable.CalendarView_showWeekNumber,
    339                 DEFAULT_SHOW_WEEK_NUMBER);
    340         mFirstDayOfWeek = attributesArray.getInt(R.styleable.CalendarView_firstDayOfWeek,
    341                 LocaleData.get(Locale.getDefault()).firstDayOfWeek);
    342         String minDate = attributesArray.getString(R.styleable.CalendarView_minDate);
    343         if (TextUtils.isEmpty(minDate) || !parseDate(minDate, mMinDate)) {
    344             parseDate(DEFAULT_MIN_DATE, mMinDate);
    345         }
    346         String maxDate = attributesArray.getString(R.styleable.CalendarView_maxDate);
    347         if (TextUtils.isEmpty(maxDate) || !parseDate(maxDate, mMaxDate)) {
    348             parseDate(DEFAULT_MAX_DATE, mMaxDate);
    349         }
    350         if (mMaxDate.before(mMinDate)) {
    351             throw new IllegalArgumentException("Max date cannot be before min date.");
    352         }
    353         mShownWeekCount = attributesArray.getInt(R.styleable.CalendarView_shownWeekCount,
    354                 DEFAULT_SHOWN_WEEK_COUNT);
    355         mSelectedWeekBackgroundColor = attributesArray.getColor(
    356                 R.styleable.CalendarView_selectedWeekBackgroundColor, 0);
    357         mFocusedMonthDateColor = attributesArray.getColor(
    358                 R.styleable.CalendarView_focusedMonthDateColor, 0);
    359         mUnfocusedMonthDateColor = attributesArray.getColor(
    360                 R.styleable.CalendarView_unfocusedMonthDateColor, 0);
    361         mWeekSeparatorLineColor = attributesArray.getColor(
    362                 R.styleable.CalendarView_weekSeparatorLineColor, 0);
    363         mWeekNumberColor = attributesArray.getColor(R.styleable.CalendarView_weekNumberColor, 0);
    364         mSelectedDateVerticalBar = attributesArray.getDrawable(
    365                 R.styleable.CalendarView_selectedDateVerticalBar);
    366 
    367         int dateTextAppearanceResId= attributesArray.getResourceId(
    368                 R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small);
    369         TypedArray dateTextAppearance = context.obtainStyledAttributes(dateTextAppearanceResId,
    370                 com.android.internal.R.styleable.TextAppearance);
    371         mDateTextSize = dateTextAppearance.getDimensionPixelSize(
    372                 R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE);
    373         dateTextAppearance.recycle();
    374 
    375         int weekDayTextAppearanceResId = attributesArray.getResourceId(
    376                 R.styleable.CalendarView_weekDayTextAppearance,
    377                 DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
    378         attributesArray.recycle();
    379 
    380         DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
    381         mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
    382                 UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics);
    383         mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
    384                 UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics);
    385         mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
    386                 UNSCALED_BOTTOM_BUFFER, displayMetrics);
    387         mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
    388                 UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics);
    389         mWeekSeperatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
    390                 UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics);
    391 
    392         LayoutInflater layoutInflater = (LayoutInflater) mContext
    393                 .getSystemService(Service.LAYOUT_INFLATER_SERVICE);
    394         View content = layoutInflater.inflate(R.layout.calendar_view, null, false);
    395         addView(content);
    396 
    397         mListView = (ListView) findViewById(R.id.list);
    398         mDayNamesHeader = (ViewGroup) content.findViewById(com.android.internal.R.id.day_names);
    399         mMonthName = (TextView) content.findViewById(com.android.internal.R.id.month_name);
    400 
    401         setUpHeader(weekDayTextAppearanceResId);
    402         setUpListView();
    403         setUpAdapter();
    404 
    405         // go to today or whichever is close to today min or max date
    406         mTempDate.setTimeInMillis(System.currentTimeMillis());
    407         if (mTempDate.before(mMinDate)) {
    408             goTo(mMinDate, false, true, true);
    409         } else if (mMaxDate.before(mTempDate)) {
    410             goTo(mMaxDate, false, true, true);
    411         } else {
    412             goTo(mTempDate, false, true, true);
    413         }
    414 
    415         invalidate();
    416     }
    417 
    418     @Override
    419     public void setEnabled(boolean enabled) {
    420         mListView.setEnabled(enabled);
    421     }
    422 
    423     @Override
    424     public boolean isEnabled() {
    425         return mListView.isEnabled();
    426     }
    427 
    428     @Override
    429     protected void onConfigurationChanged(Configuration newConfig) {
    430         super.onConfigurationChanged(newConfig);
    431         setCurrentLocale(newConfig.locale);
    432     }
    433 
    434     /**
    435      * Gets the minimal date supported by this {@link CalendarView} in milliseconds
    436      * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
    437      * zone.
    438      * <p>
    439      * Note: The default minimal date is 01/01/1900.
    440      * <p>
    441      *
    442      * @return The minimal supported date.
    443      */
    444     public long getMinDate() {
    445         return mMinDate.getTimeInMillis();
    446     }
    447 
    448     /**
    449      * Sets the minimal date supported by this {@link CalendarView} in milliseconds
    450      * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
    451      * zone.
    452      *
    453      * @param minDate The minimal supported date.
    454      */
    455     public void setMinDate(long minDate) {
    456         mTempDate.setTimeInMillis(minDate);
    457         if (isSameDate(mTempDate, mMinDate)) {
    458             return;
    459         }
    460         mMinDate.setTimeInMillis(minDate);
    461         // make sure the current date is not earlier than
    462         // the new min date since the latter is used for
    463         // calculating the indices in the adapter thus
    464         // avoiding out of bounds error
    465         Calendar date = mAdapter.mSelectedDate;
    466         if (date.before(mMinDate)) {
    467             mAdapter.setSelectedDay(mMinDate);
    468         }
    469         // reinitialize the adapter since its range depends on min date
    470         mAdapter.init();
    471         if (date.before(mMinDate)) {
    472             setDate(mTempDate.getTimeInMillis());
    473         } else {
    474             // we go to the current date to force the ListView to query its
    475             // adapter for the shown views since we have changed the adapter
    476             // range and the base from which the later calculates item indices
    477             // note that calling setDate will not work since the date is the same
    478             goTo(date, false, true, false);
    479         }
    480     }
    481 
    482     /**
    483      * Gets the maximal date supported by this {@link CalendarView} in milliseconds
    484      * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
    485      * zone.
    486      * <p>
    487      * Note: The default maximal date is 01/01/2100.
    488      * <p>
    489      *
    490      * @return The maximal supported date.
    491      */
    492     public long getMaxDate() {
    493         return mMaxDate.getTimeInMillis();
    494     }
    495 
    496     /**
    497      * Sets the maximal date supported by this {@link CalendarView} in milliseconds
    498      * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
    499      * zone.
    500      *
    501      * @param maxDate The maximal supported date.
    502      */
    503     public void setMaxDate(long maxDate) {
    504         mTempDate.setTimeInMillis(maxDate);
    505         if (isSameDate(mTempDate, mMaxDate)) {
    506             return;
    507         }
    508         mMaxDate.setTimeInMillis(maxDate);
    509         // reinitialize the adapter since its range depends on max date
    510         mAdapter.init();
    511         Calendar date = mAdapter.mSelectedDate;
    512         if (date.after(mMaxDate)) {
    513             setDate(mMaxDate.getTimeInMillis());
    514         } else {
    515             // we go to the current date to force the ListView to query its
    516             // adapter for the shown views since we have changed the adapter
    517             // range and the base from which the later calculates item indices
    518             // note that calling setDate will not work since the date is the same
    519             goTo(date, false, true, false);
    520         }
    521     }
    522 
    523     /**
    524      * Sets whether to show the week number.
    525      *
    526      * @param showWeekNumber True to show the week number.
    527      */
    528     public void setShowWeekNumber(boolean showWeekNumber) {
    529         if (mShowWeekNumber == showWeekNumber) {
    530             return;
    531         }
    532         mShowWeekNumber = showWeekNumber;
    533         mAdapter.notifyDataSetChanged();
    534         setUpHeader(DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
    535     }
    536 
    537     /**
    538      * Gets whether to show the week number.
    539      *
    540      * @return True if showing the week number.
    541      */
    542     public boolean getShowWeekNumber() {
    543         return mShowWeekNumber;
    544     }
    545 
    546     /**
    547      * Gets the first day of week.
    548      *
    549      * @return The first day of the week conforming to the {@link CalendarView}
    550      *         APIs.
    551      * @see Calendar#MONDAY
    552      * @see Calendar#TUESDAY
    553      * @see Calendar#WEDNESDAY
    554      * @see Calendar#THURSDAY
    555      * @see Calendar#FRIDAY
    556      * @see Calendar#SATURDAY
    557      * @see Calendar#SUNDAY
    558      */
    559     public int getFirstDayOfWeek() {
    560         return mFirstDayOfWeek;
    561     }
    562 
    563     /**
    564      * Sets the first day of week.
    565      *
    566      * @param firstDayOfWeek The first day of the week conforming to the
    567      *            {@link CalendarView} APIs.
    568      * @see Calendar#MONDAY
    569      * @see Calendar#TUESDAY
    570      * @see Calendar#WEDNESDAY
    571      * @see Calendar#THURSDAY
    572      * @see Calendar#FRIDAY
    573      * @see Calendar#SATURDAY
    574      * @see Calendar#SUNDAY
    575      */
    576     public void setFirstDayOfWeek(int firstDayOfWeek) {
    577         if (mFirstDayOfWeek == firstDayOfWeek) {
    578             return;
    579         }
    580         mFirstDayOfWeek = firstDayOfWeek;
    581         mAdapter.init();
    582         mAdapter.notifyDataSetChanged();
    583         setUpHeader(DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
    584     }
    585 
    586     /**
    587      * Sets the listener to be notified upon selected date change.
    588      *
    589      * @param listener The listener to be notified.
    590      */
    591     public void setOnDateChangeListener(OnDateChangeListener listener) {
    592         mOnDateChangeListener = listener;
    593     }
    594 
    595     /**
    596      * Gets the selected date in milliseconds since January 1, 1970 00:00:00 in
    597      * {@link TimeZone#getDefault()} time zone.
    598      *
    599      * @return The selected date.
    600      */
    601     public long getDate() {
    602         return mAdapter.mSelectedDate.getTimeInMillis();
    603     }
    604 
    605     /**
    606      * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
    607      * {@link TimeZone#getDefault()} time zone.
    608      *
    609      * @param date The selected date.
    610      *
    611      * @throws IllegalArgumentException of the provided date is before the
    612      *        minimal or after the maximal date.
    613      *
    614      * @see #setDate(long, boolean, boolean)
    615      * @see #setMinDate(long)
    616      * @see #setMaxDate(long)
    617      */
    618     public void setDate(long date) {
    619         setDate(date, false, false);
    620     }
    621 
    622     /**
    623      * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
    624      * {@link TimeZone#getDefault()} time zone.
    625      *
    626      * @param date The date.
    627      * @param animate Whether to animate the scroll to the current date.
    628      * @param center Whether to center the current date even if it is already visible.
    629      *
    630      * @throws IllegalArgumentException of the provided date is before the
    631      *        minimal or after the maximal date.
    632      *
    633      * @see #setMinDate(long)
    634      * @see #setMaxDate(long)
    635      */
    636     public void setDate(long date, boolean animate, boolean center) {
    637         mTempDate.setTimeInMillis(date);
    638         if (isSameDate(mTempDate, mAdapter.mSelectedDate)) {
    639             return;
    640         }
    641         goTo(mTempDate, animate, true, center);
    642     }
    643 
    644     /**
    645      * Sets the current locale.
    646      *
    647      * @param locale The current locale.
    648      */
    649     private void setCurrentLocale(Locale locale) {
    650         if (locale.equals(mCurrentLocale)) {
    651             return;
    652         }
    653 
    654         mCurrentLocale = locale;
    655 
    656         mTempDate = getCalendarForLocale(mTempDate, locale);
    657         mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale);
    658         mMinDate = getCalendarForLocale(mMinDate, locale);
    659         mMaxDate = getCalendarForLocale(mMaxDate, locale);
    660     }
    661 
    662     /**
    663      * Gets a calendar for locale bootstrapped with the value of a given calendar.
    664      *
    665      * @param oldCalendar The old calendar.
    666      * @param locale The locale.
    667      */
    668     private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
    669         if (oldCalendar == null) {
    670             return Calendar.getInstance(locale);
    671         } else {
    672             final long currentTimeMillis = oldCalendar.getTimeInMillis();
    673             Calendar newCalendar = Calendar.getInstance(locale);
    674             newCalendar.setTimeInMillis(currentTimeMillis);
    675             return newCalendar;
    676         }
    677     }
    678 
    679     /**
    680      * @return True if the <code>firstDate</code> is the same as the <code>
    681      * secondDate</code>.
    682      */
    683     private boolean isSameDate(Calendar firstDate, Calendar secondDate) {
    684         return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR)
    685                 && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR));
    686     }
    687 
    688     /**
    689      * Creates a new adapter if necessary and sets up its parameters.
    690      */
    691     private void setUpAdapter() {
    692         if (mAdapter == null) {
    693             mAdapter = new WeeksAdapter(getContext());
    694             mAdapter.registerDataSetObserver(new DataSetObserver() {
    695                 @Override
    696                 public void onChanged() {
    697                     if (mOnDateChangeListener != null) {
    698                         Calendar selectedDay = mAdapter.getSelectedDay();
    699                         mOnDateChangeListener.onSelectedDayChange(CalendarView.this,
    700                                 selectedDay.get(Calendar.YEAR),
    701                                 selectedDay.get(Calendar.MONTH),
    702                                 selectedDay.get(Calendar.DAY_OF_MONTH));
    703                     }
    704                 }
    705             });
    706             mListView.setAdapter(mAdapter);
    707         }
    708 
    709         // refresh the view with the new parameters
    710         mAdapter.notifyDataSetChanged();
    711     }
    712 
    713     /**
    714      * Sets up the strings to be used by the header.
    715      */
    716     private void setUpHeader(int weekDayTextAppearanceResId) {
    717         mDayLabels = new String[mDaysPerWeek];
    718         for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) {
    719             int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i;
    720             mDayLabels[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay,
    721                     DateUtils.LENGTH_SHORTEST);
    722         }
    723 
    724         TextView label = (TextView) mDayNamesHeader.getChildAt(0);
    725         if (mShowWeekNumber) {
    726             label.setVisibility(View.VISIBLE);
    727         } else {
    728             label.setVisibility(View.GONE);
    729         }
    730         for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) {
    731             label = (TextView) mDayNamesHeader.getChildAt(i);
    732             if (weekDayTextAppearanceResId > -1) {
    733                 label.setTextAppearance(mContext, weekDayTextAppearanceResId);
    734             }
    735             if (i < mDaysPerWeek + 1) {
    736                 label.setText(mDayLabels[i - 1]);
    737                 label.setVisibility(View.VISIBLE);
    738             } else {
    739                 label.setVisibility(View.GONE);
    740             }
    741         }
    742         mDayNamesHeader.invalidate();
    743     }
    744 
    745     /**
    746      * Sets all the required fields for the list view.
    747      */
    748     private void setUpListView() {
    749         // Configure the listview
    750         mListView.setDivider(null);
    751         mListView.setItemsCanFocus(true);
    752         mListView.setVerticalScrollBarEnabled(false);
    753         mListView.setOnScrollListener(new OnScrollListener() {
    754             public void onScrollStateChanged(AbsListView view, int scrollState) {
    755                 CalendarView.this.onScrollStateChanged(view, scrollState);
    756             }
    757 
    758             public void onScroll(
    759                     AbsListView view, int firstVisibleItem, int visibleItemCount,
    760                     int totalItemCount) {
    761                 CalendarView.this.onScroll(view, firstVisibleItem, visibleItemCount,
    762                         totalItemCount);
    763             }
    764         });
    765         // Make the scrolling behavior nicer
    766         mListView.setFriction(mFriction);
    767         mListView.setVelocityScale(mVelocityScale);
    768     }
    769 
    770     /**
    771      * This moves to the specified time in the view. If the time is not already
    772      * in range it will move the list so that the first of the month containing
    773      * the time is at the top of the view. If the new time is already in view
    774      * the list will not be scrolled unless forceScroll is true. This time may
    775      * optionally be highlighted as selected as well.
    776      *
    777      * @param date The time to move to.
    778      * @param animate Whether to scroll to the given time or just redraw at the
    779      *            new location.
    780      * @param setSelected Whether to set the given time as selected.
    781      * @param forceScroll Whether to recenter even if the time is already
    782      *            visible.
    783      *
    784      * @throws IllegalArgumentException of the provided date is before the
    785      *        range start of after the range end.
    786      */
    787     private void goTo(Calendar date, boolean animate, boolean setSelected, boolean forceScroll) {
    788         if (date.before(mMinDate) || date.after(mMaxDate)) {
    789             throw new IllegalArgumentException("Time not between " + mMinDate.getTime()
    790                     + " and " + mMaxDate.getTime());
    791         }
    792         // Find the first and last entirely visible weeks
    793         int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
    794         View firstChild = mListView.getChildAt(0);
    795         if (firstChild != null && firstChild.getTop() < 0) {
    796             firstFullyVisiblePosition++;
    797         }
    798         int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1;
    799         if (firstChild != null && firstChild.getTop() > mBottomBuffer) {
    800             lastFullyVisiblePosition--;
    801         }
    802         if (setSelected) {
    803             mAdapter.setSelectedDay(date);
    804         }
    805         // Get the week we're going to
    806         int position = getWeeksSinceMinDate(date);
    807 
    808         // Check if the selected day is now outside of our visible range
    809         // and if so scroll to the month that contains it
    810         if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition
    811                 || forceScroll) {
    812             mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis());
    813             mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1);
    814 
    815             setMonthDisplayed(mFirstDayOfMonth);
    816 
    817             // the earliest time we can scroll to is the min date
    818             if (mFirstDayOfMonth.before(mMinDate)) {
    819                 position = 0;
    820             } else {
    821                 position = getWeeksSinceMinDate(mFirstDayOfMonth);
    822             }
    823 
    824             mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
    825             if (animate) {
    826                 mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset,
    827                         GOTO_SCROLL_DURATION);
    828             } else {
    829                 mListView.setSelectionFromTop(position, mListScrollTopOffset);
    830                 // Perform any after scroll operations that are needed
    831                 onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE);
    832             }
    833         } else if (setSelected) {
    834             // Otherwise just set the selection
    835             setMonthDisplayed(date);
    836         }
    837     }
    838 
    839     /**
    840      * Parses the given <code>date</code> and in case of success sets
    841      * the result to the <code>outDate</code>.
    842      *
    843      * @return True if the date was parsed.
    844      */
    845     private boolean parseDate(String date, Calendar outDate) {
    846         try {
    847             outDate.setTime(mDateFormat.parse(date));
    848             return true;
    849         } catch (ParseException e) {
    850             Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
    851             return false;
    852         }
    853     }
    854 
    855     /**
    856      * Called when a <code>view</code> transitions to a new <code>scrollState
    857      * </code>.
    858      */
    859     private void onScrollStateChanged(AbsListView view, int scrollState) {
    860         mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
    861     }
    862 
    863     /**
    864      * Updates the title and selected month if the <code>view</code> has moved to a new
    865      * month.
    866      */
    867     private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    868             int totalItemCount) {
    869         WeekView child = (WeekView) view.getChildAt(0);
    870         if (child == null) {
    871             return;
    872         }
    873 
    874         // Figure out where we are
    875         long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
    876 
    877         // If we have moved since our last call update the direction
    878         if (currScroll < mPreviousScrollPosition) {
    879             mIsScrollingUp = true;
    880         } else if (currScroll > mPreviousScrollPosition) {
    881             mIsScrollingUp = false;
    882         } else {
    883             return;
    884         }
    885 
    886         // Use some hysteresis for checking which month to highlight. This
    887         // causes the month to transition when two full weeks of a month are
    888         // visible when scrolling up, and when the first day in a month reaches
    889         // the top of the screen when scrolling down.
    890         int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0;
    891         if (mIsScrollingUp) {
    892             child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset);
    893         } else if (offset != 0) {
    894             child = (WeekView) view.getChildAt(offset);
    895         }
    896 
    897         // Find out which month we're moving into
    898         int month;
    899         if (mIsScrollingUp) {
    900             month = child.getMonthOfFirstWeekDay();
    901         } else {
    902             month = child.getMonthOfLastWeekDay();
    903         }
    904 
    905         // And how it relates to our current highlighted month
    906         int monthDiff;
    907         if (mCurrentMonthDisplayed == 11 && month == 0) {
    908             monthDiff = 1;
    909         } else if (mCurrentMonthDisplayed == 0 && month == 11) {
    910             monthDiff = -1;
    911         } else {
    912             monthDiff = month - mCurrentMonthDisplayed;
    913         }
    914 
    915         // Only switch months if we're scrolling away from the currently
    916         // selected month
    917         if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) {
    918             Calendar firstDay = child.getFirstDay();
    919             if (mIsScrollingUp) {
    920                 firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK);
    921             } else {
    922                 firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK);
    923             }
    924             setMonthDisplayed(firstDay);
    925         }
    926         mPreviousScrollPosition = currScroll;
    927         mPreviousScrollState = mCurrentScrollState;
    928     }
    929 
    930     /**
    931      * Sets the month displayed at the top of this view based on time. Override
    932      * to add custom events when the title is changed.
    933      *
    934      * @param calendar A day in the new focus month.
    935      */
    936     private void setMonthDisplayed(Calendar calendar) {
    937         final int newMonthDisplayed = calendar.get(Calendar.MONTH);
    938         if (mCurrentMonthDisplayed != newMonthDisplayed) {
    939             mCurrentMonthDisplayed = newMonthDisplayed;
    940             mAdapter.setFocusMonth(mCurrentMonthDisplayed);
    941             final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
    942                     | DateUtils.FORMAT_SHOW_YEAR;
    943             final long millis = calendar.getTimeInMillis();
    944             String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags);
    945             mMonthName.setText(newMonthName);
    946             mMonthName.invalidate();
    947         }
    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