CalendarView.java revision 1442da620c117910e7628b36efc32ff962be02b2
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 android.annotation.Widget; 20import android.app.Service; 21import android.content.Context; 22import android.content.res.Configuration; 23import android.content.res.TypedArray; 24import android.database.DataSetObserver; 25import android.graphics.Canvas; 26import android.graphics.Paint; 27import android.graphics.Paint.Align; 28import android.graphics.Paint.Style; 29import android.graphics.Rect; 30import android.graphics.drawable.Drawable; 31import android.text.TextUtils; 32import android.text.format.DateUtils; 33import android.util.AttributeSet; 34import android.util.DisplayMetrics; 35import android.util.Log; 36import android.util.TypedValue; 37import android.view.GestureDetector; 38import android.view.LayoutInflater; 39import android.view.MotionEvent; 40import android.view.View; 41import android.view.ViewGroup; 42import android.widget.AbsListView.OnScrollListener; 43 44import com.android.internal.R; 45 46import java.text.ParseException; 47import java.text.SimpleDateFormat; 48import java.util.Calendar; 49import java.util.Locale; 50import java.util.TimeZone; 51 52import 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 74public 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