MonthView.java revision e0a0cb288106e3a25441ea57a123a812929ec79c
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 protected final DatePickerController mController; 124 125 // affects the padding on the sides of this view 126 protected int mPadding = 0; 127 128 private String mDayOfWeekTypeface; 129 private String mMonthTitleTypeface; 130 131 protected Paint mMonthNumPaint; 132 protected Paint mMonthTitlePaint; 133 protected Paint mMonthTitleBGPaint; 134 protected Paint mSelectedCirclePaint; 135 protected Paint mMonthDayLabelPaint; 136 137 private final Formatter mFormatter; 138 private final StringBuilder mStringBuilder; 139 140 // The Julian day of the first day displayed by this item 141 protected int mFirstJulianDay = -1; 142 // The month of the first day in this week 143 protected int mFirstMonth = -1; 144 // The month of the last day in this week 145 protected int mLastMonth = -1; 146 147 protected int mMonth; 148 149 protected int mYear; 150 // Quick reference to the width of this view, matches parent 151 protected int mWidth; 152 // The height this view should draw at in pixels, set by height param 153 protected int mRowHeight = DEFAULT_HEIGHT; 154 // If this view contains the today 155 protected boolean mHasToday = false; 156 // Which day is selected [0-6] or -1 if no day is selected 157 protected int mSelectedDay = -1; 158 // Which day is today [0-6] or -1 if no day is today 159 protected int mToday = DEFAULT_SELECTED_DAY; 160 // Which day of the week to start on [0-6] 161 protected int mWeekStart = DEFAULT_WEEK_START; 162 // How many days to display 163 protected int mNumDays = DEFAULT_NUM_DAYS; 164 // The number of days + a spot for week number if it is displayed 165 protected int mNumCells = mNumDays; 166 // The left edge of the selected day 167 protected int mSelectedLeft = -1; 168 // The right edge of the selected day 169 protected int mSelectedRight = -1; 170 171 private final Calendar mCalendar; 172 protected final Calendar mDayLabelCalendar; 173 private final MonthViewTouchHelper mTouchHelper; 174 175 private int mNumRows = DEFAULT_NUM_ROWS; 176 177 // Optional listener for handling day click actions 178 private OnDayClickListener mOnDayClickListener; 179 // Whether to prevent setting the accessibility delegate 180 private boolean mLockAccessibilityDelegate; 181 182 protected int mDayTextColor; 183 protected int mTodayNumberColor; 184 protected int mDisabledDayTextColor; 185 protected int mMonthTitleColor; 186 protected int mMonthTitleBGColor; 187 188 public MonthView(Context context) { 189 this(context, null); 190 } 191 192 public MonthView(Context context, DatePickerController controller) { 193 super(context); 194 195 mController = controller; 196 197 Resources res = context.getResources(); 198 199 mDayLabelCalendar = Calendar.getInstance(); 200 mCalendar = Calendar.getInstance(); 201 202 mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface); 203 mMonthTitleTypeface = res.getString(R.string.sans_serif); 204 205 mDayTextColor = res.getColor(R.color.date_picker_text_normal); 206 mTodayNumberColor = res.getColor(R.color.blue); 207 mDisabledDayTextColor = res.getColor(R.color.date_picker_text_disabled); 208 mMonthTitleColor = res.getColor(R.color.white); 209 mMonthTitleBGColor = res.getColor(R.color.circle_background); 210 211 mStringBuilder = new StringBuilder(50); 212 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 213 214 MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size); 215 MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size); 216 MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size); 217 MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height); 218 DAY_SELECTED_CIRCLE_SIZE = res 219 .getDimensionPixelSize(R.dimen.day_number_select_circle_radius); 220 221 mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height) 222 - MONTH_HEADER_SIZE) / MAX_NUM_ROWS; 223 224 // Set up accessibility components. 225 mTouchHelper = getMonthViewTouchHelper(); 226 ViewCompat.setAccessibilityDelegate(this, mTouchHelper); 227 ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 228 mLockAccessibilityDelegate = true; 229 230 // Sets up any standard paints that will be used 231 initView(); 232 } 233 234 protected MonthViewTouchHelper getMonthViewTouchHelper() { 235 return new MonthViewTouchHelper(this); 236 } 237 238 @Override 239 public void setAccessibilityDelegate(AccessibilityDelegate delegate) { 240 // Workaround for a JB MR1 issue where accessibility delegates on 241 // top-level ListView items are overwritten. 242 if (!mLockAccessibilityDelegate) { 243 super.setAccessibilityDelegate(delegate); 244 } 245 } 246 247 public void setOnDayClickListener(OnDayClickListener listener) { 248 mOnDayClickListener = listener; 249 } 250 251 @Override 252 public boolean dispatchHoverEvent(MotionEvent event) { 253 // First right-of-refusal goes the touch exploration helper. 254 if (mTouchHelper.dispatchHoverEvent(event)) { 255 return true; 256 } 257 return super.dispatchHoverEvent(event); 258 } 259 260 @Override 261 public boolean onTouchEvent(MotionEvent event) { 262 switch (event.getAction()) { 263 case MotionEvent.ACTION_UP: 264 final int day = getDayFromLocation(event.getX(), event.getY()); 265 if (day >= 0) { 266 onDayClick(day); 267 } 268 break; 269 } 270 return true; 271 } 272 273 /** 274 * Sets up the text and style properties for painting. Override this if you 275 * want to use a different paint. 276 */ 277 protected void initView() { 278 mMonthTitlePaint = new Paint(); 279 mMonthTitlePaint.setFakeBoldText(true); 280 mMonthTitlePaint.setAntiAlias(true); 281 mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE); 282 mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD)); 283 mMonthTitlePaint.setColor(mDayTextColor); 284 mMonthTitlePaint.setTextAlign(Align.CENTER); 285 mMonthTitlePaint.setStyle(Style.FILL); 286 287 mMonthTitleBGPaint = new Paint(); 288 mMonthTitleBGPaint.setFakeBoldText(true); 289 mMonthTitleBGPaint.setAntiAlias(true); 290 mMonthTitleBGPaint.setColor(mMonthTitleBGColor); 291 mMonthTitleBGPaint.setTextAlign(Align.CENTER); 292 mMonthTitleBGPaint.setStyle(Style.FILL); 293 294 mSelectedCirclePaint = new Paint(); 295 mSelectedCirclePaint.setFakeBoldText(true); 296 mSelectedCirclePaint.setAntiAlias(true); 297 mSelectedCirclePaint.setColor(mTodayNumberColor); 298 mSelectedCirclePaint.setTextAlign(Align.CENTER); 299 mSelectedCirclePaint.setStyle(Style.FILL); 300 mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA); 301 302 mMonthDayLabelPaint = new Paint(); 303 mMonthDayLabelPaint.setAntiAlias(true); 304 mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); 305 mMonthDayLabelPaint.setColor(mDayTextColor); 306 mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL)); 307 mMonthDayLabelPaint.setStyle(Style.FILL); 308 mMonthDayLabelPaint.setTextAlign(Align.CENTER); 309 mMonthDayLabelPaint.setFakeBoldText(true); 310 311 mMonthNumPaint = new Paint(); 312 mMonthNumPaint.setAntiAlias(true); 313 mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); 314 mMonthNumPaint.setStyle(Style.FILL); 315 mMonthNumPaint.setTextAlign(Align.CENTER); 316 mMonthNumPaint.setFakeBoldText(false); 317 } 318 319 @Override 320 protected void onDraw(Canvas canvas) { 321 drawMonthTitle(canvas); 322 drawMonthDayLabels(canvas); 323 drawMonthNums(canvas); 324 } 325 326 private int mDayOfWeekStart = 0; 327 328 /** 329 * Sets all the parameters for displaying this week. The only required 330 * parameter is the week number. Other parameters have a default value and 331 * will only update if a new value is included, except for focus month, 332 * which will always default to no focus month if no value is passed in. See 333 * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters. 334 * 335 * @param params A map of the new parameters, see 336 * {@link #VIEW_PARAMS_HEIGHT} 337 */ 338 public void setMonthParams(HashMap<String, Integer> params) { 339 if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { 340 throw new InvalidParameterException("You must specify month and year for this view"); 341 } 342 setTag(params); 343 // We keep the current value for any params not present 344 if (params.containsKey(VIEW_PARAMS_HEIGHT)) { 345 mRowHeight = params.get(VIEW_PARAMS_HEIGHT); 346 if (mRowHeight < MIN_HEIGHT) { 347 mRowHeight = MIN_HEIGHT; 348 } 349 } 350 if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { 351 mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY); 352 } 353 354 // Allocate space for caching the day numbers and focus values 355 mMonth = params.get(VIEW_PARAMS_MONTH); 356 mYear = params.get(VIEW_PARAMS_YEAR); 357 358 // Figure out what day today is 359 final Time today = new Time(Time.getCurrentTimezone()); 360 today.setToNow(); 361 mHasToday = false; 362 mToday = -1; 363 364 mCalendar.set(Calendar.MONTH, mMonth); 365 mCalendar.set(Calendar.YEAR, mYear); 366 mCalendar.set(Calendar.DAY_OF_MONTH, 1); 367 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); 368 369 if (params.containsKey(VIEW_PARAMS_WEEK_START)) { 370 mWeekStart = params.get(VIEW_PARAMS_WEEK_START); 371 } else { 372 mWeekStart = mCalendar.getFirstDayOfWeek(); 373 } 374 375 mNumCells = Utils.getDaysInMonth(mMonth, mYear); 376 for (int i = 0; i < mNumCells; i++) { 377 final int day = i + 1; 378 if (sameDay(day, today)) { 379 mHasToday = true; 380 mToday = day; 381 } 382 } 383 mNumRows = calculateNumRows(); 384 385 // Invalidate cached accessibility information. 386 mTouchHelper.invalidateRoot(); 387 } 388 389 public void setSelectedDay(int day) { 390 mSelectedDay = day; 391 } 392 393 public void reuse() { 394 mNumRows = DEFAULT_NUM_ROWS; 395 requestLayout(); 396 } 397 398 private int calculateNumRows() { 399 int offset = findDayOffset(); 400 int dividend = (offset + mNumCells) / mNumDays; 401 int remainder = (offset + mNumCells) % mNumDays; 402 return (dividend + (remainder > 0 ? 1 : 0)); 403 } 404 405 private boolean sameDay(int day, Time today) { 406 return mYear == today.year && 407 mMonth == today.month && 408 day == today.monthDay; 409 } 410 411 @Override 412 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 413 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows 414 + MONTH_HEADER_SIZE); 415 } 416 417 @Override 418 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 419 mWidth = w; 420 421 // Invalidate cached accessibility information. 422 mTouchHelper.invalidateRoot(); 423 } 424 425 public int getMonth() { 426 return mMonth; 427 } 428 429 public int getYear() { 430 return mYear; 431 } 432 433 private String getMonthAndYearString() { 434 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR 435 | DateUtils.FORMAT_NO_MONTH_DAY; 436 mStringBuilder.setLength(0); 437 long millis = mCalendar.getTimeInMillis(); 438 return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, 439 Time.getCurrentTimezone()).toString(); 440 } 441 442 protected void drawMonthTitle(Canvas canvas) { 443 int x = (mWidth + 2 * mPadding) / 2; 444 int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3); 445 canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); 446 } 447 448 protected void drawMonthDayLabels(Canvas canvas) { 449 int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2); 450 int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 451 452 for (int i = 0; i < mNumDays; i++) { 453 int calendarDay = (i + mWeekStart) % mNumDays; 454 int x = (2 * i + 1) * dayWidthHalf + mPadding; 455 mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); 456 canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, 457 Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y, 458 mMonthDayLabelPaint); 459 } 460 } 461 462 /** 463 * Draws the week and month day numbers for this week. Override this method 464 * if you need different placement. 465 * 466 * @param canvas The canvas to draw on 467 */ 468 protected void drawMonthNums(Canvas canvas) { 469 int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH) 470 + MONTH_HEADER_SIZE; 471 int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 472 int j = findDayOffset(); 473 for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) { 474 int x = (2 * j + 1) * dayWidthHalf + mPadding; 475 476 int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH; 477 478 int startX = x - dayWidthHalf; 479 int stopX = x + dayWidthHalf; 480 int startY = y - yRelativeToDay; 481 int stopY = startY + mRowHeight; 482 483 drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY); 484 485 j++; 486 if (j == mNumDays) { 487 j = 0; 488 y += mRowHeight; 489 } 490 } 491 } 492 493 /** 494 * This method should draw the month day. Implemented by sub-classes to allow customization. 495 * 496 * @param canvas The canvas to draw on 497 * @param year The year of this month day 498 * @param month The month of this month day 499 * @param day The day number of this month day 500 * @param x The default x position to draw the day number 501 * @param y The default y position to draw the day number 502 * @param startX The left boundary of the day number rect 503 * @param stopX The right boundary of the day number rect 504 * @param startY The top boundary of the day number rect 505 * @param stopY The bottom boundary of the day number rect 506 */ 507 public abstract void drawMonthDay(Canvas canvas, int year, int month, int day, 508 int x, int y, int startX, int stopX, int startY, int stopY); 509 510 protected int findDayOffset() { 511 return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) 512 - mWeekStart; 513 } 514 515 516 /** 517 * Calculates the day that the given x position is in, accounting for week 518 * number. Returns the day or -1 if the position wasn't in a day. 519 * 520 * @param x The x position of the touch event 521 * @return The day number, or -1 if the position wasn't in a day 522 */ 523 public int getDayFromLocation(float x, float y) { 524 int dayStart = mPadding; 525 if (x < dayStart || x > mWidth - mPadding) { 526 return -1; 527 } 528 // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels 529 int row = (int) (y - MONTH_HEADER_SIZE) / mRowHeight; 530 int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); 531 532 int day = column - findDayOffset() + 1; 533 day += row * mNumDays; 534 if (day < 1 || day > mNumCells) { 535 return -1; 536 } 537 return day; 538 } 539 540 /** 541 * Called when the user clicks on a day. Handles callbacks to the 542 * {@link OnDayClickListener} if one is set. 543 * <p/> 544 * If the day is out of the range set by minDate and/or maxDate, this is a no-op. 545 * 546 * @param day The day that was clicked 547 */ 548 private void onDayClick(int day) { 549 // If the min / max date are set, only process the click if it's a valid selection. 550 if (isOutOfRange(mYear, mMonth, day)) { 551 return; 552 } 553 554 555 if (mOnDayClickListener != null) { 556 mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day)); 557 } 558 559 // This is a no-op if accessibility is turned off. 560 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); 561 } 562 563 /** 564 * @return true if the specified year/month/day are within the range set by minDate and maxDate. 565 * If one or either have not been set, they are considered as Integer.MIN_VALUE and 566 * Integer.MAX_VALUE. 567 */ 568 protected boolean isOutOfRange(int year, int month, int day) { 569 if (isBeforeMin(year, month, day)) { 570 return true; 571 } else if (isAfterMax(year, month, day)) { 572 return true; 573 } 574 575 return false; 576 } 577 578 private boolean isBeforeMin(int year, int month, int day) { 579 if (mController == null) { 580 return false; 581 } 582 Calendar minDate = mController.getMinDate(); 583 if (minDate == null) { 584 return false; 585 } 586 587 if (year < minDate.get(Calendar.YEAR)) { 588 return true; 589 } else if (year > minDate.get(Calendar.YEAR)) { 590 return false; 591 } 592 593 if (month < minDate.get(Calendar.MONTH)) { 594 return true; 595 } else if (month > minDate.get(Calendar.MONTH)) { 596 return false; 597 } 598 599 if (day < minDate.get(Calendar.DAY_OF_MONTH)) { 600 return true; 601 } else { 602 return false; 603 } 604 } 605 606 private boolean isAfterMax(int year, int month, int day) { 607 if (mController == null) { 608 return false; 609 } 610 Calendar maxDate = mController.getMaxDate(); 611 if (maxDate == null) { 612 return false; 613 } 614 615 if (year > maxDate.get(Calendar.YEAR)) { 616 return true; 617 } else if (year < maxDate.get(Calendar.YEAR)) { 618 return false; 619 } 620 621 if (month > maxDate.get(Calendar.MONTH)) { 622 return true; 623 } else if (month < maxDate.get(Calendar.MONTH)) { 624 return false; 625 } 626 627 if (day > maxDate.get(Calendar.DAY_OF_MONTH)) { 628 return true; 629 } else { 630 return false; 631 } 632 } 633 634 /** 635 * @return The date that has accessibility focus, or {@code null} if no date 636 * has focus 637 */ 638 public CalendarDay getAccessibilityFocus() { 639 final int day = mTouchHelper.getFocusedVirtualView(); 640 if (day >= 0) { 641 return new CalendarDay(mYear, mMonth, day); 642 } 643 return null; 644 } 645 646 /** 647 * Clears accessibility focus within the view. No-op if the view does not 648 * contain accessibility focus. 649 */ 650 public void clearAccessibilityFocus() { 651 mTouchHelper.clearFocusedVirtualView(); 652 } 653 654 /** 655 * Attempts to restore accessibility focus to the specified date. 656 * 657 * @param day The date which should receive focus 658 * @return {@code false} if the date is not valid for this month view, or 659 * {@code true} if the date received focus 660 */ 661 public boolean restoreAccessibilityFocus(CalendarDay day) { 662 if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) { 663 return false; 664 } 665 mTouchHelper.setFocusedVirtualView(day.day); 666 return true; 667 } 668 669 /** 670 * Provides a virtual view hierarchy for interfacing with an accessibility 671 * service. 672 */ 673 protected class MonthViewTouchHelper extends ExploreByTouchHelper { 674 private static final String DATE_FORMAT = "dd MMMM yyyy"; 675 676 private final Rect mTempRect = new Rect(); 677 private final Calendar mTempCalendar = Calendar.getInstance(); 678 679 public MonthViewTouchHelper(View host) { 680 super(host); 681 } 682 683 public void setFocusedVirtualView(int virtualViewId) { 684 getAccessibilityNodeProvider(MonthView.this).performAction( 685 virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); 686 } 687 688 public void clearFocusedVirtualView() { 689 final int focusedVirtualView = getFocusedVirtualView(); 690 if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { 691 getAccessibilityNodeProvider(MonthView.this).performAction( 692 focusedVirtualView, 693 AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, 694 null); 695 } 696 } 697 698 @Override 699 protected int getVirtualViewAt(float x, float y) { 700 final int day = getDayFromLocation(x, y); 701 if (day >= 0) { 702 return day; 703 } 704 return ExploreByTouchHelper.INVALID_ID; 705 } 706 707 @Override 708 protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { 709 for (int day = 1; day <= mNumCells; day++) { 710 virtualViewIds.add(day); 711 } 712 } 713 714 @Override 715 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 716 event.setContentDescription(getItemDescription(virtualViewId)); 717 } 718 719 @Override 720 protected void onPopulateNodeForVirtualView(int virtualViewId, 721 AccessibilityNodeInfoCompat node) { 722 getItemBounds(virtualViewId, mTempRect); 723 724 node.setContentDescription(getItemDescription(virtualViewId)); 725 node.setBoundsInParent(mTempRect); 726 node.addAction(AccessibilityNodeInfo.ACTION_CLICK); 727 728 if (virtualViewId == mSelectedDay) { 729 node.setSelected(true); 730 } 731 732 } 733 734 @Override 735 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 736 Bundle arguments) { 737 switch (action) { 738 case AccessibilityNodeInfo.ACTION_CLICK: 739 onDayClick(virtualViewId); 740 return true; 741 } 742 743 return false; 744 } 745 746 /** 747 * Calculates the bounding rectangle of a given time object. 748 * 749 * @param day The day to calculate bounds for 750 * @param rect The rectangle in which to store the bounds 751 */ 752 protected void getItemBounds(int day, Rect rect) { 753 final int offsetX = mPadding; 754 final int offsetY = MONTH_HEADER_SIZE; 755 final int cellHeight = mRowHeight; 756 final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays); 757 final int index = ((day - 1) + findDayOffset()); 758 final int row = (index / mNumDays); 759 final int column = (index % mNumDays); 760 final int x = (offsetX + (column * cellWidth)); 761 final int y = (offsetY + (row * cellHeight)); 762 763 rect.set(x, y, (x + cellWidth), (y + cellHeight)); 764 } 765 766 /** 767 * Generates a description for a given time object. Since this 768 * description will be spoken, the components are ordered by descending 769 * specificity as DAY MONTH YEAR. 770 * 771 * @param day The day to generate a description for 772 * @return A description of the time object 773 */ 774 protected CharSequence getItemDescription(int day) { 775 mTempCalendar.set(mYear, mMonth, day); 776 final CharSequence date = DateFormat.format(DATE_FORMAT, 777 mTempCalendar.getTimeInMillis()); 778 779 if (day == mSelectedDay) { 780 return getContext().getString(R.string.item_is_selected, date); 781 } 782 783 return date; 784 } 785 } 786 787 /** 788 * Handles callbacks when the user clicks on a time object. 789 */ 790 public interface OnDayClickListener { 791 public void onDayClick(MonthView view, CalendarDay day); 792 } 793} 794