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