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 = -1; 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) context 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 setUpHeader(); 878 } 879 880 /** 881 * Sets the listener to be notified upon selected date change. 882 * 883 * @param listener The listener to be notified. 884 */ 885 public void setOnDateChangeListener(OnDateChangeListener listener) { 886 mOnDateChangeListener = listener; 887 } 888 889 /** 890 * Gets the selected date in milliseconds since January 1, 1970 00:00:00 in 891 * {@link TimeZone#getDefault()} time zone. 892 * 893 * @return The selected date. 894 */ 895 public long getDate() { 896 return mAdapter.mSelectedDate.getTimeInMillis(); 897 } 898 899 /** 900 * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in 901 * {@link TimeZone#getDefault()} time zone. 902 * 903 * @param date The selected date. 904 * 905 * @throws IllegalArgumentException of the provided date is before the 906 * minimal or after the maximal date. 907 * 908 * @see #setDate(long, boolean, boolean) 909 * @see #setMinDate(long) 910 * @see #setMaxDate(long) 911 */ 912 public void setDate(long date) { 913 setDate(date, false, false); 914 } 915 916 /** 917 * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in 918 * {@link TimeZone#getDefault()} time zone. 919 * 920 * @param date The date. 921 * @param animate Whether to animate the scroll to the current date. 922 * @param center Whether to center the current date even if it is already visible. 923 * 924 * @throws IllegalArgumentException of the provided date is before the 925 * minimal or after the maximal date. 926 * 927 * @see #setMinDate(long) 928 * @see #setMaxDate(long) 929 */ 930 public void setDate(long date, boolean animate, boolean center) { 931 mTempDate.setTimeInMillis(date); 932 if (isSameDate(mTempDate, mAdapter.mSelectedDate)) { 933 return; 934 } 935 goTo(mTempDate, animate, true, center); 936 } 937 938 private void updateDateTextSize() { 939 TypedArray dateTextAppearance = mContext.obtainStyledAttributes( 940 mDateTextAppearanceResId, R.styleable.TextAppearance); 941 mDateTextSize = dateTextAppearance.getDimensionPixelSize( 942 R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE); 943 dateTextAppearance.recycle(); 944 } 945 946 /** 947 * Invalidates all week views. 948 */ 949 private void invalidateAllWeekViews() { 950 final int childCount = mListView.getChildCount(); 951 for (int i = 0; i < childCount; i++) { 952 View view = mListView.getChildAt(i); 953 view.invalidate(); 954 } 955 } 956 957 /** 958 * Sets the current locale. 959 * 960 * @param locale The current locale. 961 */ 962 private void setCurrentLocale(Locale locale) { 963 if (locale.equals(mCurrentLocale)) { 964 return; 965 } 966 967 mCurrentLocale = locale; 968 969 mTempDate = getCalendarForLocale(mTempDate, locale); 970 mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale); 971 mMinDate = getCalendarForLocale(mMinDate, locale); 972 mMaxDate = getCalendarForLocale(mMaxDate, locale); 973 } 974 975 /** 976 * Gets a calendar for locale bootstrapped with the value of a given calendar. 977 * 978 * @param oldCalendar The old calendar. 979 * @param locale The locale. 980 */ 981 private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { 982 if (oldCalendar == null) { 983 return Calendar.getInstance(locale); 984 } else { 985 final long currentTimeMillis = oldCalendar.getTimeInMillis(); 986 Calendar newCalendar = Calendar.getInstance(locale); 987 newCalendar.setTimeInMillis(currentTimeMillis); 988 return newCalendar; 989 } 990 } 991 992 /** 993 * @return True if the <code>firstDate</code> is the same as the <code> 994 * secondDate</code>. 995 */ 996 private boolean isSameDate(Calendar firstDate, Calendar secondDate) { 997 return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR) 998 && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR)); 999 } 1000 1001 /** 1002 * Creates a new adapter if necessary and sets up its parameters. 1003 */ 1004 private void setUpAdapter() { 1005 if (mAdapter == null) { 1006 mAdapter = new WeeksAdapter(); 1007 mAdapter.registerDataSetObserver(new DataSetObserver() { 1008 @Override 1009 public void onChanged() { 1010 if (mOnDateChangeListener != null) { 1011 Calendar selectedDay = mAdapter.getSelectedDay(); 1012 mOnDateChangeListener.onSelectedDayChange(CalendarView.this, 1013 selectedDay.get(Calendar.YEAR), 1014 selectedDay.get(Calendar.MONTH), 1015 selectedDay.get(Calendar.DAY_OF_MONTH)); 1016 } 1017 } 1018 }); 1019 mListView.setAdapter(mAdapter); 1020 } 1021 1022 // refresh the view with the new parameters 1023 mAdapter.notifyDataSetChanged(); 1024 } 1025 1026 /** 1027 * Sets up the strings to be used by the header. 1028 */ 1029 private void setUpHeader() { 1030 final String[] tinyWeekdayNames = LocaleData.get(Locale.getDefault()).tinyWeekdayNames; 1031 mDayLabels = new String[mDaysPerWeek]; 1032 for (int i = 0; i < mDaysPerWeek; i++) { 1033 final int j = i + mFirstDayOfWeek; 1034 final int calendarDay = (j > Calendar.SATURDAY) ? j - Calendar.SATURDAY : j; 1035 mDayLabels[i] = tinyWeekdayNames[calendarDay]; 1036 } 1037 // Deal with week number 1038 TextView label = (TextView) mDayNamesHeader.getChildAt(0); 1039 if (mShowWeekNumber) { 1040 label.setVisibility(View.VISIBLE); 1041 } else { 1042 label.setVisibility(View.GONE); 1043 } 1044 // Deal with day labels 1045 final int count = mDayNamesHeader.getChildCount(); 1046 for (int i = 0; i < count - 1; i++) { 1047 label = (TextView) mDayNamesHeader.getChildAt(i + 1); 1048 if (mWeekDayTextAppearanceResId > -1) { 1049 label.setTextAppearance(mContext, mWeekDayTextAppearanceResId); 1050 } 1051 if (i < mDaysPerWeek) { 1052 label.setText(mDayLabels[i]); 1053 label.setVisibility(View.VISIBLE); 1054 } else { 1055 label.setVisibility(View.GONE); 1056 } 1057 } 1058 mDayNamesHeader.invalidate(); 1059 } 1060 1061 /** 1062 * Sets all the required fields for the list view. 1063 */ 1064 private void setUpListView() { 1065 // Configure the listview 1066 mListView.setDivider(null); 1067 mListView.setItemsCanFocus(true); 1068 mListView.setVerticalScrollBarEnabled(false); 1069 mListView.setOnScrollListener(new OnScrollListener() { 1070 public void onScrollStateChanged(AbsListView view, int scrollState) { 1071 CalendarView.this.onScrollStateChanged(view, scrollState); 1072 } 1073 1074 public void onScroll( 1075 AbsListView view, int firstVisibleItem, int visibleItemCount, 1076 int totalItemCount) { 1077 CalendarView.this.onScroll(view, firstVisibleItem, visibleItemCount, 1078 totalItemCount); 1079 } 1080 }); 1081 // Make the scrolling behavior nicer 1082 mListView.setFriction(mFriction); 1083 mListView.setVelocityScale(mVelocityScale); 1084 } 1085 1086 /** 1087 * This moves to the specified time in the view. If the time is not already 1088 * in range it will move the list so that the first of the month containing 1089 * the time is at the top of the view. If the new time is already in view 1090 * the list will not be scrolled unless forceScroll is true. This time may 1091 * optionally be highlighted as selected as well. 1092 * 1093 * @param date The time to move to. 1094 * @param animate Whether to scroll to the given time or just redraw at the 1095 * new location. 1096 * @param setSelected Whether to set the given time as selected. 1097 * @param forceScroll Whether to recenter even if the time is already 1098 * visible. 1099 * 1100 * @throws IllegalArgumentException of the provided date is before the 1101 * range start of after the range end. 1102 */ 1103 private void goTo(Calendar date, boolean animate, boolean setSelected, boolean forceScroll) { 1104 if (date.before(mMinDate) || date.after(mMaxDate)) { 1105 throw new IllegalArgumentException("Time not between " + mMinDate.getTime() 1106 + " and " + mMaxDate.getTime()); 1107 } 1108 // Find the first and last entirely visible weeks 1109 int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); 1110 View firstChild = mListView.getChildAt(0); 1111 if (firstChild != null && firstChild.getTop() < 0) { 1112 firstFullyVisiblePosition++; 1113 } 1114 int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1; 1115 if (firstChild != null && firstChild.getTop() > mBottomBuffer) { 1116 lastFullyVisiblePosition--; 1117 } 1118 if (setSelected) { 1119 mAdapter.setSelectedDay(date); 1120 } 1121 // Get the week we're going to 1122 int position = getWeeksSinceMinDate(date); 1123 1124 // Check if the selected day is now outside of our visible range 1125 // and if so scroll to the month that contains it 1126 if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition 1127 || forceScroll) { 1128 mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis()); 1129 mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1); 1130 1131 setMonthDisplayed(mFirstDayOfMonth); 1132 1133 // the earliest time we can scroll to is the min date 1134 if (mFirstDayOfMonth.before(mMinDate)) { 1135 position = 0; 1136 } else { 1137 position = getWeeksSinceMinDate(mFirstDayOfMonth); 1138 } 1139 1140 mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; 1141 if (animate) { 1142 mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset, 1143 GOTO_SCROLL_DURATION); 1144 } else { 1145 mListView.setSelectionFromTop(position, mListScrollTopOffset); 1146 // Perform any after scroll operations that are needed 1147 onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE); 1148 } 1149 } else if (setSelected) { 1150 // Otherwise just set the selection 1151 setMonthDisplayed(date); 1152 } 1153 } 1154 1155 /** 1156 * Parses the given <code>date</code> and in case of success sets 1157 * the result to the <code>outDate</code>. 1158 * 1159 * @return True if the date was parsed. 1160 */ 1161 private boolean parseDate(String date, Calendar outDate) { 1162 try { 1163 outDate.setTime(mDateFormat.parse(date)); 1164 return true; 1165 } catch (ParseException e) { 1166 Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); 1167 return false; 1168 } 1169 } 1170 1171 /** 1172 * Called when a <code>view</code> transitions to a new <code>scrollState 1173 * </code>. 1174 */ 1175 private void onScrollStateChanged(AbsListView view, int scrollState) { 1176 mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); 1177 } 1178 1179 /** 1180 * Updates the title and selected month if the <code>view</code> has moved to a new 1181 * month. 1182 */ 1183 private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1184 int totalItemCount) { 1185 WeekView child = (WeekView) view.getChildAt(0); 1186 if (child == null) { 1187 return; 1188 } 1189 1190 // Figure out where we are 1191 long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); 1192 1193 // If we have moved since our last call update the direction 1194 if (currScroll < mPreviousScrollPosition) { 1195 mIsScrollingUp = true; 1196 } else if (currScroll > mPreviousScrollPosition) { 1197 mIsScrollingUp = false; 1198 } else { 1199 return; 1200 } 1201 1202 // Use some hysteresis for checking which month to highlight. This 1203 // causes the month to transition when two full weeks of a month are 1204 // visible when scrolling up, and when the first day in a month reaches 1205 // the top of the screen when scrolling down. 1206 int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0; 1207 if (mIsScrollingUp) { 1208 child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset); 1209 } else if (offset != 0) { 1210 child = (WeekView) view.getChildAt(offset); 1211 } 1212 1213 // Find out which month we're moving into 1214 int month; 1215 if (mIsScrollingUp) { 1216 month = child.getMonthOfFirstWeekDay(); 1217 } else { 1218 month = child.getMonthOfLastWeekDay(); 1219 } 1220 1221 // And how it relates to our current highlighted month 1222 int monthDiff; 1223 if (mCurrentMonthDisplayed == 11 && month == 0) { 1224 monthDiff = 1; 1225 } else if (mCurrentMonthDisplayed == 0 && month == 11) { 1226 monthDiff = -1; 1227 } else { 1228 monthDiff = month - mCurrentMonthDisplayed; 1229 } 1230 1231 // Only switch months if we're scrolling away from the currently 1232 // selected month 1233 if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) { 1234 Calendar firstDay = child.getFirstDay(); 1235 if (mIsScrollingUp) { 1236 firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK); 1237 } else { 1238 firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK); 1239 } 1240 setMonthDisplayed(firstDay); 1241 } 1242 mPreviousScrollPosition = currScroll; 1243 mPreviousScrollState = mCurrentScrollState; 1244 } 1245 1246 /** 1247 * Sets the month displayed at the top of this view based on time. Override 1248 * to add custom events when the title is changed. 1249 * 1250 * @param calendar A day in the new focus month. 1251 */ 1252 private void setMonthDisplayed(Calendar calendar) { 1253 mCurrentMonthDisplayed = calendar.get(Calendar.MONTH); 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 * @return Returns the number of weeks between the current <code>date</code> 1265 * and the <code>mMinDate</code>. 1266 */ 1267 private int getWeeksSinceMinDate(Calendar date) { 1268 if (date.before(mMinDate)) { 1269 throw new IllegalArgumentException("fromDate: " + mMinDate.getTime() 1270 + " does not precede toDate: " + date.getTime()); 1271 } 1272 long endTimeMillis = date.getTimeInMillis() 1273 + date.getTimeZone().getOffset(date.getTimeInMillis()); 1274 long startTimeMillis = mMinDate.getTimeInMillis() 1275 + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis()); 1276 long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek) 1277 * MILLIS_IN_DAY; 1278 return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK); 1279 } 1280 1281 /** 1282 * Command responsible for acting upon scroll state changes. 1283 */ 1284 private class ScrollStateRunnable implements Runnable { 1285 private AbsListView mView; 1286 1287 private int mNewState; 1288 1289 /** 1290 * Sets up the runnable with a short delay in case the scroll state 1291 * immediately changes again. 1292 * 1293 * @param view The list view that changed state 1294 * @param scrollState The new state it changed to 1295 */ 1296 public void doScrollStateChange(AbsListView view, int scrollState) { 1297 mView = view; 1298 mNewState = scrollState; 1299 removeCallbacks(this); 1300 postDelayed(this, SCROLL_CHANGE_DELAY); 1301 } 1302 1303 public void run() { 1304 mCurrentScrollState = mNewState; 1305 // Fix the position after a scroll or a fling ends 1306 if (mNewState == OnScrollListener.SCROLL_STATE_IDLE 1307 && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { 1308 View child = mView.getChildAt(0); 1309 if (child == null) { 1310 // The view is no longer visible, just return 1311 return; 1312 } 1313 int dist = child.getBottom() - mListScrollTopOffset; 1314 if (dist > mListScrollTopOffset) { 1315 if (mIsScrollingUp) { 1316 mView.smoothScrollBy(dist - child.getHeight(), ADJUSTMENT_SCROLL_DURATION); 1317 } else { 1318 mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION); 1319 } 1320 } 1321 } 1322 mPreviousScrollState = mNewState; 1323 } 1324 } 1325 1326 /** 1327 * <p> 1328 * This is a specialized adapter for creating a list of weeks with 1329 * selectable days. It can be configured to display the week number, start 1330 * the week on a given day, show a reduced number of days, or display an 1331 * arbitrary number of weeks at a time. 1332 * </p> 1333 */ 1334 private class WeeksAdapter extends BaseAdapter implements OnTouchListener { 1335 private final Calendar mSelectedDate = Calendar.getInstance(); 1336 private final GestureDetector mGestureDetector; 1337 1338 private int mSelectedWeek; 1339 1340 private int mFocusedMonth; 1341 1342 private int mTotalWeekCount; 1343 1344 public WeeksAdapter() { 1345 mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); 1346 init(); 1347 } 1348 1349 /** 1350 * Set up the gesture detector and selected time 1351 */ 1352 private void init() { 1353 mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); 1354 mTotalWeekCount = getWeeksSinceMinDate(mMaxDate); 1355 if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek 1356 || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) { 1357 mTotalWeekCount++; 1358 } 1359 notifyDataSetChanged(); 1360 } 1361 1362 /** 1363 * Updates the selected day and related parameters. 1364 * 1365 * @param selectedDay The time to highlight 1366 */ 1367 public void setSelectedDay(Calendar selectedDay) { 1368 if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR) 1369 && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) { 1370 return; 1371 } 1372 mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis()); 1373 mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); 1374 mFocusedMonth = mSelectedDate.get(Calendar.MONTH); 1375 notifyDataSetChanged(); 1376 } 1377 1378 /** 1379 * @return The selected day of month. 1380 */ 1381 public Calendar getSelectedDay() { 1382 return mSelectedDate; 1383 } 1384 1385 @Override 1386 public int getCount() { 1387 return mTotalWeekCount; 1388 } 1389 1390 @Override 1391 public Object getItem(int position) { 1392 return null; 1393 } 1394 1395 @Override 1396 public long getItemId(int position) { 1397 return position; 1398 } 1399 1400 @Override 1401 public View getView(int position, View convertView, ViewGroup parent) { 1402 WeekView weekView = null; 1403 if (convertView != null) { 1404 weekView = (WeekView) convertView; 1405 } else { 1406 weekView = new WeekView(mContext); 1407 android.widget.AbsListView.LayoutParams params = 1408 new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT, 1409 LayoutParams.WRAP_CONTENT); 1410 weekView.setLayoutParams(params); 1411 weekView.setClickable(true); 1412 weekView.setOnTouchListener(this); 1413 } 1414 1415 int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get( 1416 Calendar.DAY_OF_WEEK) : -1; 1417 weekView.init(position, selectedWeekDay, mFocusedMonth); 1418 1419 return weekView; 1420 } 1421 1422 /** 1423 * Changes which month is in focus and updates the view. 1424 * 1425 * @param month The month to show as in focus [0-11] 1426 */ 1427 public void setFocusMonth(int month) { 1428 if (mFocusedMonth == month) { 1429 return; 1430 } 1431 mFocusedMonth = month; 1432 notifyDataSetChanged(); 1433 } 1434 1435 @Override 1436 public boolean onTouch(View v, MotionEvent event) { 1437 if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) { 1438 WeekView weekView = (WeekView) v; 1439 // if we cannot find a day for the given location we are done 1440 if (!weekView.getDayFromLocation(event.getX(), mTempDate)) { 1441 return true; 1442 } 1443 // it is possible that the touched day is outside the valid range 1444 // we draw whole weeks but range end can fall not on the week end 1445 if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { 1446 return true; 1447 } 1448 onDateTapped(mTempDate); 1449 return true; 1450 } 1451 return false; 1452 } 1453 1454 /** 1455 * Maintains the same hour/min/sec but moves the day to the tapped day. 1456 * 1457 * @param day The day that was tapped 1458 */ 1459 private void onDateTapped(Calendar day) { 1460 setSelectedDay(day); 1461 setMonthDisplayed(day); 1462 } 1463 1464 /** 1465 * This is here so we can identify single tap events and set the 1466 * selected day correctly 1467 */ 1468 class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { 1469 @Override 1470 public boolean onSingleTapUp(MotionEvent e) { 1471 return true; 1472 } 1473 } 1474 } 1475 1476 /** 1477 * <p> 1478 * This is a dynamic view for drawing a single week. It can be configured to 1479 * display the week number, start the week on a given day, or show a reduced 1480 * number of days. It is intended for use as a single view within a 1481 * ListView. See {@link WeeksAdapter} for usage. 1482 * </p> 1483 */ 1484 private class WeekView extends View { 1485 1486 private final Rect mTempRect = new Rect(); 1487 1488 private final Paint mDrawPaint = new Paint(); 1489 1490 private final Paint mMonthNumDrawPaint = new Paint(); 1491 1492 // Cache the number strings so we don't have to recompute them each time 1493 private String[] mDayNumbers; 1494 1495 // Quick lookup for checking which days are in the focus month 1496 private boolean[] mFocusDay; 1497 1498 // Whether this view has a focused day. 1499 private boolean mHasFocusedDay; 1500 1501 // Whether this view has only focused days. 1502 private boolean mHasUnfocusedDay; 1503 1504 // The first day displayed by this item 1505 private Calendar mFirstDay; 1506 1507 // The month of the first day in this week 1508 private int mMonthOfFirstWeekDay = -1; 1509 1510 // The month of the last day in this week 1511 private int mLastWeekDayMonth = -1; 1512 1513 // The position of this week, equivalent to weeks since the week of Jan 1514 // 1st, 1900 1515 private int mWeek = -1; 1516 1517 // Quick reference to the width of this view, matches parent 1518 private int mWidth; 1519 1520 // The height this view should draw at in pixels, set by height param 1521 private int mHeight; 1522 1523 // If this view contains the selected day 1524 private boolean mHasSelectedDay = false; 1525 1526 // Which day is selected [0-6] or -1 if no day is selected 1527 private int mSelectedDay = -1; 1528 1529 // The number of days + a spot for week number if it is displayed 1530 private int mNumCells; 1531 1532 // The left edge of the selected day 1533 private int mSelectedLeft = -1; 1534 1535 // The right edge of the selected day 1536 private int mSelectedRight = -1; 1537 1538 public WeekView(Context context) { 1539 super(context); 1540 1541 // Sets up any standard paints that will be used 1542 initilaizePaints(); 1543 } 1544 1545 /** 1546 * Initializes this week view. 1547 * 1548 * @param weekNumber The number of the week this view represents. The 1549 * week number is a zero based index of the weeks since 1550 * {@link CalendarView#getMinDate()}. 1551 * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no 1552 * selected day. 1553 * @param focusedMonth The month that is currently in focus i.e. 1554 * highlighted. 1555 */ 1556 public void init(int weekNumber, int selectedWeekDay, int focusedMonth) { 1557 mSelectedDay = selectedWeekDay; 1558 mHasSelectedDay = mSelectedDay != -1; 1559 mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek; 1560 mWeek = weekNumber; 1561 mTempDate.setTimeInMillis(mMinDate.getTimeInMillis()); 1562 1563 mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek); 1564 mTempDate.setFirstDayOfWeek(mFirstDayOfWeek); 1565 1566 // Allocate space for caching the day numbers and focus values 1567 mDayNumbers = new String[mNumCells]; 1568 mFocusDay = new boolean[mNumCells]; 1569 1570 // If we're showing the week number calculate it based on Monday 1571 int i = 0; 1572 if (mShowWeekNumber) { 1573 mDayNumbers[0] = String.format(Locale.getDefault(), "%d", 1574 mTempDate.get(Calendar.WEEK_OF_YEAR)); 1575 i++; 1576 } 1577 1578 // Now adjust our starting day based on the start day of the week 1579 int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK); 1580 mTempDate.add(Calendar.DAY_OF_MONTH, diff); 1581 1582 mFirstDay = (Calendar) mTempDate.clone(); 1583 mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH); 1584 1585 mHasUnfocusedDay = true; 1586 for (; i < mNumCells; i++) { 1587 final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth); 1588 mFocusDay[i] = isFocusedDay; 1589 mHasFocusedDay |= isFocusedDay; 1590 mHasUnfocusedDay &= !isFocusedDay; 1591 // do not draw dates outside the valid range to avoid user confusion 1592 if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { 1593 mDayNumbers[i] = ""; 1594 } else { 1595 mDayNumbers[i] = String.format(Locale.getDefault(), "%d", 1596 mTempDate.get(Calendar.DAY_OF_MONTH)); 1597 } 1598 mTempDate.add(Calendar.DAY_OF_MONTH, 1); 1599 } 1600 // We do one extra add at the end of the loop, if that pushed us to 1601 // new month undo it 1602 if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) { 1603 mTempDate.add(Calendar.DAY_OF_MONTH, -1); 1604 } 1605 mLastWeekDayMonth = mTempDate.get(Calendar.MONTH); 1606 1607 updateSelectionPositions(); 1608 } 1609 1610 /** 1611 * Initialize the paint instances. 1612 */ 1613 private void initilaizePaints() { 1614 mDrawPaint.setFakeBoldText(false); 1615 mDrawPaint.setAntiAlias(true); 1616 mDrawPaint.setStyle(Style.FILL); 1617 1618 mMonthNumDrawPaint.setFakeBoldText(true); 1619 mMonthNumDrawPaint.setAntiAlias(true); 1620 mMonthNumDrawPaint.setStyle(Style.FILL); 1621 mMonthNumDrawPaint.setTextAlign(Align.CENTER); 1622 mMonthNumDrawPaint.setTextSize(mDateTextSize); 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 final boolean isLayoutRtl = isLayoutRtl(); 1661 1662 int start; 1663 int end; 1664 1665 if (isLayoutRtl) { 1666 start = 0; 1667 end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; 1668 } else { 1669 start = mShowWeekNumber ? mWidth / mNumCells : 0; 1670 end = mWidth; 1671 } 1672 1673 if (x < start || x > end) { 1674 outCalendar.clear(); 1675 return false; 1676 } 1677 1678 // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels 1679 int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start)); 1680 1681 if (isLayoutRtl) { 1682 dayPosition = mDaysPerWeek - 1 - dayPosition; 1683 } 1684 1685 outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis()); 1686 outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition); 1687 1688 return true; 1689 } 1690 1691 @Override 1692 protected void onDraw(Canvas canvas) { 1693 drawBackground(canvas); 1694 drawWeekNumbersAndDates(canvas); 1695 drawWeekSeparators(canvas); 1696 drawSelectedDateVerticalBars(canvas); 1697 } 1698 1699 /** 1700 * This draws the selection highlight if a day is selected in this week. 1701 * 1702 * @param canvas The canvas to draw on 1703 */ 1704 private void drawBackground(Canvas canvas) { 1705 if (!mHasSelectedDay) { 1706 return; 1707 } 1708 mDrawPaint.setColor(mSelectedWeekBackgroundColor); 1709 1710 mTempRect.top = mWeekSeperatorLineWidth; 1711 mTempRect.bottom = mHeight; 1712 1713 final boolean isLayoutRtl = isLayoutRtl(); 1714 1715 if (isLayoutRtl) { 1716 mTempRect.left = 0; 1717 mTempRect.right = mSelectedLeft - 2; 1718 } else { 1719 mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0; 1720 mTempRect.right = mSelectedLeft - 2; 1721 } 1722 canvas.drawRect(mTempRect, mDrawPaint); 1723 1724 if (isLayoutRtl) { 1725 mTempRect.left = mSelectedRight + 3; 1726 mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; 1727 } else { 1728 mTempRect.left = mSelectedRight + 3; 1729 mTempRect.right = mWidth; 1730 } 1731 canvas.drawRect(mTempRect, mDrawPaint); 1732 } 1733 1734 /** 1735 * Draws the week and month day numbers for this week. 1736 * 1737 * @param canvas The canvas to draw on 1738 */ 1739 private void drawWeekNumbersAndDates(Canvas canvas) { 1740 final float textHeight = mDrawPaint.getTextSize(); 1741 final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth; 1742 final int nDays = mNumCells; 1743 final int divisor = 2 * nDays; 1744 1745 mDrawPaint.setTextAlign(Align.CENTER); 1746 mDrawPaint.setTextSize(mDateTextSize); 1747 1748 int i = 0; 1749 1750 if (isLayoutRtl()) { 1751 for (; i < nDays - 1; i++) { 1752 mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor 1753 : mUnfocusedMonthDateColor); 1754 int x = (2 * i + 1) * mWidth / divisor; 1755 canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint); 1756 } 1757 if (mShowWeekNumber) { 1758 mDrawPaint.setColor(mWeekNumberColor); 1759 int x = mWidth - mWidth / divisor; 1760 canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); 1761 } 1762 } else { 1763 if (mShowWeekNumber) { 1764 mDrawPaint.setColor(mWeekNumberColor); 1765 int x = mWidth / divisor; 1766 canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); 1767 i++; 1768 } 1769 for (; i < nDays; i++) { 1770 mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor 1771 : mUnfocusedMonthDateColor); 1772 int x = (2 * i + 1) * mWidth / divisor; 1773 canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint); 1774 } 1775 } 1776 } 1777 1778 /** 1779 * Draws a horizontal line for separating the weeks. 1780 * 1781 * @param canvas The canvas to draw on. 1782 */ 1783 private void drawWeekSeparators(Canvas canvas) { 1784 // If it is the topmost fully visible child do not draw separator line 1785 int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); 1786 if (mListView.getChildAt(0).getTop() < 0) { 1787 firstFullyVisiblePosition++; 1788 } 1789 if (firstFullyVisiblePosition == mWeek) { 1790 return; 1791 } 1792 mDrawPaint.setColor(mWeekSeparatorLineColor); 1793 mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth); 1794 float startX; 1795 float stopX; 1796 if (isLayoutRtl()) { 1797 startX = 0; 1798 stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; 1799 } else { 1800 startX = mShowWeekNumber ? mWidth / mNumCells : 0; 1801 stopX = mWidth; 1802 } 1803 canvas.drawLine(startX, 0, stopX, 0, mDrawPaint); 1804 } 1805 1806 /** 1807 * Draws the selected date bars if this week has a selected day. 1808 * 1809 * @param canvas The canvas to draw on 1810 */ 1811 private void drawSelectedDateVerticalBars(Canvas canvas) { 1812 if (!mHasSelectedDay) { 1813 return; 1814 } 1815 mSelectedDateVerticalBar.setBounds(mSelectedLeft - mSelectedDateVerticalBarWidth / 2, 1816 mWeekSeperatorLineWidth, 1817 mSelectedLeft + mSelectedDateVerticalBarWidth / 2, mHeight); 1818 mSelectedDateVerticalBar.draw(canvas); 1819 mSelectedDateVerticalBar.setBounds(mSelectedRight - mSelectedDateVerticalBarWidth / 2, 1820 mWeekSeperatorLineWidth, 1821 mSelectedRight + mSelectedDateVerticalBarWidth / 2, mHeight); 1822 mSelectedDateVerticalBar.draw(canvas); 1823 } 1824 1825 @Override 1826 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1827 mWidth = w; 1828 updateSelectionPositions(); 1829 } 1830 1831 /** 1832 * This calculates the positions for the selected day lines. 1833 */ 1834 private void updateSelectionPositions() { 1835 if (mHasSelectedDay) { 1836 final boolean isLayoutRtl = isLayoutRtl(); 1837 int selectedPosition = mSelectedDay - mFirstDayOfWeek; 1838 if (selectedPosition < 0) { 1839 selectedPosition += 7; 1840 } 1841 if (mShowWeekNumber && !isLayoutRtl) { 1842 selectedPosition++; 1843 } 1844 if (isLayoutRtl) { 1845 mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells; 1846 1847 } else { 1848 mSelectedLeft = selectedPosition * mWidth / mNumCells; 1849 } 1850 mSelectedRight = mSelectedLeft + mWidth / mNumCells; 1851 } 1852 } 1853 1854 @Override 1855 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1856 mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView 1857 .getPaddingBottom()) / mShownWeekCount; 1858 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight); 1859 } 1860 } 1861} 1862