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 17 package android.widget; 18 19 import android.annotation.Widget; 20 import android.app.Service; 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.content.res.TypedArray; 24 import android.database.DataSetObserver; 25 import android.graphics.Canvas; 26 import android.graphics.Paint; 27 import android.graphics.Paint.Align; 28 import android.graphics.Paint.Style; 29 import android.graphics.Rect; 30 import android.graphics.drawable.Drawable; 31 import android.text.TextUtils; 32 import android.text.format.DateUtils; 33 import android.util.AttributeSet; 34 import android.util.DisplayMetrics; 35 import android.util.Log; 36 import android.util.TypedValue; 37 import android.view.GestureDetector; 38 import android.view.LayoutInflater; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.view.accessibility.AccessibilityEvent; 43 import android.view.accessibility.AccessibilityNodeInfo; 44 import android.widget.AbsListView.OnScrollListener; 45 46 import com.android.internal.R; 47 48 import java.text.ParseException; 49 import java.text.SimpleDateFormat; 50 import java.util.Calendar; 51 import java.util.Locale; 52 import java.util.TimeZone; 53 54 import 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 76 public 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 if (child != null) { 1214 // Find out which month we're moving into 1215 int month; 1216 if (mIsScrollingUp) { 1217 month = child.getMonthOfFirstWeekDay(); 1218 } else { 1219 month = child.getMonthOfLastWeekDay(); 1220 } 1221 1222 // And how it relates to our current highlighted month 1223 int monthDiff; 1224 if (mCurrentMonthDisplayed == 11 && month == 0) { 1225 monthDiff = 1; 1226 } else if (mCurrentMonthDisplayed == 0 && month == 11) { 1227 monthDiff = -1; 1228 } else { 1229 monthDiff = month - mCurrentMonthDisplayed; 1230 } 1231 1232 // Only switch months if we're scrolling away from the currently 1233 // selected month 1234 if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) { 1235 Calendar firstDay = child.getFirstDay(); 1236 if (mIsScrollingUp) { 1237 firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK); 1238 } else { 1239 firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK); 1240 } 1241 setMonthDisplayed(firstDay); 1242 } 1243 } 1244 1245 mPreviousScrollPosition = currScroll; 1246 mPreviousScrollState = mCurrentScrollState; 1247 } 1248 1249 /** 1250 * Sets the month displayed at the top of this view based on time. Override 1251 * to add custom events when the title is changed. 1252 * 1253 * @param calendar A day in the new focus month. 1254 */ 1255 private void setMonthDisplayed(Calendar calendar) { 1256 mCurrentMonthDisplayed = calendar.get(Calendar.MONTH); 1257 mAdapter.setFocusMonth(mCurrentMonthDisplayed); 1258 final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY 1259 | DateUtils.FORMAT_SHOW_YEAR; 1260 final long millis = calendar.getTimeInMillis(); 1261 String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags); 1262 mMonthName.setText(newMonthName); 1263 mMonthName.invalidate(); 1264 } 1265 1266 /** 1267 * @return Returns the number of weeks between the current <code>date</code> 1268 * and the <code>mMinDate</code>. 1269 */ 1270 private int getWeeksSinceMinDate(Calendar date) { 1271 if (date.before(mMinDate)) { 1272 throw new IllegalArgumentException("fromDate: " + mMinDate.getTime() 1273 + " does not precede toDate: " + date.getTime()); 1274 } 1275 long endTimeMillis = date.getTimeInMillis() 1276 + date.getTimeZone().getOffset(date.getTimeInMillis()); 1277 long startTimeMillis = mMinDate.getTimeInMillis() 1278 + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis()); 1279 long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek) 1280 * MILLIS_IN_DAY; 1281 return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK); 1282 } 1283 1284 /** 1285 * Command responsible for acting upon scroll state changes. 1286 */ 1287 private class ScrollStateRunnable implements Runnable { 1288 private AbsListView mView; 1289 1290 private int mNewState; 1291 1292 /** 1293 * Sets up the runnable with a short delay in case the scroll state 1294 * immediately changes again. 1295 * 1296 * @param view The list view that changed state 1297 * @param scrollState The new state it changed to 1298 */ 1299 public void doScrollStateChange(AbsListView view, int scrollState) { 1300 mView = view; 1301 mNewState = scrollState; 1302 removeCallbacks(this); 1303 postDelayed(this, SCROLL_CHANGE_DELAY); 1304 } 1305 1306 public void run() { 1307 mCurrentScrollState = mNewState; 1308 // Fix the position after a scroll or a fling ends 1309 if (mNewState == OnScrollListener.SCROLL_STATE_IDLE 1310 && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) { 1311 View child = mView.getChildAt(0); 1312 if (child == null) { 1313 // The view is no longer visible, just return 1314 return; 1315 } 1316 int dist = child.getBottom() - mListScrollTopOffset; 1317 if (dist > mListScrollTopOffset) { 1318 if (mIsScrollingUp) { 1319 mView.smoothScrollBy(dist - child.getHeight(), ADJUSTMENT_SCROLL_DURATION); 1320 } else { 1321 mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION); 1322 } 1323 } 1324 } 1325 mPreviousScrollState = mNewState; 1326 } 1327 } 1328 1329 /** 1330 * <p> 1331 * This is a specialized adapter for creating a list of weeks with 1332 * selectable days. It can be configured to display the week number, start 1333 * the week on a given day, show a reduced number of days, or display an 1334 * arbitrary number of weeks at a time. 1335 * </p> 1336 */ 1337 private class WeeksAdapter extends BaseAdapter implements OnTouchListener { 1338 private final Calendar mSelectedDate = Calendar.getInstance(); 1339 private final GestureDetector mGestureDetector; 1340 1341 private int mSelectedWeek; 1342 1343 private int mFocusedMonth; 1344 1345 private int mTotalWeekCount; 1346 1347 public WeeksAdapter() { 1348 mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); 1349 init(); 1350 } 1351 1352 /** 1353 * Set up the gesture detector and selected time 1354 */ 1355 private void init() { 1356 mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); 1357 mTotalWeekCount = getWeeksSinceMinDate(mMaxDate); 1358 if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek 1359 || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) { 1360 mTotalWeekCount++; 1361 } 1362 notifyDataSetChanged(); 1363 } 1364 1365 /** 1366 * Updates the selected day and related parameters. 1367 * 1368 * @param selectedDay The time to highlight 1369 */ 1370 public void setSelectedDay(Calendar selectedDay) { 1371 if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR) 1372 && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) { 1373 return; 1374 } 1375 mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis()); 1376 mSelectedWeek = getWeeksSinceMinDate(mSelectedDate); 1377 mFocusedMonth = mSelectedDate.get(Calendar.MONTH); 1378 notifyDataSetChanged(); 1379 } 1380 1381 /** 1382 * @return The selected day of month. 1383 */ 1384 public Calendar getSelectedDay() { 1385 return mSelectedDate; 1386 } 1387 1388 @Override 1389 public int getCount() { 1390 return mTotalWeekCount; 1391 } 1392 1393 @Override 1394 public Object getItem(int position) { 1395 return null; 1396 } 1397 1398 @Override 1399 public long getItemId(int position) { 1400 return position; 1401 } 1402 1403 @Override 1404 public View getView(int position, View convertView, ViewGroup parent) { 1405 WeekView weekView = null; 1406 if (convertView != null) { 1407 weekView = (WeekView) convertView; 1408 } else { 1409 weekView = new WeekView(mContext); 1410 android.widget.AbsListView.LayoutParams params = 1411 new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT, 1412 LayoutParams.WRAP_CONTENT); 1413 weekView.setLayoutParams(params); 1414 weekView.setClickable(true); 1415 weekView.setOnTouchListener(this); 1416 } 1417 1418 int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get( 1419 Calendar.DAY_OF_WEEK) : -1; 1420 weekView.init(position, selectedWeekDay, mFocusedMonth); 1421 1422 return weekView; 1423 } 1424 1425 /** 1426 * Changes which month is in focus and updates the view. 1427 * 1428 * @param month The month to show as in focus [0-11] 1429 */ 1430 public void setFocusMonth(int month) { 1431 if (mFocusedMonth == month) { 1432 return; 1433 } 1434 mFocusedMonth = month; 1435 notifyDataSetChanged(); 1436 } 1437 1438 @Override 1439 public boolean onTouch(View v, MotionEvent event) { 1440 if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) { 1441 WeekView weekView = (WeekView) v; 1442 // if we cannot find a day for the given location we are done 1443 if (!weekView.getDayFromLocation(event.getX(), mTempDate)) { 1444 return true; 1445 } 1446 // it is possible that the touched day is outside the valid range 1447 // we draw whole weeks but range end can fall not on the week end 1448 if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { 1449 return true; 1450 } 1451 onDateTapped(mTempDate); 1452 return true; 1453 } 1454 return false; 1455 } 1456 1457 /** 1458 * Maintains the same hour/min/sec but moves the day to the tapped day. 1459 * 1460 * @param day The day that was tapped 1461 */ 1462 private void onDateTapped(Calendar day) { 1463 setSelectedDay(day); 1464 setMonthDisplayed(day); 1465 } 1466 1467 /** 1468 * This is here so we can identify single tap events and set the 1469 * selected day correctly 1470 */ 1471 class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { 1472 @Override 1473 public boolean onSingleTapUp(MotionEvent e) { 1474 return true; 1475 } 1476 } 1477 } 1478 1479 /** 1480 * <p> 1481 * This is a dynamic view for drawing a single week. It can be configured to 1482 * display the week number, start the week on a given day, or show a reduced 1483 * number of days. It is intended for use as a single view within a 1484 * ListView. See {@link WeeksAdapter} for usage. 1485 * </p> 1486 */ 1487 private class WeekView extends View { 1488 1489 private final Rect mTempRect = new Rect(); 1490 1491 private final Paint mDrawPaint = new Paint(); 1492 1493 private final Paint mMonthNumDrawPaint = new Paint(); 1494 1495 // Cache the number strings so we don't have to recompute them each time 1496 private String[] mDayNumbers; 1497 1498 // Quick lookup for checking which days are in the focus month 1499 private boolean[] mFocusDay; 1500 1501 // Whether this view has a focused day. 1502 private boolean mHasFocusedDay; 1503 1504 // Whether this view has only focused days. 1505 private boolean mHasUnfocusedDay; 1506 1507 // The first day displayed by this item 1508 private Calendar mFirstDay; 1509 1510 // The month of the first day in this week 1511 private int mMonthOfFirstWeekDay = -1; 1512 1513 // The month of the last day in this week 1514 private int mLastWeekDayMonth = -1; 1515 1516 // The position of this week, equivalent to weeks since the week of Jan 1517 // 1st, 1900 1518 private int mWeek = -1; 1519 1520 // Quick reference to the width of this view, matches parent 1521 private int mWidth; 1522 1523 // The height this view should draw at in pixels, set by height param 1524 private int mHeight; 1525 1526 // If this view contains the selected day 1527 private boolean mHasSelectedDay = false; 1528 1529 // Which day is selected [0-6] or -1 if no day is selected 1530 private int mSelectedDay = -1; 1531 1532 // The number of days + a spot for week number if it is displayed 1533 private int mNumCells; 1534 1535 // The left edge of the selected day 1536 private int mSelectedLeft = -1; 1537 1538 // The right edge of the selected day 1539 private int mSelectedRight = -1; 1540 1541 public WeekView(Context context) { 1542 super(context); 1543 1544 // Sets up any standard paints that will be used 1545 initilaizePaints(); 1546 } 1547 1548 /** 1549 * Initializes this week view. 1550 * 1551 * @param weekNumber The number of the week this view represents. The 1552 * week number is a zero based index of the weeks since 1553 * {@link CalendarView#getMinDate()}. 1554 * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no 1555 * selected day. 1556 * @param focusedMonth The month that is currently in focus i.e. 1557 * highlighted. 1558 */ 1559 public void init(int weekNumber, int selectedWeekDay, int focusedMonth) { 1560 mSelectedDay = selectedWeekDay; 1561 mHasSelectedDay = mSelectedDay != -1; 1562 mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek; 1563 mWeek = weekNumber; 1564 mTempDate.setTimeInMillis(mMinDate.getTimeInMillis()); 1565 1566 mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek); 1567 mTempDate.setFirstDayOfWeek(mFirstDayOfWeek); 1568 1569 // Allocate space for caching the day numbers and focus values 1570 mDayNumbers = new String[mNumCells]; 1571 mFocusDay = new boolean[mNumCells]; 1572 1573 // If we're showing the week number calculate it based on Monday 1574 int i = 0; 1575 if (mShowWeekNumber) { 1576 mDayNumbers[0] = String.format(Locale.getDefault(), "%d", 1577 mTempDate.get(Calendar.WEEK_OF_YEAR)); 1578 i++; 1579 } 1580 1581 // Now adjust our starting day based on the start day of the week 1582 int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK); 1583 mTempDate.add(Calendar.DAY_OF_MONTH, diff); 1584 1585 mFirstDay = (Calendar) mTempDate.clone(); 1586 mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH); 1587 1588 mHasUnfocusedDay = true; 1589 for (; i < mNumCells; i++) { 1590 final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth); 1591 mFocusDay[i] = isFocusedDay; 1592 mHasFocusedDay |= isFocusedDay; 1593 mHasUnfocusedDay &= !isFocusedDay; 1594 // do not draw dates outside the valid range to avoid user confusion 1595 if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) { 1596 mDayNumbers[i] = ""; 1597 } else { 1598 mDayNumbers[i] = String.format(Locale.getDefault(), "%d", 1599 mTempDate.get(Calendar.DAY_OF_MONTH)); 1600 } 1601 mTempDate.add(Calendar.DAY_OF_MONTH, 1); 1602 } 1603 // We do one extra add at the end of the loop, if that pushed us to 1604 // new month undo it 1605 if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) { 1606 mTempDate.add(Calendar.DAY_OF_MONTH, -1); 1607 } 1608 mLastWeekDayMonth = mTempDate.get(Calendar.MONTH); 1609 1610 updateSelectionPositions(); 1611 } 1612 1613 /** 1614 * Initialize the paint instances. 1615 */ 1616 private void initilaizePaints() { 1617 mDrawPaint.setFakeBoldText(false); 1618 mDrawPaint.setAntiAlias(true); 1619 mDrawPaint.setStyle(Style.FILL); 1620 1621 mMonthNumDrawPaint.setFakeBoldText(true); 1622 mMonthNumDrawPaint.setAntiAlias(true); 1623 mMonthNumDrawPaint.setStyle(Style.FILL); 1624 mMonthNumDrawPaint.setTextAlign(Align.CENTER); 1625 mMonthNumDrawPaint.setTextSize(mDateTextSize); 1626 } 1627 1628 /** 1629 * Returns the month of the first day in this week. 1630 * 1631 * @return The month the first day of this view is in. 1632 */ 1633 public int getMonthOfFirstWeekDay() { 1634 return mMonthOfFirstWeekDay; 1635 } 1636 1637 /** 1638 * Returns the month of the last day in this week 1639 * 1640 * @return The month the last day of this view is in 1641 */ 1642 public int getMonthOfLastWeekDay() { 1643 return mLastWeekDayMonth; 1644 } 1645 1646 /** 1647 * Returns the first day in this view. 1648 * 1649 * @return The first day in the view. 1650 */ 1651 public Calendar getFirstDay() { 1652 return mFirstDay; 1653 } 1654 1655 /** 1656 * Calculates the day that the given x position is in, accounting for 1657 * week number. 1658 * 1659 * @param x The x position of the touch event. 1660 * @return True if a day was found for the given location. 1661 */ 1662 public boolean getDayFromLocation(float x, Calendar outCalendar) { 1663 final boolean isLayoutRtl = isLayoutRtl(); 1664 1665 int start; 1666 int end; 1667 1668 if (isLayoutRtl) { 1669 start = 0; 1670 end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; 1671 } else { 1672 start = mShowWeekNumber ? mWidth / mNumCells : 0; 1673 end = mWidth; 1674 } 1675 1676 if (x < start || x > end) { 1677 outCalendar.clear(); 1678 return false; 1679 } 1680 1681 // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels 1682 int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start)); 1683 1684 if (isLayoutRtl) { 1685 dayPosition = mDaysPerWeek - 1 - dayPosition; 1686 } 1687 1688 outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis()); 1689 outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition); 1690 1691 return true; 1692 } 1693 1694 @Override 1695 protected void onDraw(Canvas canvas) { 1696 drawBackground(canvas); 1697 drawWeekNumbersAndDates(canvas); 1698 drawWeekSeparators(canvas); 1699 drawSelectedDateVerticalBars(canvas); 1700 } 1701 1702 /** 1703 * This draws the selection highlight if a day is selected in this week. 1704 * 1705 * @param canvas The canvas to draw on 1706 */ 1707 private void drawBackground(Canvas canvas) { 1708 if (!mHasSelectedDay) { 1709 return; 1710 } 1711 mDrawPaint.setColor(mSelectedWeekBackgroundColor); 1712 1713 mTempRect.top = mWeekSeperatorLineWidth; 1714 mTempRect.bottom = mHeight; 1715 1716 final boolean isLayoutRtl = isLayoutRtl(); 1717 1718 if (isLayoutRtl) { 1719 mTempRect.left = 0; 1720 mTempRect.right = mSelectedLeft - 2; 1721 } else { 1722 mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0; 1723 mTempRect.right = mSelectedLeft - 2; 1724 } 1725 canvas.drawRect(mTempRect, mDrawPaint); 1726 1727 if (isLayoutRtl) { 1728 mTempRect.left = mSelectedRight + 3; 1729 mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; 1730 } else { 1731 mTempRect.left = mSelectedRight + 3; 1732 mTempRect.right = mWidth; 1733 } 1734 canvas.drawRect(mTempRect, mDrawPaint); 1735 } 1736 1737 /** 1738 * Draws the week and month day numbers for this week. 1739 * 1740 * @param canvas The canvas to draw on 1741 */ 1742 private void drawWeekNumbersAndDates(Canvas canvas) { 1743 final float textHeight = mDrawPaint.getTextSize(); 1744 final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth; 1745 final int nDays = mNumCells; 1746 final int divisor = 2 * nDays; 1747 1748 mDrawPaint.setTextAlign(Align.CENTER); 1749 mDrawPaint.setTextSize(mDateTextSize); 1750 1751 int i = 0; 1752 1753 if (isLayoutRtl()) { 1754 for (; i < nDays - 1; i++) { 1755 mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor 1756 : mUnfocusedMonthDateColor); 1757 int x = (2 * i + 1) * mWidth / divisor; 1758 canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint); 1759 } 1760 if (mShowWeekNumber) { 1761 mDrawPaint.setColor(mWeekNumberColor); 1762 int x = mWidth - mWidth / divisor; 1763 canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); 1764 } 1765 } else { 1766 if (mShowWeekNumber) { 1767 mDrawPaint.setColor(mWeekNumberColor); 1768 int x = mWidth / divisor; 1769 canvas.drawText(mDayNumbers[0], x, y, mDrawPaint); 1770 i++; 1771 } 1772 for (; i < nDays; i++) { 1773 mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor 1774 : mUnfocusedMonthDateColor); 1775 int x = (2 * i + 1) * mWidth / divisor; 1776 canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint); 1777 } 1778 } 1779 } 1780 1781 /** 1782 * Draws a horizontal line for separating the weeks. 1783 * 1784 * @param canvas The canvas to draw on. 1785 */ 1786 private void drawWeekSeparators(Canvas canvas) { 1787 // If it is the topmost fully visible child do not draw separator line 1788 int firstFullyVisiblePosition = mListView.getFirstVisiblePosition(); 1789 if (mListView.getChildAt(0).getTop() < 0) { 1790 firstFullyVisiblePosition++; 1791 } 1792 if (firstFullyVisiblePosition == mWeek) { 1793 return; 1794 } 1795 mDrawPaint.setColor(mWeekSeparatorLineColor); 1796 mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth); 1797 float startX; 1798 float stopX; 1799 if (isLayoutRtl()) { 1800 startX = 0; 1801 stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth; 1802 } else { 1803 startX = mShowWeekNumber ? mWidth / mNumCells : 0; 1804 stopX = mWidth; 1805 } 1806 canvas.drawLine(startX, 0, stopX, 0, mDrawPaint); 1807 } 1808 1809 /** 1810 * Draws the selected date bars if this week has a selected day. 1811 * 1812 * @param canvas The canvas to draw on 1813 */ 1814 private void drawSelectedDateVerticalBars(Canvas canvas) { 1815 if (!mHasSelectedDay) { 1816 return; 1817 } 1818 mSelectedDateVerticalBar.setBounds(mSelectedLeft - mSelectedDateVerticalBarWidth / 2, 1819 mWeekSeperatorLineWidth, 1820 mSelectedLeft + mSelectedDateVerticalBarWidth / 2, mHeight); 1821 mSelectedDateVerticalBar.draw(canvas); 1822 mSelectedDateVerticalBar.setBounds(mSelectedRight - mSelectedDateVerticalBarWidth / 2, 1823 mWeekSeperatorLineWidth, 1824 mSelectedRight + mSelectedDateVerticalBarWidth / 2, mHeight); 1825 mSelectedDateVerticalBar.draw(canvas); 1826 } 1827 1828 @Override 1829 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1830 mWidth = w; 1831 updateSelectionPositions(); 1832 } 1833 1834 /** 1835 * This calculates the positions for the selected day lines. 1836 */ 1837 private void updateSelectionPositions() { 1838 if (mHasSelectedDay) { 1839 final boolean isLayoutRtl = isLayoutRtl(); 1840 int selectedPosition = mSelectedDay - mFirstDayOfWeek; 1841 if (selectedPosition < 0) { 1842 selectedPosition += 7; 1843 } 1844 if (mShowWeekNumber && !isLayoutRtl) { 1845 selectedPosition++; 1846 } 1847 if (isLayoutRtl) { 1848 mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells; 1849 1850 } else { 1851 mSelectedLeft = selectedPosition * mWidth / mNumCells; 1852 } 1853 mSelectedRight = mSelectedLeft + mWidth / mNumCells; 1854 } 1855 } 1856 1857 @Override 1858 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1859 mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView 1860 .getPaddingBottom()) / mShownWeekCount; 1861 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight); 1862 } 1863 } 1864 } 1865