MonthView.java revision 5cfe197091de6e7b5550fc2503e00fe554bef5cd
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.MonthAdapter.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 abstract class MonthView extends View { 55 private static final String TAG = "MonthView"; 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 MonthView(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 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 setSelectedDay(int day) { 376 mSelectedDay = day; 377 } 378 379 public void reuse() { 380 mNumRows = DEFAULT_NUM_ROWS; 381 requestLayout(); 382 } 383 384 private int calculateNumRows() { 385 int offset = findDayOffset(); 386 int dividend = (offset + mNumCells) / mNumDays; 387 int remainder = (offset + mNumCells) % mNumDays; 388 return (dividend + (remainder > 0 ? 1 : 0)); 389 } 390 391 private boolean sameDay(int day, Time today) { 392 return mYear == today.year && 393 mMonth == today.month && 394 day == today.monthDay; 395 } 396 397 @Override 398 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 399 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows 400 + MONTH_HEADER_SIZE); 401 } 402 403 @Override 404 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 405 mWidth = w; 406 407 // Invalidate cached accessibility information. 408 mTouchHelper.invalidateRoot(); 409 } 410 411 public int getMonth() { 412 return mMonth; 413 } 414 415 public int getYear() { 416 return mYear; 417 } 418 419 private String getMonthAndYearString() { 420 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR 421 | DateUtils.FORMAT_NO_MONTH_DAY; 422 mStringBuilder.setLength(0); 423 long millis = mCalendar.getTimeInMillis(); 424 return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, 425 Time.getCurrentTimezone()).toString(); 426 } 427 428 private void drawMonthTitle(Canvas canvas) { 429 int x = (mWidth + 2 * mPadding) / 2; 430 int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3); 431 canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); 432 } 433 434 private void drawMonthDayLabels(Canvas canvas) { 435 int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2); 436 int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 437 438 for (int i = 0; i < mNumDays; i++) { 439 int calendarDay = (i + mWeekStart) % mNumDays; 440 int x = (2 * i + 1) * dayWidthHalf + mPadding; 441 mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); 442 canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, 443 Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y, 444 mMonthDayLabelPaint); 445 } 446 } 447 448 /** 449 * Draws the week and month day numbers for this week. Override this method 450 * if you need different placement. 451 * 452 * @param canvas The canvas to draw on 453 */ 454 protected void drawMonthNums(Canvas canvas) { 455 int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH) 456 + MONTH_HEADER_SIZE; 457 int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 458 int j = findDayOffset(); 459 for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) { 460 int x = (2 * j + 1) * dayWidthHalf + mPadding; 461 462 int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH; 463 464 int startX = x - dayWidthHalf; 465 int stopX = x + dayWidthHalf; 466 int startY = y - yRelativeToDay; 467 int stopY = startY + mRowHeight; 468 469 drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY); 470 471 j++; 472 if (j == mNumDays) { 473 j = 0; 474 y += mRowHeight; 475 } 476 } 477 } 478 479 /** 480 * This method should draw the month day. Implemented by sub-classes to allow customization. 481 * 482 * @param canvas The canvas to draw on 483 * @param year The year of this month day 484 * @param month The month of this month day 485 * @param day The day number of this month day 486 * @param x The default x position to draw the day number 487 * @param y The default y position to draw the day number 488 * @param startX The left boundary of the day number rect 489 * @param stopX The right boundary of the day number rect 490 * @param startY The top boundary of the day number rect 491 * @param stopY The bottom boundary of the day number rect 492 */ 493 public abstract void drawMonthDay(Canvas canvas, int year, int month, int day, 494 int x, int y, int startX, int stopX, int startY, int stopY); 495 496 private int findDayOffset() { 497 return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) 498 - mWeekStart; 499 } 500 501 502 /** 503 * Calculates the day that the given x position is in, accounting for week 504 * number. Returns the day or -1 if the position wasn't in a day. 505 * 506 * @param x The x position of the touch event 507 * @return The day number, or -1 if the position wasn't in a day 508 */ 509 public int getDayFromLocation(float x, float y) { 510 int dayStart = mPadding; 511 if (x < dayStart || x > mWidth - mPadding) { 512 return -1; 513 } 514 // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels 515 int row = (int) (y - MONTH_HEADER_SIZE) / mRowHeight; 516 int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); 517 518 int day = column - findDayOffset() + 1; 519 day += row * mNumDays; 520 if (day < 1 || day > mNumCells) { 521 return -1; 522 } 523 return day; 524 } 525 526 /** 527 * Called when the user clicks on a day. Handles callbacks to the 528 * {@link OnDayClickListener} if one is set. 529 * 530 * @param day The day that was clicked 531 */ 532 private void onDayClick(int day) { 533 if (mOnDayClickListener != null) { 534 mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day)); 535 } 536 537 // This is a no-op if accessibility is turned off. 538 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); 539 } 540 541 /** 542 * @return The date that has accessibility focus, or {@code null} if no date 543 * has focus 544 */ 545 public CalendarDay getAccessibilityFocus() { 546 final int day = mTouchHelper.getFocusedVirtualView(); 547 if (day >= 0) { 548 return new CalendarDay(mYear, mMonth, day); 549 } 550 return null; 551 } 552 553 /** 554 * Clears accessibility focus within the view. No-op if the view does not 555 * contain accessibility focus. 556 */ 557 public void clearAccessibilityFocus() { 558 mTouchHelper.clearFocusedVirtualView(); 559 } 560 561 /** 562 * Attempts to restore accessibility focus to the specified date. 563 * 564 * @param day The date which should receive focus 565 * @return {@code false} if the date is not valid for this month view, or 566 * {@code true} if the date received focus 567 */ 568 public boolean restoreAccessibilityFocus(CalendarDay day) { 569 if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) { 570 return false; 571 } 572 mTouchHelper.setFocusedVirtualView(day.day); 573 return true; 574 } 575 576 /** 577 * Provides a virtual view hierarchy for interfacing with an accessibility 578 * service. 579 */ 580 private class MonthViewTouchHelper extends ExploreByTouchHelper { 581 private static final String DATE_FORMAT = "dd MMMM yyyy"; 582 583 private final Rect mTempRect = new Rect(); 584 private final Calendar mTempCalendar = Calendar.getInstance(); 585 586 public MonthViewTouchHelper(View host) { 587 super(host); 588 } 589 590 public void setFocusedVirtualView(int virtualViewId) { 591 getAccessibilityNodeProvider(MonthView.this).performAction( 592 virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); 593 } 594 595 public void clearFocusedVirtualView() { 596 final int focusedVirtualView = getFocusedVirtualView(); 597 if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { 598 getAccessibilityNodeProvider(MonthView.this).performAction( 599 focusedVirtualView, 600 AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, 601 null); 602 } 603 } 604 605 @Override 606 protected int getVirtualViewAt(float x, float y) { 607 final int day = getDayFromLocation(x, y); 608 if (day >= 0) { 609 return day; 610 } 611 return ExploreByTouchHelper.INVALID_ID; 612 } 613 614 @Override 615 protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { 616 for (int day = 1; day <= mNumCells; day++) { 617 virtualViewIds.add(day); 618 } 619 } 620 621 @Override 622 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 623 event.setContentDescription(getItemDescription(virtualViewId)); 624 } 625 626 @Override 627 protected void onPopulateNodeForVirtualView(int virtualViewId, 628 AccessibilityNodeInfoCompat node) { 629 getItemBounds(virtualViewId, mTempRect); 630 631 node.setContentDescription(getItemDescription(virtualViewId)); 632 node.setBoundsInParent(mTempRect); 633 node.addAction(AccessibilityNodeInfo.ACTION_CLICK); 634 635 if (virtualViewId == mSelectedDay) { 636 node.setSelected(true); 637 } 638 639 } 640 641 @Override 642 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 643 Bundle arguments) { 644 switch (action) { 645 case AccessibilityNodeInfo.ACTION_CLICK: 646 onDayClick(virtualViewId); 647 return true; 648 } 649 650 return false; 651 } 652 653 /** 654 * Calculates the bounding rectangle of a given time object. 655 * 656 * @param day The day to calculate bounds for 657 * @param rect The rectangle in which to store the bounds 658 */ 659 private void getItemBounds(int day, Rect rect) { 660 final int offsetX = mPadding; 661 final int offsetY = MONTH_HEADER_SIZE; 662 final int cellHeight = mRowHeight; 663 final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays); 664 final int index = ((day - 1) + findDayOffset()); 665 final int row = (index / mNumDays); 666 final int column = (index % mNumDays); 667 final int x = (offsetX + (column * cellWidth)); 668 final int y = (offsetY + (row * cellHeight)); 669 670 rect.set(x, y, (x + cellWidth), (y + cellHeight)); 671 } 672 673 /** 674 * Generates a description for a given time object. Since this 675 * description will be spoken, the components are ordered by descending 676 * specificity as DAY MONTH YEAR. 677 * 678 * @param day The day to generate a description for 679 * @return A description of the time object 680 */ 681 private CharSequence getItemDescription(int day) { 682 mTempCalendar.set(mYear, mMonth, day); 683 final CharSequence date = DateFormat.format(DATE_FORMAT, 684 mTempCalendar.getTimeInMillis()); 685 686 if (day == mSelectedDay) { 687 return getContext().getString(R.string.item_is_selected, date); 688 } 689 690 return date; 691 } 692 } 693 694 /** 695 * Handles callbacks when the user clicks on a time object. 696 */ 697 public interface OnDayClickListener { 698 public void onDayClick(MonthView view, CalendarDay day); 699 } 700} 701