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