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