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