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