MonthView.java revision 48bf1debfe72dee562d95f796e4fff3fe78c0bd4
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.util.AttributeSet; 35import android.view.MotionEvent; 36import android.view.View; 37import android.view.accessibility.AccessibilityEvent; 38import android.view.accessibility.AccessibilityNodeInfo; 39 40import com.android.datetimepicker.R; 41import com.android.datetimepicker.Utils; 42import com.android.datetimepicker.date.MonthAdapter.CalendarDay; 43 44import java.security.InvalidParameterException; 45import java.util.Calendar; 46import java.util.Formatter; 47import java.util.HashMap; 48import java.util.List; 49import 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 */ 55public 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 190 public MonthView(Context context) { 191 this(context, null); 192 } 193 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(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 233 public void setDatePickerController(DatePickerController controller) { 234 mController = controller; 235 } 236 237 protected MonthViewTouchHelper getMonthViewTouchHelper() { 238 return new MonthViewTouchHelper(this); 239 } 240 241 @Override 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 250 public void setOnDayClickListener(OnDayClickListener listener) { 251 mOnDayClickListener = listener; 252 } 253 254 @Override 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 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 */ 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 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 */ 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 392 public void setSelectedDay(int day) { 393 mSelectedDay = day; 394 } 395 396 public void reuse() { 397 mNumRows = DEFAULT_NUM_ROWS; 398 requestLayout(); 399 } 400 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 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 415 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 416 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows 417 + getMonthHeaderSize()); 418 } 419 420 @Override 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 428 public int getMonth() { 429 return mMonth; 430 } 431 432 public int getYear() { 433 return mYear; 434 } 435 436 /** 437 * A wrapper to the MonthHeaderSize to allow override it in children 438 */ 439 protected int getMonthHeaderSize() { 440 return MONTH_HEADER_SIZE; 441 } 442 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 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 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 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 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 */ 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 */ 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 */ 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 701 public MonthViewTouchHelper(View host) { 702 super(host); 703 } 704 705 public void setFocusedVirtualView(int virtualViewId) { 706 getAccessibilityNodeProvider(MonthView.this).performAction( 707 virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); 708 } 709 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 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 730 protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { 731 for (int day = 1; day <= mNumCells; day++) { 732 virtualViewIds.add(day); 733 } 734 } 735 736 @Override 737 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 738 event.setContentDescription(getItemDescription(virtualViewId)); 739 } 740 741 @Override 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 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 */ 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 */ 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 { 813 public void onDayClick(MonthView view, CalendarDay day); 814 } 815} 816