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