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