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