SimpleMonthView.java revision ddf655c49f4173aa55c9ba1a2622cf75cf5bc2f2
1/* 2 * Copyright (C) 2014 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 android.widget; 18 19import android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.Resources; 22import android.content.res.TypedArray; 23import android.graphics.Canvas; 24import android.graphics.Paint; 25import android.graphics.Paint.Align; 26import android.graphics.Paint.Style; 27import android.graphics.Rect; 28import android.graphics.Typeface; 29import android.os.Bundle; 30import android.text.TextPaint; 31import android.text.format.DateFormat; 32import android.util.AttributeSet; 33import android.util.IntArray; 34import android.util.StateSet; 35import android.view.MotionEvent; 36import android.view.View; 37import android.view.accessibility.AccessibilityEvent; 38import android.view.accessibility.AccessibilityNodeInfo; 39import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 40 41import com.android.internal.R; 42import com.android.internal.widget.ExploreByTouchHelper; 43 44import java.text.SimpleDateFormat; 45import java.util.Calendar; 46import java.util.Locale; 47 48/** 49 * A calendar-like view displaying a specified month and the appropriate selectable day numbers 50 * within the specified month. 51 */ 52class SimpleMonthView extends View { 53 private static final int DAYS_IN_WEEK = 7; 54 private static final int MAX_WEEKS_IN_MONTH = 6; 55 56 private static final int DEFAULT_SELECTED_DAY = -1; 57 private static final int DEFAULT_WEEK_START = Calendar.SUNDAY; 58 59 private static final String DEFAULT_TITLE_FORMAT = "MMMMy"; 60 private static final String DAY_OF_WEEK_FORMAT = "EEEEE"; 61 62 private final TextPaint mMonthPaint = new TextPaint(); 63 private final TextPaint mDayOfWeekPaint = new TextPaint(); 64 private final TextPaint mDayPaint = new TextPaint(); 65 private final Paint mDaySelectorPaint = new Paint(); 66 private final Paint mDayHighlightPaint = new Paint(); 67 68 private final Calendar mCalendar = Calendar.getInstance(); 69 private final Calendar mDayOfWeekLabelCalendar = Calendar.getInstance(); 70 71 private final MonthViewTouchHelper mTouchHelper; 72 73 private final SimpleDateFormat mTitleFormatter; 74 private final SimpleDateFormat mDayOfWeekFormatter; 75 76 // Desired dimensions. 77 private final int mDesiredMonthHeight; 78 private final int mDesiredDayOfWeekHeight; 79 private final int mDesiredDayHeight; 80 private final int mDesiredCellWidth; 81 private final int mDesiredDaySelectorRadius; 82 83 private CharSequence mTitle; 84 85 private int mMonth; 86 private int mYear; 87 88 // Dimensions as laid out. 89 private int mMonthHeight; 90 private int mDayOfWeekHeight; 91 private int mDayHeight; 92 private int mCellWidth; 93 private int mDaySelectorRadius; 94 95 private int mPaddedWidth; 96 private int mPaddedHeight; 97 98 /** The day of month for the selected day, or -1 if no day is selected. */ 99 private int mActivatedDay = -1; 100 101 /** 102 * The day of month for today, or -1 if the today is not in the current 103 * month. 104 */ 105 private int mToday = DEFAULT_SELECTED_DAY; 106 107 /** The first day of the week (ex. Calendar.SUNDAY). */ 108 private int mWeekStart = DEFAULT_WEEK_START; 109 110 /** The number of days (ex. 28) in the current month. */ 111 private int mDaysInMonth; 112 113 /** 114 * The day of week (ex. Calendar.SUNDAY) for the first day of the current 115 * month. 116 */ 117 private int mDayOfWeekStart; 118 119 /** The day of month for the first (inclusive) enabled day. */ 120 private int mEnabledDayStart = 1; 121 122 /** The day of month for the last (inclusive) enabled day. */ 123 private int mEnabledDayEnd = 31; 124 125 /** Optional listener for handling day click actions. */ 126 private OnDayClickListener mOnDayClickListener; 127 128 private ColorStateList mDayTextColor; 129 130 private int mTouchedItem = -1; 131 132 public SimpleMonthView(Context context) { 133 this(context, null); 134 } 135 136 public SimpleMonthView(Context context, AttributeSet attrs) { 137 this(context, attrs, R.attr.datePickerStyle); 138 } 139 140 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) { 141 this(context, attrs, defStyleAttr, 0); 142 } 143 144 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 145 super(context, attrs, defStyleAttr, defStyleRes); 146 147 final Resources res = context.getResources(); 148 mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height); 149 mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height); 150 mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height); 151 mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width); 152 mDesiredDaySelectorRadius = res.getDimensionPixelSize( 153 R.dimen.date_picker_day_selector_radius); 154 155 // Set up accessibility components. 156 mTouchHelper = new MonthViewTouchHelper(this); 157 setAccessibilityDelegate(mTouchHelper); 158 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 159 160 final Locale locale = res.getConfiguration().locale; 161 final String titleFormat = DateFormat.getBestDateTimePattern(locale, DEFAULT_TITLE_FORMAT); 162 mTitleFormatter = new SimpleDateFormat(titleFormat, locale); 163 mDayOfWeekFormatter = new SimpleDateFormat(DAY_OF_WEEK_FORMAT, locale); 164 165 initPaints(res); 166 } 167 168 /** 169 * Applies the specified text appearance resource to a paint, returning the 170 * text color if one is set in the text appearance. 171 * 172 * @param p the paint to modify 173 * @param resId the resource ID of the text appearance 174 * @return the text color, if available 175 */ 176 private ColorStateList applyTextAppearance(Paint p, int resId) { 177 final TypedArray ta = mContext.obtainStyledAttributes(null, 178 R.styleable.TextAppearance, 0, resId); 179 180 final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily); 181 if (fontFamily != null) { 182 p.setTypeface(Typeface.create(fontFamily, 0)); 183 } 184 185 p.setTextSize(ta.getDimensionPixelSize( 186 R.styleable.TextAppearance_textSize, (int) p.getTextSize())); 187 188 final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor); 189 if (textColor != null) { 190 final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0); 191 p.setColor(enabledColor); 192 } 193 194 ta.recycle(); 195 196 return textColor; 197 } 198 199 public int getMonthHeight() { 200 return mMonthHeight; 201 } 202 203 public int getCellWidth() { 204 return mCellWidth; 205 } 206 207 public void setMonthTextAppearance(int resId) { 208 applyTextAppearance(mMonthPaint, resId); 209 210 invalidate(); 211 } 212 213 public void setDayOfWeekTextAppearance(int resId) { 214 applyTextAppearance(mDayOfWeekPaint, resId); 215 invalidate(); 216 } 217 218 public void setDayTextAppearance(int resId) { 219 final ColorStateList textColor = applyTextAppearance(mDayPaint, resId); 220 if (textColor != null) { 221 mDayTextColor = textColor; 222 } 223 224 invalidate(); 225 } 226 227 public CharSequence getTitle() { 228 if (mTitle == null) { 229 mTitle = mTitleFormatter.format(mCalendar.getTime()); 230 } 231 return mTitle; 232 } 233 234 /** 235 * Sets up the text and style properties for painting. 236 */ 237 private void initPaints(Resources res) { 238 final String monthTypeface = res.getString(R.string.date_picker_month_typeface); 239 final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface); 240 final String dayTypeface = res.getString(R.string.date_picker_day_typeface); 241 242 final int monthTextSize = res.getDimensionPixelSize( 243 R.dimen.date_picker_month_text_size); 244 final int dayOfWeekTextSize = res.getDimensionPixelSize( 245 R.dimen.date_picker_day_of_week_text_size); 246 final int dayTextSize = res.getDimensionPixelSize( 247 R.dimen.date_picker_day_text_size); 248 249 mMonthPaint.setAntiAlias(true); 250 mMonthPaint.setTextSize(monthTextSize); 251 mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0)); 252 mMonthPaint.setTextAlign(Align.CENTER); 253 mMonthPaint.setStyle(Style.FILL); 254 255 mDayOfWeekPaint.setAntiAlias(true); 256 mDayOfWeekPaint.setTextSize(dayOfWeekTextSize); 257 mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0)); 258 mDayOfWeekPaint.setTextAlign(Align.CENTER); 259 mDayOfWeekPaint.setStyle(Style.FILL); 260 261 mDaySelectorPaint.setAntiAlias(true); 262 mDaySelectorPaint.setStyle(Style.FILL); 263 264 mDayHighlightPaint.setAntiAlias(true); 265 mDayHighlightPaint.setStyle(Style.FILL); 266 267 mDayPaint.setAntiAlias(true); 268 mDayPaint.setTextSize(dayTextSize); 269 mDayPaint.setTypeface(Typeface.create(dayTypeface, 0)); 270 mDayPaint.setTextAlign(Align.CENTER); 271 mDayPaint.setStyle(Style.FILL); 272 } 273 274 void setMonthTextColor(ColorStateList monthTextColor) { 275 final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0); 276 mMonthPaint.setColor(enabledColor); 277 invalidate(); 278 } 279 280 void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) { 281 final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0); 282 mDayOfWeekPaint.setColor(enabledColor); 283 invalidate(); 284 } 285 286 void setDayTextColor(ColorStateList dayTextColor) { 287 mDayTextColor = dayTextColor; 288 invalidate(); 289 } 290 291 void setDaySelectorColor(ColorStateList dayBackgroundColor) { 292 final int activatedColor = dayBackgroundColor.getColorForState( 293 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0); 294 mDaySelectorPaint.setColor(activatedColor); 295 invalidate(); 296 } 297 298 void setDayHighlightColor(ColorStateList dayHighlightColor) { 299 final int pressedColor = dayHighlightColor.getColorForState( 300 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0); 301 mDayHighlightPaint.setColor(pressedColor); 302 invalidate(); 303 } 304 305 public void setOnDayClickListener(OnDayClickListener listener) { 306 mOnDayClickListener = listener; 307 } 308 309 @Override 310 public boolean dispatchHoverEvent(MotionEvent event) { 311 // First right-of-refusal goes the touch exploration helper. 312 return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); 313 } 314 315 @Override 316 public boolean onTouchEvent(MotionEvent event) { 317 final int x = (int) (event.getX() + 0.5f); 318 final int y = (int) (event.getY() + 0.5f); 319 320 final int action = event.getAction(); 321 switch (action) { 322 case MotionEvent.ACTION_DOWN: 323 case MotionEvent.ACTION_MOVE: 324 final int touchedItem = getDayAtLocation(x, y); 325 if (mTouchedItem != touchedItem) { 326 mTouchedItem = touchedItem; 327 invalidate(); 328 } 329 if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) { 330 // Touch something that's not an item, reject event. 331 return false; 332 } 333 break; 334 335 case MotionEvent.ACTION_UP: 336 final int clickedDay = getDayAtLocation(x, y); 337 onDayClicked(clickedDay); 338 // Fall through. 339 case MotionEvent.ACTION_CANCEL: 340 // Reset touched day on stream end. 341 mTouchedItem = -1; 342 invalidate(); 343 break; 344 } 345 return true; 346 } 347 348 @Override 349 protected void onDraw(Canvas canvas) { 350 final int paddingLeft = getPaddingLeft(); 351 final int paddingTop = getPaddingTop(); 352 canvas.translate(paddingLeft, paddingTop); 353 354 drawMonth(canvas); 355 drawDaysOfWeek(canvas); 356 drawDays(canvas); 357 358 canvas.translate(-paddingLeft, -paddingTop); 359 } 360 361 private void drawMonth(Canvas canvas) { 362 final float x = mPaddedWidth / 2f; 363 364 // Vertically centered within the month header height. 365 final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent(); 366 final float y = (mMonthHeight - lineHeight) / 2f; 367 368 canvas.drawText(getTitle().toString(), x, y, mMonthPaint); 369 } 370 371 private void drawDaysOfWeek(Canvas canvas) { 372 final TextPaint p = mDayOfWeekPaint; 373 final int headerHeight = mMonthHeight; 374 final int rowHeight = mDayOfWeekHeight; 375 final int colWidth = mCellWidth; 376 377 // Text is vertically centered within the day of week height. 378 final float halfLineHeight = (p.ascent() + p.descent()) / 2f; 379 final int rowCenter = headerHeight + rowHeight / 2; 380 381 for (int col = 0; col < DAYS_IN_WEEK; col++) { 382 final int colCenter = colWidth * col + colWidth / 2; 383 final int colCenterRtl; 384 if (isLayoutRtl()) { 385 colCenterRtl = mPaddedWidth - colCenter; 386 } else { 387 colCenterRtl = colCenter; 388 } 389 390 final int dayOfWeek = (col + mWeekStart) % DAYS_IN_WEEK; 391 final String label = getDayOfWeekLabel(dayOfWeek); 392 canvas.drawText(label, colCenterRtl, rowCenter - halfLineHeight, p); 393 } 394 } 395 396 private String getDayOfWeekLabel(int dayOfWeek) { 397 mDayOfWeekLabelCalendar.set(Calendar.DAY_OF_WEEK, dayOfWeek); 398 return mDayOfWeekFormatter.format(mDayOfWeekLabelCalendar.getTime()); 399 } 400 401 /** 402 * Draws the month days. 403 */ 404 private void drawDays(Canvas canvas) { 405 final TextPaint p = mDayPaint; 406 final int headerHeight = mMonthHeight + mDayOfWeekHeight; 407 final int rowHeight = mDayHeight; 408 final int colWidth = mCellWidth; 409 410 // Text is vertically centered within the row height. 411 final float halfLineHeight = (p.ascent() + p.descent()) / 2f; 412 int rowCenter = headerHeight + rowHeight / 2; 413 414 for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) { 415 final int colCenter = colWidth * col + colWidth / 2; 416 final int colCenterRtl; 417 if (isLayoutRtl()) { 418 colCenterRtl = mPaddedWidth - colCenter; 419 } else { 420 colCenterRtl = colCenter; 421 } 422 423 int stateMask = 0; 424 425 if (day >= mEnabledDayStart && day <= mEnabledDayEnd) { 426 stateMask |= StateSet.VIEW_STATE_ENABLED; 427 } 428 429 final boolean isDayActivated = mActivatedDay == day; 430 if (isDayActivated) { 431 stateMask |= StateSet.VIEW_STATE_ACTIVATED; 432 433 // Adjust the circle to be centered on the row. 434 canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, mDaySelectorPaint); 435 } else if (mTouchedItem == day) { 436 stateMask |= StateSet.VIEW_STATE_PRESSED; 437 438 // Adjust the circle to be centered on the row. 439 canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, mDayHighlightPaint); 440 } 441 442 final boolean isDayToday = mToday == day; 443 final int dayTextColor; 444 if (isDayToday && !isDayActivated) { 445 dayTextColor = mDaySelectorPaint.getColor(); 446 } else { 447 final int[] stateSet = StateSet.get(stateMask); 448 dayTextColor = mDayTextColor.getColorForState(stateSet, 0); 449 } 450 p.setColor(dayTextColor); 451 452 canvas.drawText(Integer.toString(day), colCenterRtl, rowCenter - halfLineHeight, p); 453 454 col++; 455 456 if (col == DAYS_IN_WEEK) { 457 col = 0; 458 rowCenter += rowHeight; 459 } 460 } 461 } 462 463 private static boolean isValidDayOfWeek(int day) { 464 return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY; 465 } 466 467 private static boolean isValidMonth(int month) { 468 return month >= Calendar.JANUARY && month <= Calendar.DECEMBER; 469 } 470 471 /** 472 * Sets the selected day. 473 * 474 * @param dayOfMonth the selected day of the month, or {@code -1} to clear 475 * the selection 476 */ 477 public void setSelectedDay(int dayOfMonth) { 478 mActivatedDay = dayOfMonth; 479 480 // Invalidate cached accessibility information. 481 mTouchHelper.invalidateRoot(); 482 invalidate(); 483 } 484 485 /** 486 * Sets the first day of the week. 487 * 488 * @param weekStart which day the week should start on, valid values are 489 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY} 490 */ 491 public void setFirstDayOfWeek(int weekStart) { 492 if (isValidDayOfWeek(weekStart)) { 493 mWeekStart = weekStart; 494 } else { 495 mWeekStart = mCalendar.getFirstDayOfWeek(); 496 } 497 498 // Invalidate cached accessibility information. 499 mTouchHelper.invalidateRoot(); 500 invalidate(); 501 } 502 503 /** 504 * Sets all the parameters for displaying this week. 505 * <p> 506 * Parameters have a default value and will only update if a new value is 507 * included, except for focus month, which will always default to no focus 508 * month if no value is passed in. The only required parameter is the week 509 * start. 510 * 511 * @param selectedDay the selected day of the month, or -1 for no selection 512 * @param month the month 513 * @param year the year 514 * @param weekStart which day the week should start on, valid values are 515 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY} 516 * @param enabledDayStart the first enabled day 517 * @param enabledDayEnd the last enabled day 518 */ 519 void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart, 520 int enabledDayEnd) { 521 mActivatedDay = selectedDay; 522 523 if (isValidMonth(month)) { 524 mMonth = month; 525 } 526 mYear = year; 527 528 mCalendar.set(Calendar.MONTH, mMonth); 529 mCalendar.set(Calendar.YEAR, mYear); 530 mCalendar.set(Calendar.DAY_OF_MONTH, 1); 531 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); 532 533 if (isValidDayOfWeek(weekStart)) { 534 mWeekStart = weekStart; 535 } else { 536 mWeekStart = mCalendar.getFirstDayOfWeek(); 537 } 538 539 if (enabledDayStart > 0 && enabledDayEnd < 32) { 540 mEnabledDayStart = enabledDayStart; 541 } 542 if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) { 543 mEnabledDayEnd = enabledDayEnd; 544 } 545 546 // Figure out what day today is. 547 final Calendar today = Calendar.getInstance(); 548 mToday = -1; 549 mDaysInMonth = getDaysInMonth(mMonth, mYear); 550 for (int i = 0; i < mDaysInMonth; i++) { 551 final int day = i + 1; 552 if (sameDay(day, today)) { 553 mToday = day; 554 } 555 } 556 557 // Invalidate the old title. 558 mTitle = null; 559 560 // Invalidate cached accessibility information. 561 mTouchHelper.invalidateRoot(); 562 } 563 564 private static int getDaysInMonth(int month, int year) { 565 switch (month) { 566 case Calendar.JANUARY: 567 case Calendar.MARCH: 568 case Calendar.MAY: 569 case Calendar.JULY: 570 case Calendar.AUGUST: 571 case Calendar.OCTOBER: 572 case Calendar.DECEMBER: 573 return 31; 574 case Calendar.APRIL: 575 case Calendar.JUNE: 576 case Calendar.SEPTEMBER: 577 case Calendar.NOVEMBER: 578 return 30; 579 case Calendar.FEBRUARY: 580 return (year % 4 == 0) ? 29 : 28; 581 default: 582 throw new IllegalArgumentException("Invalid Month"); 583 } 584 } 585 586 private boolean sameDay(int day, Calendar today) { 587 return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH) 588 && day == today.get(Calendar.DAY_OF_MONTH); 589 } 590 591 @Override 592 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 593 final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH 594 + mDesiredDayOfWeekHeight + mDesiredMonthHeight 595 + getPaddingTop() + getPaddingBottom(); 596 final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK 597 + getPaddingStart() + getPaddingEnd(); 598 final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec); 599 final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec); 600 setMeasuredDimension(resolvedWidth, resolvedHeight); 601 } 602 603 @Override 604 public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) { 605 super.onRtlPropertiesChanged(layoutDirection); 606 607 requestLayout(); 608 } 609 610 @Override 611 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 612 if (!changed) { 613 return; 614 } 615 616 // Let's initialize a completely reasonable number of variables. 617 final int w = right - left; 618 final int h = bottom - top; 619 final int paddingLeft = getPaddingLeft(); 620 final int paddingTop = getPaddingTop(); 621 final int paddingRight = getPaddingRight(); 622 final int paddingBottom = getPaddingBottom(); 623 final int paddedRight = w - paddingRight; 624 final int paddedBottom = h - paddingBottom; 625 final int paddedWidth = paddedRight - paddingLeft; 626 final int paddedHeight = paddedBottom - paddingTop; 627 if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) { 628 return; 629 } 630 631 mPaddedWidth = paddedWidth; 632 mPaddedHeight = paddedHeight; 633 634 // We may have been laid out smaller than our preferred size. If so, 635 // scale all dimensions to fit. 636 final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom; 637 final float scaleH = paddedHeight / (float) measuredPaddedHeight; 638 final int monthHeight = (int) (mDesiredMonthHeight * scaleH); 639 final int cellWidth = mPaddedWidth / DAYS_IN_WEEK; 640 mMonthHeight = monthHeight; 641 mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH); 642 mDayHeight = (int) (mDesiredDayHeight * scaleH); 643 mCellWidth = cellWidth; 644 645 // Compute the largest day selector radius that's still within the clip 646 // bounds and desired selector radius. 647 final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight); 648 final int maxSelectorHeight = mDayHeight / 2 + paddingBottom; 649 mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius, 650 Math.min(maxSelectorWidth, maxSelectorHeight)); 651 652 // Invalidate cached accessibility information. 653 mTouchHelper.invalidateRoot(); 654 } 655 656 private int findDayOffset() { 657 final int offset = mDayOfWeekStart - mWeekStart; 658 if (mDayOfWeekStart < mWeekStart) { 659 return offset + DAYS_IN_WEEK; 660 } 661 return offset; 662 } 663 664 /** 665 * Calculates the day of the month at the specified touch position. Returns 666 * the day of the month or -1 if the position wasn't in a valid day. 667 * 668 * @param x the x position of the touch event 669 * @param y the y position of the touch event 670 * @return the day of the month at (x, y), or -1 if the position wasn't in 671 * a valid day 672 */ 673 private int getDayAtLocation(int x, int y) { 674 final int paddedX = x - getPaddingLeft(); 675 if (paddedX < 0 || paddedX >= mPaddedWidth) { 676 return -1; 677 } 678 679 final int headerHeight = mMonthHeight + mDayOfWeekHeight; 680 final int paddedY = y - getPaddingTop(); 681 if (paddedY < headerHeight || paddedY >= mPaddedHeight) { 682 return -1; 683 } 684 685 // Adjust for RTL after applying padding. 686 final int paddedXRtl; 687 if (isLayoutRtl()) { 688 paddedXRtl = mPaddedWidth - paddedX; 689 } else { 690 paddedXRtl = paddedX; 691 } 692 693 final int row = (paddedY - headerHeight) / mDayHeight; 694 final int col = (paddedXRtl * DAYS_IN_WEEK) / mPaddedWidth; 695 final int index = col + row * DAYS_IN_WEEK; 696 final int day = index + 1 - findDayOffset(); 697 if (day < 1 || day > mDaysInMonth) { 698 return -1; 699 } 700 701 return day; 702 } 703 704 /** 705 * Calculates the bounds of the specified day. 706 * 707 * @param id the day of the month 708 * @param outBounds the rect to populate with bounds 709 */ 710 private boolean getBoundsForDay(int id, Rect outBounds) { 711 if (id < 1 || id > mDaysInMonth) { 712 return false; 713 } 714 715 final int index = id - 1 + findDayOffset(); 716 717 // Compute left edge, taking into account RTL. 718 final int col = index % DAYS_IN_WEEK; 719 final int colWidth = mCellWidth; 720 final int left; 721 if (isLayoutRtl()) { 722 left = getWidth() - getPaddingRight() - (col + 1) * colWidth; 723 } else { 724 left = getPaddingLeft() + col * colWidth; 725 } 726 727 // Compute top edge. 728 final int row = index / DAYS_IN_WEEK; 729 final int rowHeight = mDayHeight; 730 final int headerHeight = mMonthHeight + mDayOfWeekHeight; 731 final int top = getPaddingTop() + headerHeight + row * rowHeight; 732 733 outBounds.set(left, top, left + colWidth, top + rowHeight); 734 735 return true; 736 } 737 738 /** 739 * Called when the user clicks on a day. Handles callbacks to the 740 * {@link OnDayClickListener} if one is set. 741 * 742 * @param day the day that was clicked 743 */ 744 private boolean onDayClicked(int day) { 745 if (day < 0 || day > mDaysInMonth) { 746 return false; 747 } 748 749 if (mOnDayClickListener != null) { 750 final Calendar date = Calendar.getInstance(); 751 date.set(mYear, mMonth, day); 752 mOnDayClickListener.onDayClick(this, date); 753 } 754 755 // This is a no-op if accessibility is turned off. 756 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); 757 return true; 758 } 759 760 /** 761 * Provides a virtual view hierarchy for interfacing with an accessibility 762 * service. 763 */ 764 private class MonthViewTouchHelper extends ExploreByTouchHelper { 765 private static final String DATE_FORMAT = "dd MMMM yyyy"; 766 767 private final Rect mTempRect = new Rect(); 768 private final Calendar mTempCalendar = Calendar.getInstance(); 769 770 public MonthViewTouchHelper(View host) { 771 super(host); 772 } 773 774 @Override 775 protected int getVirtualViewAt(float x, float y) { 776 final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f)); 777 if (day >= 0) { 778 return day; 779 } 780 return ExploreByTouchHelper.INVALID_ID; 781 } 782 783 @Override 784 protected void getVisibleVirtualViews(IntArray virtualViewIds) { 785 for (int day = 1; day <= mDaysInMonth; day++) { 786 virtualViewIds.add(day); 787 } 788 } 789 790 @Override 791 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 792 event.setContentDescription(getDayDescription(virtualViewId)); 793 } 794 795 @Override 796 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { 797 final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect); 798 799 if (!hasBounds) { 800 // The day is invalid, kill the node. 801 mTempRect.setEmpty(); 802 node.setContentDescription(""); 803 node.setBoundsInParent(mTempRect); 804 node.setVisibleToUser(false); 805 return; 806 } 807 808 node.setText(getDayText(virtualViewId)); 809 node.setContentDescription(getDayDescription(virtualViewId)); 810 node.setBoundsInParent(mTempRect); 811 node.addAction(AccessibilityAction.ACTION_CLICK); 812 813 if (virtualViewId == mActivatedDay) { 814 // TODO: This should use activated once that's supported. 815 node.setChecked(true); 816 } 817 818 } 819 820 @Override 821 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 822 Bundle arguments) { 823 switch (action) { 824 case AccessibilityNodeInfo.ACTION_CLICK: 825 return onDayClicked(virtualViewId); 826 } 827 828 return false; 829 } 830 831 /** 832 * Generates a description for a given virtual view. 833 * 834 * @param id the day to generate a description for 835 * @return a description of the virtual view 836 */ 837 private CharSequence getDayDescription(int id) { 838 if (id >= 1 && id <= mDaysInMonth) { 839 mTempCalendar.set(mYear, mMonth, id); 840 return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis()); 841 } 842 843 return ""; 844 } 845 846 /** 847 * Generates displayed text for a given virtual view. 848 * 849 * @param id the day to generate text for 850 * @return the visible text of the virtual view 851 */ 852 private CharSequence getDayText(int id) { 853 if (id >= 1 && id <= mDaysInMonth) { 854 return Integer.toString(id); 855 } 856 857 return null; 858 } 859 } 860 861 /** 862 * Handles callbacks when the user clicks on a time object. 863 */ 864 public interface OnDayClickListener { 865 void onDayClick(SimpleMonthView view, Calendar day); 866 } 867} 868