SimpleMonthView.java revision ffb46bf2956d89e3190007ccf2ef3ce3eed005fe
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19import android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.Configuration; 22import android.content.res.Resources; 23import android.graphics.Canvas; 24import android.graphics.Paint; 25import android.graphics.Paint.Align; 26import android.graphics.Paint.Style; 27import android.graphics.Rect; 28import android.graphics.Typeface; 29import android.os.Bundle; 30import android.text.format.DateFormat; 31import android.text.format.DateUtils; 32import android.text.format.Time; 33import android.util.AttributeSet; 34import android.util.IntArray; 35import android.util.MathUtils; 36import android.view.MotionEvent; 37import android.view.View; 38import android.view.accessibility.AccessibilityEvent; 39import android.view.accessibility.AccessibilityNodeInfo; 40 41import com.android.internal.R; 42import com.android.internal.widget.ExploreByTouchHelper; 43 44import java.text.SimpleDateFormat; 45import java.util.Calendar; 46import java.util.Formatter; 47import java.util.List; 48import java.util.Locale; 49 50/** 51 * A calendar-like view displaying a specified month and the appropriate selectable day numbers 52 * within the specified month. 53 */ 54class SimpleMonthView extends View { 55 private static final String TAG = "SimpleMonthView"; 56 57 private static final int DEFAULT_HEIGHT = 32; 58 private static final int MIN_HEIGHT = 10; 59 60 private static final int DEFAULT_SELECTED_DAY = -1; 61 private static final int DEFAULT_WEEK_START = Calendar.SUNDAY; 62 private static final int DEFAULT_NUM_DAYS = 7; 63 private static final int DEFAULT_NUM_ROWS = 6; 64 private static final int MAX_NUM_ROWS = 6; 65 66 private static final int SELECTED_CIRCLE_ALPHA = 60; 67 68 private static final int DAY_SEPARATOR_WIDTH = 1; 69 70 private final int mMiniDayNumberTextSize; 71 private final int mMonthLabelTextSize; 72 private final int mMonthDayLabelTextSize; 73 private final int mMonthHeaderSize; 74 private final int mDaySelectedCircleSize; 75 76 // used for scaling to the device density 77 private static float mScale = 0; 78 79 /** Single-letter (when available) formatter for the day of week label. */ 80 private SimpleDateFormat mDayFormatter = new SimpleDateFormat("EEEEE", Locale.getDefault()); 81 82 // affects the padding on the sides of this view 83 private int mPadding = 0; 84 85 private String mDayOfWeekTypeface; 86 private String mMonthTitleTypeface; 87 88 private Paint mDayNumberPaint; 89 private Paint mDayNumberDisabledPaint; 90 private Paint mDayNumberSelectedPaint; 91 92 private Paint mMonthTitlePaint; 93 private Paint mMonthDayLabelPaint; 94 95 private final Formatter mFormatter; 96 private final StringBuilder mStringBuilder; 97 98 private int mMonth; 99 private int mYear; 100 101 // Quick reference to the width of this view, matches parent 102 private int mWidth; 103 104 // The height this view should draw at in pixels, set by height param 105 private int mRowHeight = DEFAULT_HEIGHT; 106 107 // If this view contains the today 108 private boolean mHasToday = false; 109 110 // Which day is selected [0-6] or -1 if no day is selected 111 private int mSelectedDay = -1; 112 113 // Which day is today [0-6] or -1 if no day is today 114 private int mToday = DEFAULT_SELECTED_DAY; 115 116 // Which day of the week to start on [0-6] 117 private int mWeekStart = DEFAULT_WEEK_START; 118 119 // How many days to display 120 private int mNumDays = DEFAULT_NUM_DAYS; 121 122 // The number of days + a spot for week number if it is displayed 123 private int mNumCells = mNumDays; 124 125 private int mDayOfWeekStart = 0; 126 127 // First enabled day 128 private int mEnabledDayStart = 1; 129 130 // Last enabled day 131 private int mEnabledDayEnd = 31; 132 133 private final Calendar mCalendar = Calendar.getInstance(); 134 private final Calendar mDayLabelCalendar = Calendar.getInstance(); 135 136 private final MonthViewTouchHelper mTouchHelper; 137 138 private int mNumRows = DEFAULT_NUM_ROWS; 139 140 // Optional listener for handling day click actions 141 private OnDayClickListener mOnDayClickListener; 142 143 // Whether to prevent setting the accessibility delegate 144 private boolean mLockAccessibilityDelegate; 145 146 private int mNormalTextColor; 147 private int mDisabledTextColor; 148 private int mSelectedDayColor; 149 150 public SimpleMonthView(Context context) { 151 this(context, null); 152 } 153 154 public SimpleMonthView(Context context, AttributeSet attrs) { 155 this(context, attrs, R.attr.datePickerStyle); 156 } 157 158 public SimpleMonthView(Context context, AttributeSet attrs, int defStyle) { 159 super(context, attrs); 160 161 final Resources res = context.getResources(); 162 163 mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface); 164 mMonthTitleTypeface = res.getString(R.string.sans_serif); 165 166 mStringBuilder = new StringBuilder(50); 167 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 168 169 mMiniDayNumberTextSize = res.getDimensionPixelSize(R.dimen.datepicker_day_number_size); 170 mMonthLabelTextSize = res.getDimensionPixelSize(R.dimen.datepicker_month_label_size); 171 mMonthDayLabelTextSize = res.getDimensionPixelSize( 172 R.dimen.datepicker_month_day_label_text_size); 173 mMonthHeaderSize = res.getDimensionPixelOffset( 174 R.dimen.datepicker_month_list_item_header_height); 175 mDaySelectedCircleSize = res.getDimensionPixelSize( 176 R.dimen.datepicker_day_number_select_circle_radius); 177 178 mRowHeight = (res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height) 179 - mMonthHeaderSize) / MAX_NUM_ROWS; 180 181 // Set up accessibility components. 182 mTouchHelper = new MonthViewTouchHelper(this); 183 setAccessibilityDelegate(mTouchHelper); 184 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 185 mLockAccessibilityDelegate = true; 186 187 // Sets up any standard paints that will be used 188 initView(); 189 } 190 191 @Override 192 protected void onConfigurationChanged(Configuration newConfig) { 193 super.onConfigurationChanged(newConfig); 194 195 mDayFormatter = new SimpleDateFormat("EEEEE", newConfig.locale); 196 } 197 198 void setTextColor(ColorStateList colors) { 199 final Resources res = getContext().getResources(); 200 201 mNormalTextColor = colors.getColorForState(ENABLED_STATE_SET, 202 res.getColor(R.color.datepicker_default_normal_text_color_holo_light)); 203 mMonthTitlePaint.setColor(mNormalTextColor); 204 mMonthDayLabelPaint.setColor(mNormalTextColor); 205 206 mDisabledTextColor = colors.getColorForState(EMPTY_STATE_SET, 207 res.getColor(R.color.datepicker_default_disabled_text_color_holo_light)); 208 mDayNumberDisabledPaint.setColor(mDisabledTextColor); 209 210 mSelectedDayColor = colors.getColorForState(ENABLED_SELECTED_STATE_SET, 211 res.getColor(R.color.holo_blue_light)); 212 mDayNumberSelectedPaint.setColor(mSelectedDayColor); 213 mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); 214 } 215 216 @Override 217 public void setAccessibilityDelegate(AccessibilityDelegate delegate) { 218 // Workaround for a JB MR1 issue where accessibility delegates on 219 // top-level ListView items are overwritten. 220 if (!mLockAccessibilityDelegate) { 221 super.setAccessibilityDelegate(delegate); 222 } 223 } 224 225 public void setOnDayClickListener(OnDayClickListener listener) { 226 mOnDayClickListener = listener; 227 } 228 229 @Override 230 public boolean dispatchHoverEvent(MotionEvent event) { 231 // First right-of-refusal goes the touch exploration helper. 232 if (mTouchHelper.dispatchHoverEvent(event)) { 233 return true; 234 } 235 return super.dispatchHoverEvent(event); 236 } 237 238 @Override 239 public boolean onTouchEvent(MotionEvent event) { 240 switch (event.getAction()) { 241 case MotionEvent.ACTION_UP: 242 final int day = getDayFromLocation(event.getX(), event.getY()); 243 if (day >= 0) { 244 onDayClick(day); 245 } 246 break; 247 } 248 return true; 249 } 250 251 /** 252 * Sets up the text and style properties for painting. 253 */ 254 private void initView() { 255 mMonthTitlePaint = new Paint(); 256 mMonthTitlePaint.setAntiAlias(true); 257 mMonthTitlePaint.setColor(mNormalTextColor); 258 mMonthTitlePaint.setTextSize(mMonthLabelTextSize); 259 mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD)); 260 mMonthTitlePaint.setTextAlign(Align.CENTER); 261 mMonthTitlePaint.setStyle(Style.FILL); 262 mMonthTitlePaint.setFakeBoldText(true); 263 264 mMonthDayLabelPaint = new Paint(); 265 mMonthDayLabelPaint.setAntiAlias(true); 266 mMonthDayLabelPaint.setColor(mNormalTextColor); 267 mMonthDayLabelPaint.setTextSize(mMonthDayLabelTextSize); 268 mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL)); 269 mMonthDayLabelPaint.setTextAlign(Align.CENTER); 270 mMonthDayLabelPaint.setStyle(Style.FILL); 271 mMonthDayLabelPaint.setFakeBoldText(true); 272 273 mDayNumberSelectedPaint = new Paint(); 274 mDayNumberSelectedPaint.setAntiAlias(true); 275 mDayNumberSelectedPaint.setColor(mSelectedDayColor); 276 mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); 277 mDayNumberSelectedPaint.setTextAlign(Align.CENTER); 278 mDayNumberSelectedPaint.setStyle(Style.FILL); 279 mDayNumberSelectedPaint.setFakeBoldText(true); 280 281 mDayNumberPaint = new Paint(); 282 mDayNumberPaint.setAntiAlias(true); 283 mDayNumberPaint.setTextSize(mMiniDayNumberTextSize); 284 mDayNumberPaint.setTextAlign(Align.CENTER); 285 mDayNumberPaint.setStyle(Style.FILL); 286 mDayNumberPaint.setFakeBoldText(false); 287 288 mDayNumberDisabledPaint = new Paint(); 289 mDayNumberDisabledPaint.setAntiAlias(true); 290 mDayNumberDisabledPaint.setColor(mDisabledTextColor); 291 mDayNumberDisabledPaint.setTextSize(mMiniDayNumberTextSize); 292 mDayNumberDisabledPaint.setTextAlign(Align.CENTER); 293 mDayNumberDisabledPaint.setStyle(Style.FILL); 294 mDayNumberDisabledPaint.setFakeBoldText(false); 295 } 296 297 @Override 298 protected void onDraw(Canvas canvas) { 299 drawMonthTitle(canvas); 300 drawWeekDayLabels(canvas); 301 drawDays(canvas); 302 } 303 304 private static boolean isValidDayOfWeek(int day) { 305 return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY; 306 } 307 308 private static boolean isValidMonth(int month) { 309 return month >= Calendar.JANUARY && month <= Calendar.DECEMBER; 310 } 311 312 /** 313 * Sets all the parameters for displaying this week. Parameters have a default value and 314 * will only update if a new value is included, except for focus month, which will always 315 * default to no focus month if no value is passed in. The only required parameter is the 316 * week start. 317 * 318 * @param selectedDay the selected day of the month, or -1 for no selection. 319 * @param month the month. 320 * @param year the year. 321 * @param weekStart which day the week should start on. {@link Calendar#SUNDAY} through 322 * {@link Calendar#SATURDAY}. 323 * @param enabledDayStart the first enabled day. 324 * @param enabledDayEnd the last enabled day. 325 */ 326 void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart, 327 int enabledDayEnd) { 328 if (mRowHeight < MIN_HEIGHT) { 329 mRowHeight = MIN_HEIGHT; 330 } 331 332 mSelectedDay = selectedDay; 333 334 if (isValidMonth(month)) { 335 mMonth = month; 336 } 337 mYear = year; 338 339 // Figure out what day today is 340 final Time today = new Time(Time.getCurrentTimezone()); 341 today.setToNow(); 342 mHasToday = false; 343 mToday = -1; 344 345 mCalendar.set(Calendar.MONTH, mMonth); 346 mCalendar.set(Calendar.YEAR, mYear); 347 mCalendar.set(Calendar.DAY_OF_MONTH, 1); 348 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); 349 350 if (isValidDayOfWeek(weekStart)) { 351 mWeekStart = weekStart; 352 } else { 353 mWeekStart = mCalendar.getFirstDayOfWeek(); 354 } 355 356 if (enabledDayStart > 0 && enabledDayEnd < 32) { 357 mEnabledDayStart = enabledDayStart; 358 } 359 if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) { 360 mEnabledDayEnd = enabledDayEnd; 361 } 362 363 mNumCells = getDaysInMonth(mMonth, mYear); 364 for (int i = 0; i < mNumCells; i++) { 365 final int day = i + 1; 366 if (sameDay(day, today)) { 367 mHasToday = true; 368 mToday = day; 369 } 370 } 371 mNumRows = calculateNumRows(); 372 373 // Invalidate cached accessibility information. 374 mTouchHelper.invalidateRoot(); 375 } 376 377 private static int getDaysInMonth(int month, int year) { 378 switch (month) { 379 case Calendar.JANUARY: 380 case Calendar.MARCH: 381 case Calendar.MAY: 382 case Calendar.JULY: 383 case Calendar.AUGUST: 384 case Calendar.OCTOBER: 385 case Calendar.DECEMBER: 386 return 31; 387 case Calendar.APRIL: 388 case Calendar.JUNE: 389 case Calendar.SEPTEMBER: 390 case Calendar.NOVEMBER: 391 return 30; 392 case Calendar.FEBRUARY: 393 return (year % 4 == 0) ? 29 : 28; 394 default: 395 throw new IllegalArgumentException("Invalid Month"); 396 } 397 } 398 399 public void reuse() { 400 mNumRows = DEFAULT_NUM_ROWS; 401 requestLayout(); 402 } 403 404 private int calculateNumRows() { 405 int offset = findDayOffset(); 406 int dividend = (offset + mNumCells) / mNumDays; 407 int remainder = (offset + mNumCells) % mNumDays; 408 return (dividend + (remainder > 0 ? 1 : 0)); 409 } 410 411 private boolean sameDay(int day, Time today) { 412 return mYear == today.year && 413 mMonth == today.month && 414 day == today.monthDay; 415 } 416 417 @Override 418 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 419 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows 420 + mMonthHeaderSize); 421 } 422 423 @Override 424 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 425 mWidth = w; 426 427 // Invalidate cached accessibility information. 428 mTouchHelper.invalidateRoot(); 429 } 430 431 private String getMonthAndYearString() { 432 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR 433 | DateUtils.FORMAT_NO_MONTH_DAY; 434 mStringBuilder.setLength(0); 435 long millis = mCalendar.getTimeInMillis(); 436 return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, 437 Time.getCurrentTimezone()).toString(); 438 } 439 440 private void drawMonthTitle(Canvas canvas) { 441 final float x = (mWidth + 2 * mPadding) / 2f; 442 final float y = (mMonthHeaderSize - mMonthDayLabelTextSize) / 2f; 443 canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); 444 } 445 446 private void drawWeekDayLabels(Canvas canvas) { 447 final int y = mMonthHeaderSize - (mMonthDayLabelTextSize / 2); 448 final int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 449 450 for (int i = 0; i < mNumDays; i++) { 451 final int calendarDay = (i + mWeekStart) % mNumDays; 452 mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); 453 454 final String dayLabel = mDayFormatter.format(mDayLabelCalendar.getTime()); 455 final int x = (2 * i + 1) * dayWidthHalf + mPadding; 456 canvas.drawText(dayLabel, x, y, mMonthDayLabelPaint); 457 } 458 } 459 460 /** 461 * Draws the month days. 462 */ 463 private void drawDays(Canvas canvas) { 464 int y = (((mRowHeight + mMiniDayNumberTextSize) / 2) - DAY_SEPARATOR_WIDTH) 465 + mMonthHeaderSize; 466 int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 467 int j = findDayOffset(); 468 for (int day = 1; day <= mNumCells; day++) { 469 int x = (2 * j + 1) * dayWidthHalf + mPadding; 470 if (mSelectedDay == day) { 471 canvas.drawCircle(x, y - (mMiniDayNumberTextSize / 3), mDaySelectedCircleSize, 472 mDayNumberSelectedPaint); 473 } 474 475 if (mHasToday && mToday == day) { 476 mDayNumberPaint.setColor(mSelectedDayColor); 477 } else { 478 mDayNumberPaint.setColor(mNormalTextColor); 479 } 480 final Paint paint = (day < mEnabledDayStart || day > mEnabledDayEnd) ? 481 mDayNumberDisabledPaint : mDayNumberPaint; 482 canvas.drawText(String.format("%d", day), x, y, paint); 483 j++; 484 if (j == mNumDays) { 485 j = 0; 486 y += mRowHeight; 487 } 488 } 489 } 490 491 private int findDayOffset() { 492 return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) 493 - mWeekStart; 494 } 495 496 /** 497 * Calculates the day that the given x position is in, accounting for week 498 * number. Returns the day or -1 if the position wasn't in a day. 499 * 500 * @param x The x position of the touch event 501 * @return The day number, or -1 if the position wasn't in a day 502 */ 503 private int getDayFromLocation(float x, float y) { 504 int dayStart = mPadding; 505 if (x < dayStart || x > mWidth - mPadding) { 506 return -1; 507 } 508 // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels 509 int row = (int) (y - mMonthHeaderSize) / mRowHeight; 510 int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); 511 512 int day = column - findDayOffset() + 1; 513 day += row * mNumDays; 514 if (day < 1 || day > mNumCells) { 515 return -1; 516 } 517 return day; 518 } 519 520 /** 521 * Called when the user clicks on a day. Handles callbacks to the 522 * {@link OnDayClickListener} if one is set. 523 * 524 * @param day The day that was clicked 525 */ 526 private void onDayClick(int day) { 527 if (mOnDayClickListener != null) { 528 Calendar date = Calendar.getInstance(); 529 date.set(mYear, mMonth, day); 530 mOnDayClickListener.onDayClick(this, date); 531 } 532 533 // This is a no-op if accessibility is turned off. 534 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); 535 } 536 537 /** 538 * @return The date that has accessibility focus, or {@code null} if no date 539 * has focus 540 */ 541 Calendar getAccessibilityFocus() { 542 final int day = mTouchHelper.getFocusedVirtualView(); 543 Calendar date = null; 544 if (day >= 0) { 545 date = Calendar.getInstance(); 546 date.set(mYear, mMonth, day); 547 } 548 return date; 549 } 550 551 /** 552 * Clears accessibility focus within the view. No-op if the view does not 553 * contain accessibility focus. 554 */ 555 public void clearAccessibilityFocus() { 556 mTouchHelper.clearFocusedVirtualView(); 557 } 558 559 /** 560 * Attempts to restore accessibility focus to the specified date. 561 * 562 * @param day The date which should receive focus 563 * @return {@code false} if the date is not valid for this month view, or 564 * {@code true} if the date received focus 565 */ 566 boolean restoreAccessibilityFocus(Calendar day) { 567 if ((day.get(Calendar.YEAR) != mYear) || (day.get(Calendar.MONTH) != mMonth) || 568 (day.get(Calendar.DAY_OF_MONTH) > mNumCells)) { 569 return false; 570 } 571 mTouchHelper.setFocusedVirtualView(day.get(Calendar.DAY_OF_MONTH)); 572 return true; 573 } 574 575 /** 576 * Provides a virtual view hierarchy for interfacing with an accessibility 577 * service. 578 */ 579 private class MonthViewTouchHelper extends ExploreByTouchHelper { 580 private static final String DATE_FORMAT = "dd MMMM yyyy"; 581 582 private final Rect mTempRect = new Rect(); 583 private final Calendar mTempCalendar = Calendar.getInstance(); 584 585 public MonthViewTouchHelper(View host) { 586 super(host); 587 } 588 589 public void setFocusedVirtualView(int virtualViewId) { 590 getAccessibilityNodeProvider(SimpleMonthView.this).performAction( 591 virtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 592 } 593 594 public void clearFocusedVirtualView() { 595 final int focusedVirtualView = getFocusedVirtualView(); 596 if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { 597 getAccessibilityNodeProvider(SimpleMonthView.this).performAction( 598 focusedVirtualView, 599 AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, 600 null); 601 } 602 } 603 604 @Override 605 protected int getVirtualViewAt(float x, float y) { 606 final int day = getDayFromLocation(x, y); 607 if (day >= 0) { 608 return day; 609 } 610 return ExploreByTouchHelper.INVALID_ID; 611 } 612 613 @Override 614 protected void getVisibleVirtualViews(IntArray virtualViewIds) { 615 for (int day = 1; day <= mNumCells; day++) { 616 virtualViewIds.add(day); 617 } 618 } 619 620 @Override 621 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 622 event.setContentDescription(getItemDescription(virtualViewId)); 623 } 624 625 @Override 626 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { 627 getItemBounds(virtualViewId, mTempRect); 628 629 node.setContentDescription(getItemDescription(virtualViewId)); 630 node.setBoundsInParent(mTempRect); 631 node.addAction(AccessibilityNodeInfo.ACTION_CLICK); 632 633 if (virtualViewId == mSelectedDay) { 634 node.setSelected(true); 635 } 636 637 } 638 639 @Override 640 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 641 Bundle arguments) { 642 switch (action) { 643 case AccessibilityNodeInfo.ACTION_CLICK: 644 onDayClick(virtualViewId); 645 return true; 646 } 647 648 return false; 649 } 650 651 /** 652 * Calculates the bounding rectangle of a given time object. 653 * 654 * @param day The day to calculate bounds for 655 * @param rect The rectangle in which to store the bounds 656 */ 657 private void getItemBounds(int day, Rect rect) { 658 final int offsetX = mPadding; 659 final int offsetY = mMonthHeaderSize; 660 final int cellHeight = mRowHeight; 661 final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays); 662 final int index = ((day - 1) + findDayOffset()); 663 final int row = (index / mNumDays); 664 final int column = (index % mNumDays); 665 final int x = (offsetX + (column * cellWidth)); 666 final int y = (offsetY + (row * cellHeight)); 667 668 rect.set(x, y, (x + cellWidth), (y + cellHeight)); 669 } 670 671 /** 672 * Generates a description for a given time object. Since this 673 * description will be spoken, the components are ordered by descending 674 * specificity as DAY MONTH YEAR. 675 * 676 * @param day The day to generate a description for 677 * @return A description of the time object 678 */ 679 private CharSequence getItemDescription(int day) { 680 mTempCalendar.set(mYear, mMonth, day); 681 final CharSequence date = DateFormat.format(DATE_FORMAT, 682 mTempCalendar.getTimeInMillis()); 683 684 if (day == mSelectedDay) { 685 return getContext().getString(R.string.item_is_selected, date); 686 } 687 688 return date; 689 } 690 } 691 692 /** 693 * Handles callbacks when the user clicks on a time object. 694 */ 695 public interface OnDayClickListener { 696 public void onDayClick(SimpleMonthView view, Calendar day); 697 } 698} 699