SimpleMonthView.java revision 0ef59ac0e57e9b99d174d4a53f7d9639357743ac
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 mDayLabelCalendar = Calendar.getInstance(); 70 71 private final MonthViewTouchHelper mTouchHelper; 72 73 private final SimpleDateFormat mTitleFormatter; 74 private final SimpleDateFormat mDayOfWeekFormatter; 75 76 private CharSequence mTitle; 77 78 private int mMonth; 79 private int mYear; 80 81 private int mPaddedWidth; 82 private int mPaddedHeight; 83 84 private final int mMonthHeight; 85 private final int mDayOfWeekHeight; 86 private final int mDayHeight; 87 private final int mCellWidth; 88 private final int mDaySelectorRadius; 89 90 /** The day of month for the selected day, or -1 if no day is selected. */ 91 private int mActivatedDay = -1; 92 93 /** 94 * The day of month for today, or -1 if the today is not in the current 95 * month. 96 */ 97 private int mToday = DEFAULT_SELECTED_DAY; 98 99 /** The first day of the week (ex. Calendar.SUNDAY). */ 100 private int mWeekStart = DEFAULT_WEEK_START; 101 102 /** The number of days (ex. 28) in the current month. */ 103 private int mDaysInMonth; 104 105 /** 106 * The day of week (ex. Calendar.SUNDAY) for the first day of the current 107 * month. 108 */ 109 private int mDayOfWeekStart; 110 111 /** The day of month for the first (inclusive) enabled day. */ 112 private int mEnabledDayStart = 1; 113 114 /** The day of month for the last (inclusive) enabled day. */ 115 private int mEnabledDayEnd = 31; 116 117 /** The number of week rows needed to display the current month. */ 118 private int mNumWeeks = MAX_WEEKS_IN_MONTH; 119 120 /** Optional listener for handling day click actions. */ 121 private OnDayClickListener mOnDayClickListener; 122 123 private ColorStateList mDayTextColor; 124 125 private int mTouchedDay = -1; 126 127 public SimpleMonthView(Context context) { 128 this(context, null); 129 } 130 131 public SimpleMonthView(Context context, AttributeSet attrs) { 132 this(context, attrs, R.attr.datePickerStyle); 133 } 134 135 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) { 136 this(context, attrs, defStyleAttr, 0); 137 } 138 139 public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 140 super(context, attrs, defStyleAttr, defStyleRes); 141 142 final Resources res = context.getResources(); 143 mMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height); 144 mDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height); 145 mDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height); 146 mCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width); 147 mDaySelectorRadius = res.getDimensionPixelSize(R.dimen.date_picker_day_selector_radius); 148 149 // Set up accessibility components. 150 mTouchHelper = new MonthViewTouchHelper(this); 151 setAccessibilityDelegate(mTouchHelper); 152 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 153 154 final Locale locale = res.getConfiguration().locale; 155 final String titleFormat = DateFormat.getBestDateTimePattern(locale, DEFAULT_TITLE_FORMAT); 156 mTitleFormatter = new SimpleDateFormat(titleFormat, locale); 157 mDayOfWeekFormatter = new SimpleDateFormat(DAY_OF_WEEK_FORMAT, locale); 158 159 setClickable(true); 160 initPaints(res); 161 } 162 163 /** 164 * Applies the specified text appearance resource to a paint, returning the 165 * text color if one is set in the text appearance. 166 * 167 * @param p the paint to modify 168 * @param resId the resource ID of the text appearance 169 * @return the text color, if available 170 */ 171 private ColorStateList applyTextAppearance(Paint p, int resId) { 172 final TypedArray ta = mContext.obtainStyledAttributes(null, 173 R.styleable.TextAppearance, 0, resId); 174 175 final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily); 176 if (fontFamily != null) { 177 p.setTypeface(Typeface.create(fontFamily, 0)); 178 } 179 180 p.setTextSize(ta.getDimensionPixelSize( 181 R.styleable.TextAppearance_textSize, (int) p.getTextSize())); 182 183 final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor); 184 if (textColor != null) { 185 final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0); 186 p.setColor(enabledColor); 187 } 188 189 ta.recycle(); 190 191 return textColor; 192 } 193 194 public void setMonthTextAppearance(int resId) { 195 applyTextAppearance(mMonthPaint, resId); 196 invalidate(); 197 } 198 199 public void setDayOfWeekTextAppearance(int resId) { 200 applyTextAppearance(mDayOfWeekPaint, resId); 201 invalidate(); 202 } 203 204 public void setDayTextAppearance(int resId) { 205 final ColorStateList textColor = applyTextAppearance(mDayPaint, resId); 206 if (textColor != null) { 207 mDayTextColor = textColor; 208 } 209 210 invalidate(); 211 } 212 213 public CharSequence getTitle() { 214 if (mTitle == null) { 215 mTitle = mTitleFormatter.format(mCalendar.getTime()); 216 } 217 return mTitle; 218 } 219 220 /** 221 * Sets up the text and style properties for painting. 222 */ 223 private void initPaints(Resources res) { 224 final String monthTypeface = res.getString(R.string.date_picker_month_typeface); 225 final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface); 226 final String dayTypeface = res.getString(R.string.date_picker_day_typeface); 227 228 final int monthTextSize = res.getDimensionPixelSize( 229 R.dimen.date_picker_month_text_size); 230 final int dayOfWeekTextSize = res.getDimensionPixelSize( 231 R.dimen.date_picker_day_of_week_text_size); 232 final int dayTextSize = res.getDimensionPixelSize( 233 R.dimen.date_picker_day_text_size); 234 235 mMonthPaint.setAntiAlias(true); 236 mMonthPaint.setTextSize(monthTextSize); 237 mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0)); 238 mMonthPaint.setTextAlign(Align.CENTER); 239 mMonthPaint.setStyle(Style.FILL); 240 241 mDayOfWeekPaint.setAntiAlias(true); 242 mDayOfWeekPaint.setTextSize(dayOfWeekTextSize); 243 mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0)); 244 mDayOfWeekPaint.setTextAlign(Align.CENTER); 245 mDayOfWeekPaint.setStyle(Style.FILL); 246 247 mDaySelectorPaint.setAntiAlias(true); 248 mDaySelectorPaint.setStyle(Style.FILL); 249 250 mDayHighlightPaint.setAntiAlias(true); 251 mDayHighlightPaint.setStyle(Style.FILL); 252 253 mDayPaint.setAntiAlias(true); 254 mDayPaint.setTextSize(dayTextSize); 255 mDayPaint.setTypeface(Typeface.create(dayTypeface, 0)); 256 mDayPaint.setTextAlign(Align.CENTER); 257 mDayPaint.setStyle(Style.FILL); 258 } 259 260 void setMonthTextColor(ColorStateList monthTextColor) { 261 final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0); 262 mMonthPaint.setColor(enabledColor); 263 invalidate(); 264 } 265 266 void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) { 267 final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0); 268 mDayOfWeekPaint.setColor(enabledColor); 269 invalidate(); 270 } 271 272 void setDayTextColor(ColorStateList dayTextColor) { 273 mDayTextColor = dayTextColor; 274 invalidate(); 275 } 276 277 void setDaySelectorColor(ColorStateList dayBackgroundColor) { 278 final int activatedColor = dayBackgroundColor.getColorForState( 279 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0); 280 mDaySelectorPaint.setColor(activatedColor); 281 invalidate(); 282 } 283 284 void setDayHighlightColor(ColorStateList dayHighlightColor) { 285 final int pressedColor = dayHighlightColor.getColorForState( 286 StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0); 287 mDayHighlightPaint.setColor(pressedColor); 288 invalidate(); 289 } 290 291 public void setOnDayClickListener(OnDayClickListener listener) { 292 mOnDayClickListener = listener; 293 } 294 295 @Override 296 public boolean dispatchHoverEvent(MotionEvent event) { 297 // First right-of-refusal goes the touch exploration helper. 298 return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); 299 } 300 301 @Override 302 public boolean onTouchEvent(MotionEvent event) { 303 switch (event.getAction()) { 304 case MotionEvent.ACTION_DOWN: 305 case MotionEvent.ACTION_MOVE: 306 final int touchedDay = getDayAtLocation(event.getX(), event.getY()); 307 if (mTouchedDay != touchedDay) { 308 mTouchedDay = touchedDay; 309 invalidate(); 310 } 311 break; 312 313 case MotionEvent.ACTION_UP: 314 final int clickedDay = getDayAtLocation(event.getX(), event.getY()); 315 onDayClicked(clickedDay); 316 // Fall through. 317 case MotionEvent.ACTION_CANCEL: 318 // Reset touched day on stream end. 319 mTouchedDay = -1; 320 invalidate(); 321 break; 322 } 323 return true; 324 } 325 326 @Override 327 protected void onDraw(Canvas canvas) { 328 final int paddingLeft = getPaddingLeft(); 329 final int paddingTop = getPaddingTop(); 330 canvas.translate(paddingLeft, paddingTop); 331 332 drawMonth(canvas); 333 drawDaysOfWeek(canvas); 334 drawDays(canvas); 335 336 canvas.translate(-paddingLeft, -paddingTop); 337 } 338 339 private void drawMonth(Canvas canvas) { 340 final float x = mPaddedWidth / 2f; 341 342 // Vertically centered within the month header height. 343 final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent(); 344 final float y = (mMonthHeight - lineHeight) / 2f; 345 346 canvas.drawText(getTitle().toString(), x, y, mMonthPaint); 347 } 348 349 private void drawDaysOfWeek(Canvas canvas) { 350 final float cellWidthHalf = mPaddedWidth / (DAYS_IN_WEEK * 2); 351 352 // Vertically centered within the cell height. 353 final float lineHeight = mDayOfWeekPaint.ascent() + mDayOfWeekPaint.descent(); 354 final float y = mMonthHeight + (mDayOfWeekHeight - lineHeight) / 2f; 355 356 for (int i = 0; i < DAYS_IN_WEEK; i++) { 357 final int calendarDay = (i + mWeekStart) % DAYS_IN_WEEK; 358 mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); 359 360 final String dayLabel = mDayOfWeekFormatter.format(mDayLabelCalendar.getTime()); 361 final float x = (2 * i + 1) * cellWidthHalf; 362 canvas.drawText(dayLabel, x, y, mDayOfWeekPaint); 363 } 364 } 365 366 /** 367 * Draws the month days. 368 */ 369 private void drawDays(Canvas canvas) { 370 final int cellWidthHalf = mPaddedWidth / (DAYS_IN_WEEK * 2); 371 372 // Vertically centered within the cell height. 373 final float halfLineHeight = (mDayPaint.ascent() + mDayPaint.descent()) / 2; 374 float centerY = mMonthHeight + mDayOfWeekHeight + mDayHeight / 2f; 375 376 for (int day = 1, j = findDayOffset(); day <= mDaysInMonth; day++) { 377 final int x = (2 * j + 1) * cellWidthHalf; 378 int stateMask = 0; 379 380 if (day >= mEnabledDayStart && day <= mEnabledDayEnd) { 381 stateMask |= StateSet.VIEW_STATE_ENABLED; 382 } 383 384 final boolean isDayActivated = mActivatedDay == day; 385 if (isDayActivated) { 386 stateMask |= StateSet.VIEW_STATE_ACTIVATED; 387 388 // Adjust the circle to be centered on the row. 389 canvas.drawCircle(x, centerY, mDaySelectorRadius, mDaySelectorPaint); 390 } else if (mTouchedDay == day) { 391 stateMask |= StateSet.VIEW_STATE_PRESSED; 392 393 // Adjust the circle to be centered on the row. 394 canvas.drawCircle(x, centerY, mDaySelectorRadius, mDayHighlightPaint); 395 } 396 397 final boolean isDayToday = mToday == day; 398 final int dayTextColor; 399 if (isDayToday && !isDayActivated) { 400 dayTextColor = mDaySelectorPaint.getColor(); 401 } else { 402 final int[] stateSet = StateSet.get(stateMask); 403 dayTextColor = mDayTextColor.getColorForState(stateSet, 0); 404 } 405 mDayPaint.setColor(dayTextColor); 406 407 canvas.drawText("" + day, x, centerY - halfLineHeight, mDayPaint); 408 409 j++; 410 411 if (j == DAYS_IN_WEEK) { 412 j = 0; 413 centerY += mDayHeight; 414 } 415 } 416 } 417 418 private static boolean isValidDayOfWeek(int day) { 419 return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY; 420 } 421 422 private static boolean isValidMonth(int month) { 423 return month >= Calendar.JANUARY && month <= Calendar.DECEMBER; 424 } 425 426 /** 427 * Sets the selected day. 428 * 429 * @param dayOfMonth the selected day of the month, or {@code -1} to clear 430 * the selection 431 */ 432 public void setSelectedDay(int dayOfMonth) { 433 mActivatedDay = dayOfMonth; 434 435 // Invalidate cached accessibility information. 436 mTouchHelper.invalidateRoot(); 437 invalidate(); 438 } 439 440 /** 441 * Sets the first day of the week. 442 * 443 * @param weekStart which day the week should start on, valid values are 444 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY} 445 */ 446 public void setFirstDayOfWeek(int weekStart) { 447 if (isValidDayOfWeek(weekStart)) { 448 mWeekStart = weekStart; 449 } else { 450 mWeekStart = mCalendar.getFirstDayOfWeek(); 451 } 452 453 // Invalidate cached accessibility information. 454 mTouchHelper.invalidateRoot(); 455 invalidate(); 456 } 457 458 /** 459 * Sets all the parameters for displaying this week. 460 * <p> 461 * Parameters have a default value and will only update if a new value is 462 * included, except for focus month, which will always default to no focus 463 * month if no value is passed in. The only required parameter is the week 464 * start. 465 * 466 * @param selectedDay the selected day of the month, or -1 for no selection 467 * @param month the month 468 * @param year the year 469 * @param weekStart which day the week should start on, valid values are 470 * {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY} 471 * @param enabledDayStart the first enabled day 472 * @param enabledDayEnd the last enabled day 473 */ 474 void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart, 475 int enabledDayEnd) { 476 mActivatedDay = selectedDay; 477 478 if (isValidMonth(month)) { 479 mMonth = month; 480 } 481 mYear = year; 482 483 mCalendar.set(Calendar.MONTH, mMonth); 484 mCalendar.set(Calendar.YEAR, mYear); 485 mCalendar.set(Calendar.DAY_OF_MONTH, 1); 486 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); 487 488 if (isValidDayOfWeek(weekStart)) { 489 mWeekStart = weekStart; 490 } else { 491 mWeekStart = mCalendar.getFirstDayOfWeek(); 492 } 493 494 if (enabledDayStart > 0 && enabledDayEnd < 32) { 495 mEnabledDayStart = enabledDayStart; 496 } 497 if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) { 498 mEnabledDayEnd = enabledDayEnd; 499 } 500 501 // Figure out what day today is. 502 final Calendar today = Calendar.getInstance(); 503 mToday = -1; 504 mDaysInMonth = getDaysInMonth(mMonth, mYear); 505 for (int i = 0; i < mDaysInMonth; i++) { 506 final int day = i + 1; 507 if (sameDay(day, today)) { 508 mToday = day; 509 } 510 } 511 mNumWeeks = calculateNumRows(); 512 513 // Invalidate the old title. 514 mTitle = null; 515 516 // Invalidate cached accessibility information. 517 mTouchHelper.invalidateRoot(); 518 } 519 520 private static int getDaysInMonth(int month, int year) { 521 switch (month) { 522 case Calendar.JANUARY: 523 case Calendar.MARCH: 524 case Calendar.MAY: 525 case Calendar.JULY: 526 case Calendar.AUGUST: 527 case Calendar.OCTOBER: 528 case Calendar.DECEMBER: 529 return 31; 530 case Calendar.APRIL: 531 case Calendar.JUNE: 532 case Calendar.SEPTEMBER: 533 case Calendar.NOVEMBER: 534 return 30; 535 case Calendar.FEBRUARY: 536 return (year % 4 == 0) ? 29 : 28; 537 default: 538 throw new IllegalArgumentException("Invalid Month"); 539 } 540 } 541 542 public void reuse() { 543 mNumWeeks = MAX_WEEKS_IN_MONTH; 544 requestLayout(); 545 } 546 547 private int calculateNumRows() { 548 final int offset = findDayOffset(); 549 final int dividend = (offset + mDaysInMonth) / DAYS_IN_WEEK; 550 final int remainder = (offset + mDaysInMonth) % DAYS_IN_WEEK; 551 return dividend + (remainder > 0 ? 1 : 0); 552 } 553 554 private boolean sameDay(int day, Calendar today) { 555 return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH) 556 && day == today.get(Calendar.DAY_OF_MONTH); 557 } 558 559 @Override 560 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 561 final int preferredHeight = mDayHeight * mNumWeeks + mDayOfWeekHeight + mMonthHeight 562 + getPaddingTop() + getPaddingBottom(); 563 final int preferredWidth = mCellWidth * DAYS_IN_WEEK 564 + getPaddingStart() + getPaddingEnd(); 565 final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec); 566 final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec); 567 setMeasuredDimension(resolvedWidth, resolvedHeight); 568 } 569 570 @Override 571 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 572 mPaddedWidth = w - getPaddingLeft() - getPaddingRight(); 573 mPaddedHeight = w - getPaddingTop() - getPaddingBottom(); 574 575 // Invalidate cached accessibility information. 576 mTouchHelper.invalidateRoot(); 577 } 578 579 private int findDayOffset() { 580 final int offset = mDayOfWeekStart - mWeekStart; 581 if (mDayOfWeekStart < mWeekStart) { 582 return offset + DAYS_IN_WEEK; 583 } 584 return offset; 585 } 586 587 /** 588 * Calculates the day of the month at the specified touch position. Returns 589 * the day of the month or -1 if the position wasn't in a valid day. 590 * 591 * @param x the x position of the touch event 592 * @param y the y position of the touch event 593 * @return the day of the month at (x, y) or -1 if the position wasn't in a 594 * valid day 595 */ 596 private int getDayAtLocation(float x, float y) { 597 final int paddedX = (int) (x - getPaddingLeft() + 0.5f); 598 if (paddedX < 0 || paddedX >= mPaddedWidth) { 599 return -1; 600 } 601 602 final int headerHeight = mMonthHeight + mDayOfWeekHeight; 603 final int paddedY = (int) (y - getPaddingTop() + 0.5f); 604 if (paddedY < headerHeight || paddedY >= mPaddedHeight) { 605 return -1; 606 } 607 608 final int row = (paddedY - headerHeight) / mDayHeight; 609 final int col = (paddedX * DAYS_IN_WEEK) / mPaddedWidth; 610 final int index = col + row * DAYS_IN_WEEK; 611 final int day = index + 1 - findDayOffset(); 612 if (day < 1 || day > mDaysInMonth) { 613 return -1; 614 } 615 616 return day; 617 } 618 619 /** 620 * Calculates the bounds of the specified day. 621 * 622 * @param day the day of the month 623 * @param outBounds the rect to populate with bounds 624 */ 625 private boolean getBoundsForDay(int day, Rect outBounds) { 626 if (day < 1 || day > mDaysInMonth) { 627 return false; 628 } 629 630 final int index = day - 1 + findDayOffset(); 631 final int row = index / DAYS_IN_WEEK; 632 final int col = index % DAYS_IN_WEEK; 633 634 final int headerHeight = mMonthHeight + mDayOfWeekHeight; 635 final int paddedY = row * mDayHeight + headerHeight; 636 final int paddedX = col * mPaddedWidth; 637 638 final int y = paddedY + getPaddingTop(); 639 final int x = paddedX + getPaddingLeft(); 640 641 final int cellHeight = mDayHeight; 642 final int cellWidth = mPaddedWidth / DAYS_IN_WEEK; 643 outBounds.set(x, y, (x + cellWidth), (y + cellHeight)); 644 645 return true; 646 } 647 648 /** 649 * Called when the user clicks on a day. Handles callbacks to the 650 * {@link OnDayClickListener} if one is set. 651 * 652 * @param day the day that was clicked 653 */ 654 private void onDayClicked(int day) { 655 if (mOnDayClickListener != null) { 656 Calendar date = Calendar.getInstance(); 657 date.set(mYear, mMonth, day); 658 mOnDayClickListener.onDayClick(this, date); 659 } 660 661 // This is a no-op if accessibility is turned off. 662 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); 663 } 664 665 /** 666 * Provides a virtual view hierarchy for interfacing with an accessibility 667 * service. 668 */ 669 private class MonthViewTouchHelper extends ExploreByTouchHelper { 670 private static final String DATE_FORMAT = "dd MMMM yyyy"; 671 672 private final Rect mTempRect = new Rect(); 673 private final Calendar mTempCalendar = Calendar.getInstance(); 674 675 public MonthViewTouchHelper(View host) { 676 super(host); 677 } 678 679 @Override 680 protected int getVirtualViewAt(float x, float y) { 681 final int day = getDayAtLocation(x, y); 682 if (day >= 0) { 683 return day; 684 } 685 return ExploreByTouchHelper.INVALID_ID; 686 } 687 688 @Override 689 protected void getVisibleVirtualViews(IntArray virtualViewIds) { 690 for (int day = 1; day <= mDaysInMonth; day++) { 691 virtualViewIds.add(day); 692 } 693 } 694 695 @Override 696 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 697 event.setContentDescription(getItemDescription(virtualViewId)); 698 } 699 700 @Override 701 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { 702 final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect); 703 704 if (!hasBounds) { 705 // The day is invalid, kill the node. 706 mTempRect.setEmpty(); 707 node.setContentDescription(""); 708 node.setBoundsInParent(mTempRect); 709 node.setVisibleToUser(false); 710 return; 711 } 712 713 node.setContentDescription(getItemDescription(virtualViewId)); 714 node.setBoundsInParent(mTempRect); 715 node.addAction(AccessibilityAction.ACTION_CLICK); 716 717 if (virtualViewId == mActivatedDay) { 718 node.setSelected(true); 719 } 720 721 } 722 723 @Override 724 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 725 Bundle arguments) { 726 switch (action) { 727 case AccessibilityNodeInfo.ACTION_CLICK: 728 onDayClicked(virtualViewId); 729 return true; 730 } 731 732 return false; 733 } 734 735 /** 736 * Generates a description for a given time object. Since this 737 * description will be spoken, the components are ordered by descending 738 * specificity as DAY MONTH YEAR. 739 * 740 * @param day The day to generate a description for 741 * @return A description of the time object 742 */ 743 private CharSequence getItemDescription(int day) { 744 mTempCalendar.set(mYear, mMonth, day); 745 final CharSequence date = DateFormat.format(DATE_FORMAT, 746 mTempCalendar.getTimeInMillis()); 747 748 if (day == mActivatedDay) { 749 return getContext().getString(R.string.item_is_selected, date); 750 } 751 752 return date; 753 } 754 } 755 756 /** 757 * Handles callbacks when the user clicks on a time object. 758 */ 759 public interface OnDayClickListener { 760 public void onDayClick(SimpleMonthView view, Calendar day); 761 } 762} 763