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