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 17 package android.widget; 18 19 import android.content.Context; 20 import android.content.res.ColorStateList; 21 import android.content.res.Configuration; 22 import android.content.res.Resources; 23 import android.graphics.Canvas; 24 import android.graphics.Paint; 25 import android.graphics.Paint.Align; 26 import android.graphics.Paint.Style; 27 import android.graphics.Rect; 28 import android.graphics.Typeface; 29 import android.os.Bundle; 30 import android.text.format.DateFormat; 31 import android.text.format.DateUtils; 32 import android.text.format.Time; 33 import android.util.AttributeSet; 34 import android.util.MathUtils; 35 import android.view.MotionEvent; 36 import android.view.View; 37 import android.view.accessibility.AccessibilityEvent; 38 import android.view.accessibility.AccessibilityNodeInfo; 39 40 import com.android.internal.R; 41 import com.android.internal.widget.ExploreByTouchHelper; 42 43 import java.text.SimpleDateFormat; 44 import java.util.Calendar; 45 import java.util.Formatter; 46 import java.util.List; 47 import java.util.Locale; 48 49 /** 50 * A calendar-like view displaying a specified month and the appropriate selectable day numbers 51 * within the specified month. 52 */ 53 class SimpleMonthView extends View { 54 private static final String TAG = "SimpleMonthView"; 55 56 private static final int DEFAULT_HEIGHT = 32; 57 private static final int MIN_HEIGHT = 10; 58 59 private static final int DEFAULT_SELECTED_DAY = -1; 60 private static final int DEFAULT_WEEK_START = Calendar.SUNDAY; 61 private static final int DEFAULT_NUM_DAYS = 7; 62 private static final int DEFAULT_NUM_ROWS = 6; 63 private static final int MAX_NUM_ROWS = 6; 64 65 private static final int SELECTED_CIRCLE_ALPHA = 60; 66 67 private static final int DAY_SEPARATOR_WIDTH = 1; 68 69 private final int mMiniDayNumberTextSize; 70 private final int mMonthLabelTextSize; 71 private final int mMonthDayLabelTextSize; 72 private final int mMonthHeaderSize; 73 private final int mDaySelectedCircleSize; 74 75 // used for scaling to the device density 76 private static float mScale = 0; 77 78 /** Single-letter (when available) formatter for the day of week label. */ 79 private SimpleDateFormat mDayFormatter = new SimpleDateFormat("EEEEE", Locale.getDefault()); 80 81 // affects the padding on the sides of this view 82 private int mPadding = 0; 83 84 private String mDayOfWeekTypeface; 85 private String mMonthTitleTypeface; 86 87 private Paint mDayNumberPaint; 88 private Paint mDayNumberDisabledPaint; 89 private Paint mDayNumberSelectedPaint; 90 91 private Paint mMonthTitlePaint; 92 private Paint mMonthDayLabelPaint; 93 94 private final Formatter mFormatter; 95 private final StringBuilder mStringBuilder; 96 97 private int mMonth; 98 private int mYear; 99 100 // Quick reference to the width of this view, matches parent 101 private int mWidth; 102 103 // The height this view should draw at in pixels, set by height param 104 private int mRowHeight = DEFAULT_HEIGHT; 105 106 // If this view contains the today 107 private boolean mHasToday = false; 108 109 // Which day is selected [0-6] or -1 if no day is selected 110 private int mSelectedDay = -1; 111 112 // Which day is today [0-6] or -1 if no day is today 113 private int mToday = DEFAULT_SELECTED_DAY; 114 115 // Which day of the week to start on [0-6] 116 private int mWeekStart = DEFAULT_WEEK_START; 117 118 // How many days to display 119 private int mNumDays = DEFAULT_NUM_DAYS; 120 121 // The number of days + a spot for week number if it is displayed 122 private int mNumCells = mNumDays; 123 124 private int mDayOfWeekStart = 0; 125 126 // First enabled day 127 private int mEnabledDayStart = 1; 128 129 // Last enabled day 130 private int mEnabledDayEnd = 31; 131 132 private final Calendar mCalendar = Calendar.getInstance(); 133 private final Calendar mDayLabelCalendar = Calendar.getInstance(); 134 135 private final MonthViewTouchHelper mTouchHelper; 136 137 private int mNumRows = DEFAULT_NUM_ROWS; 138 139 // Optional listener for handling day click actions 140 private OnDayClickListener mOnDayClickListener; 141 142 // Whether to prevent setting the accessibility delegate 143 private boolean mLockAccessibilityDelegate; 144 145 private int mNormalTextColor; 146 private int mDisabledTextColor; 147 private int mSelectedDayColor; 148 SimpleMonthView(Context context)149 public SimpleMonthView(Context context) { 150 this(context, null); 151 } 152 SimpleMonthView(Context context, AttributeSet attrs)153 public SimpleMonthView(Context context, AttributeSet attrs) { 154 this(context, attrs, R.attr.datePickerStyle); 155 } 156 SimpleMonthView(Context context, AttributeSet attrs, int defStyle)157 public SimpleMonthView(Context context, AttributeSet attrs, int defStyle) { 158 super(context, attrs); 159 160 final Resources res = context.getResources(); 161 162 mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface); 163 mMonthTitleTypeface = res.getString(R.string.sans_serif); 164 165 mStringBuilder = new StringBuilder(50); 166 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 167 168 mMiniDayNumberTextSize = res.getDimensionPixelSize(R.dimen.datepicker_day_number_size); 169 mMonthLabelTextSize = res.getDimensionPixelSize(R.dimen.datepicker_month_label_size); 170 mMonthDayLabelTextSize = res.getDimensionPixelSize( 171 R.dimen.datepicker_month_day_label_text_size); 172 mMonthHeaderSize = res.getDimensionPixelOffset( 173 R.dimen.datepicker_month_list_item_header_height); 174 mDaySelectedCircleSize = res.getDimensionPixelSize( 175 R.dimen.datepicker_day_number_select_circle_radius); 176 177 mRowHeight = (res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height) 178 - mMonthHeaderSize) / MAX_NUM_ROWS; 179 180 // Set up accessibility components. 181 mTouchHelper = new MonthViewTouchHelper(this); 182 setAccessibilityDelegate(mTouchHelper); 183 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 184 mLockAccessibilityDelegate = true; 185 186 // Sets up any standard paints that will be used 187 initView(); 188 } 189 190 @Override onConfigurationChanged(Configuration newConfig)191 protected void onConfigurationChanged(Configuration newConfig) { 192 super.onConfigurationChanged(newConfig); 193 194 mDayFormatter = new SimpleDateFormat("EEEEE", newConfig.locale); 195 } 196 setTextColor(ColorStateList colors)197 void setTextColor(ColorStateList colors) { 198 final Resources res = getContext().getResources(); 199 200 mNormalTextColor = colors.getColorForState(ENABLED_STATE_SET, 201 res.getColor(R.color.datepicker_default_normal_text_color_holo_light)); 202 mMonthTitlePaint.setColor(mNormalTextColor); 203 mMonthDayLabelPaint.setColor(mNormalTextColor); 204 205 mDisabledTextColor = colors.getColorForState(EMPTY_STATE_SET, 206 res.getColor(R.color.datepicker_default_disabled_text_color_holo_light)); 207 mDayNumberDisabledPaint.setColor(mDisabledTextColor); 208 209 mSelectedDayColor = colors.getColorForState(ENABLED_SELECTED_STATE_SET, 210 res.getColor(R.color.holo_blue_light)); 211 mDayNumberSelectedPaint.setColor(mSelectedDayColor); 212 mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); 213 } 214 215 @Override setAccessibilityDelegate(AccessibilityDelegate delegate)216 public void setAccessibilityDelegate(AccessibilityDelegate delegate) { 217 // Workaround for a JB MR1 issue where accessibility delegates on 218 // top-level ListView items are overwritten. 219 if (!mLockAccessibilityDelegate) { 220 super.setAccessibilityDelegate(delegate); 221 } 222 } 223 setOnDayClickListener(OnDayClickListener listener)224 public void setOnDayClickListener(OnDayClickListener listener) { 225 mOnDayClickListener = listener; 226 } 227 228 @Override dispatchHoverEvent(MotionEvent event)229 public boolean dispatchHoverEvent(MotionEvent event) { 230 // First right-of-refusal goes the touch exploration helper. 231 if (mTouchHelper.dispatchHoverEvent(event)) { 232 return true; 233 } 234 return super.dispatchHoverEvent(event); 235 } 236 237 @Override onTouchEvent(MotionEvent event)238 public boolean onTouchEvent(MotionEvent event) { 239 switch (event.getAction()) { 240 case MotionEvent.ACTION_UP: 241 final int day = getDayFromLocation(event.getX(), event.getY()); 242 if (day >= 0) { 243 onDayClick(day); 244 } 245 break; 246 } 247 return true; 248 } 249 250 /** 251 * Sets up the text and style properties for painting. 252 */ initView()253 private void initView() { 254 mMonthTitlePaint = new Paint(); 255 mMonthTitlePaint.setAntiAlias(true); 256 mMonthTitlePaint.setColor(mNormalTextColor); 257 mMonthTitlePaint.setTextSize(mMonthLabelTextSize); 258 mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD)); 259 mMonthTitlePaint.setTextAlign(Align.CENTER); 260 mMonthTitlePaint.setStyle(Style.FILL); 261 mMonthTitlePaint.setFakeBoldText(true); 262 263 mMonthDayLabelPaint = new Paint(); 264 mMonthDayLabelPaint.setAntiAlias(true); 265 mMonthDayLabelPaint.setColor(mNormalTextColor); 266 mMonthDayLabelPaint.setTextSize(mMonthDayLabelTextSize); 267 mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL)); 268 mMonthDayLabelPaint.setTextAlign(Align.CENTER); 269 mMonthDayLabelPaint.setStyle(Style.FILL); 270 mMonthDayLabelPaint.setFakeBoldText(true); 271 272 mDayNumberSelectedPaint = new Paint(); 273 mDayNumberSelectedPaint.setAntiAlias(true); 274 mDayNumberSelectedPaint.setColor(mSelectedDayColor); 275 mDayNumberSelectedPaint.setAlpha(SELECTED_CIRCLE_ALPHA); 276 mDayNumberSelectedPaint.setTextAlign(Align.CENTER); 277 mDayNumberSelectedPaint.setStyle(Style.FILL); 278 mDayNumberSelectedPaint.setFakeBoldText(true); 279 280 mDayNumberPaint = new Paint(); 281 mDayNumberPaint.setAntiAlias(true); 282 mDayNumberPaint.setTextSize(mMiniDayNumberTextSize); 283 mDayNumberPaint.setTextAlign(Align.CENTER); 284 mDayNumberPaint.setStyle(Style.FILL); 285 mDayNumberPaint.setFakeBoldText(false); 286 287 mDayNumberDisabledPaint = new Paint(); 288 mDayNumberDisabledPaint.setAntiAlias(true); 289 mDayNumberDisabledPaint.setColor(mDisabledTextColor); 290 mDayNumberDisabledPaint.setTextSize(mMiniDayNumberTextSize); 291 mDayNumberDisabledPaint.setTextAlign(Align.CENTER); 292 mDayNumberDisabledPaint.setStyle(Style.FILL); 293 mDayNumberDisabledPaint.setFakeBoldText(false); 294 } 295 296 @Override onDraw(Canvas canvas)297 protected void onDraw(Canvas canvas) { 298 drawMonthTitle(canvas); 299 drawWeekDayLabels(canvas); 300 drawDays(canvas); 301 } 302 isValidDayOfWeek(int day)303 private static boolean isValidDayOfWeek(int day) { 304 return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY; 305 } 306 isValidMonth(int month)307 private static boolean isValidMonth(int month) { 308 return month >= Calendar.JANUARY && month <= Calendar.DECEMBER; 309 } 310 311 /** 312 * Sets all the parameters for displaying this week. Parameters have a default value and 313 * will only update if a new value is included, except for focus month, which will always 314 * default to no focus month if no value is passed in. The only required parameter is the 315 * week start. 316 * 317 * @param selectedDay the selected day of the month, or -1 for no selection. 318 * @param month the month. 319 * @param year the year. 320 * @param weekStart which day the week should start on. {@link Calendar#SUNDAY} through 321 * {@link Calendar#SATURDAY}. 322 * @param enabledDayStart the first enabled day. 323 * @param enabledDayEnd the last enabled day. 324 */ setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart, int enabledDayEnd)325 void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart, 326 int enabledDayEnd) { 327 if (mRowHeight < MIN_HEIGHT) { 328 mRowHeight = MIN_HEIGHT; 329 } 330 331 mSelectedDay = selectedDay; 332 333 if (isValidMonth(month)) { 334 mMonth = month; 335 } 336 mYear = year; 337 338 // Figure out what day today is 339 final Time today = new Time(Time.getCurrentTimezone()); 340 today.setToNow(); 341 mHasToday = false; 342 mToday = -1; 343 344 mCalendar.set(Calendar.MONTH, mMonth); 345 mCalendar.set(Calendar.YEAR, mYear); 346 mCalendar.set(Calendar.DAY_OF_MONTH, 1); 347 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); 348 349 if (isValidDayOfWeek(weekStart)) { 350 mWeekStart = weekStart; 351 } else { 352 mWeekStart = mCalendar.getFirstDayOfWeek(); 353 } 354 355 if (enabledDayStart > 0 && enabledDayEnd < 32) { 356 mEnabledDayStart = enabledDayStart; 357 } 358 if (enabledDayEnd > 0 && enabledDayEnd < 32 && enabledDayEnd >= enabledDayStart) { 359 mEnabledDayEnd = enabledDayEnd; 360 } 361 362 mNumCells = getDaysInMonth(mMonth, mYear); 363 for (int i = 0; i < mNumCells; i++) { 364 final int day = i + 1; 365 if (sameDay(day, today)) { 366 mHasToday = true; 367 mToday = day; 368 } 369 } 370 mNumRows = calculateNumRows(); 371 372 // Invalidate cached accessibility information. 373 mTouchHelper.invalidateRoot(); 374 } 375 getDaysInMonth(int month, int year)376 private static int getDaysInMonth(int month, int year) { 377 switch (month) { 378 case Calendar.JANUARY: 379 case Calendar.MARCH: 380 case Calendar.MAY: 381 case Calendar.JULY: 382 case Calendar.AUGUST: 383 case Calendar.OCTOBER: 384 case Calendar.DECEMBER: 385 return 31; 386 case Calendar.APRIL: 387 case Calendar.JUNE: 388 case Calendar.SEPTEMBER: 389 case Calendar.NOVEMBER: 390 return 30; 391 case Calendar.FEBRUARY: 392 return (year % 4 == 0) ? 29 : 28; 393 default: 394 throw new IllegalArgumentException("Invalid Month"); 395 } 396 } 397 reuse()398 public void reuse() { 399 mNumRows = DEFAULT_NUM_ROWS; 400 requestLayout(); 401 } 402 calculateNumRows()403 private int calculateNumRows() { 404 int offset = findDayOffset(); 405 int dividend = (offset + mNumCells) / mNumDays; 406 int remainder = (offset + mNumCells) % mNumDays; 407 return (dividend + (remainder > 0 ? 1 : 0)); 408 } 409 sameDay(int day, Time today)410 private boolean sameDay(int day, Time today) { 411 return mYear == today.year && 412 mMonth == today.month && 413 day == today.monthDay; 414 } 415 416 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)417 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 418 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows 419 + mMonthHeaderSize); 420 } 421 422 @Override onSizeChanged(int w, int h, int oldw, int oldh)423 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 424 mWidth = w; 425 426 // Invalidate cached accessibility information. 427 mTouchHelper.invalidateRoot(); 428 } 429 getMonthAndYearString()430 private String getMonthAndYearString() { 431 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR 432 | DateUtils.FORMAT_NO_MONTH_DAY; 433 mStringBuilder.setLength(0); 434 long millis = mCalendar.getTimeInMillis(); 435 return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, 436 Time.getCurrentTimezone()).toString(); 437 } 438 drawMonthTitle(Canvas canvas)439 private void drawMonthTitle(Canvas canvas) { 440 final float x = (mWidth + 2 * mPadding) / 2f; 441 final float y = (mMonthHeaderSize - mMonthDayLabelTextSize) / 2f; 442 canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); 443 } 444 drawWeekDayLabels(Canvas canvas)445 private void drawWeekDayLabels(Canvas canvas) { 446 final int y = mMonthHeaderSize - (mMonthDayLabelTextSize / 2); 447 final int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 448 449 for (int i = 0; i < mNumDays; i++) { 450 final int calendarDay = (i + mWeekStart) % mNumDays; 451 mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); 452 453 final String dayLabel = mDayFormatter.format(mDayLabelCalendar.getTime()); 454 final int x = (2 * i + 1) * dayWidthHalf + mPadding; 455 canvas.drawText(dayLabel, x, y, mMonthDayLabelPaint); 456 } 457 } 458 459 /** 460 * Draws the month days. 461 */ drawDays(Canvas canvas)462 private void drawDays(Canvas canvas) { 463 int y = (((mRowHeight + mMiniDayNumberTextSize) / 2) - DAY_SEPARATOR_WIDTH) 464 + mMonthHeaderSize; 465 int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 466 int j = findDayOffset(); 467 for (int day = 1; day <= mNumCells; day++) { 468 int x = (2 * j + 1) * dayWidthHalf + mPadding; 469 if (mSelectedDay == day) { 470 canvas.drawCircle(x, y - (mMiniDayNumberTextSize / 3), mDaySelectedCircleSize, 471 mDayNumberSelectedPaint); 472 } 473 474 if (mHasToday && mToday == day) { 475 mDayNumberPaint.setColor(mSelectedDayColor); 476 } else { 477 mDayNumberPaint.setColor(mNormalTextColor); 478 } 479 final Paint paint = (day < mEnabledDayStart || day > mEnabledDayEnd) ? 480 mDayNumberDisabledPaint : mDayNumberPaint; 481 canvas.drawText(String.format("%d", day), x, y, paint); 482 j++; 483 if (j == mNumDays) { 484 j = 0; 485 y += mRowHeight; 486 } 487 } 488 } 489 findDayOffset()490 private int findDayOffset() { 491 return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) 492 - mWeekStart; 493 } 494 495 /** 496 * Calculates the day that the given x position is in, accounting for week 497 * number. Returns the day or -1 if the position wasn't in a day. 498 * 499 * @param x The x position of the touch event 500 * @return The day number, or -1 if the position wasn't in a day 501 */ getDayFromLocation(float x, float y)502 private int getDayFromLocation(float x, float y) { 503 int dayStart = mPadding; 504 if (x < dayStart || x > mWidth - mPadding) { 505 return -1; 506 } 507 // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels 508 int row = (int) (y - mMonthHeaderSize) / mRowHeight; 509 int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); 510 511 int day = column - findDayOffset() + 1; 512 day += row * mNumDays; 513 if (day < 1 || day > mNumCells) { 514 return -1; 515 } 516 return day; 517 } 518 519 /** 520 * Called when the user clicks on a day. Handles callbacks to the 521 * {@link OnDayClickListener} if one is set. 522 * 523 * @param day The day that was clicked 524 */ onDayClick(int day)525 private void onDayClick(int day) { 526 if (mOnDayClickListener != null) { 527 Calendar date = Calendar.getInstance(); 528 date.set(mYear, mMonth, day); 529 mOnDayClickListener.onDayClick(this, date); 530 } 531 532 // This is a no-op if accessibility is turned off. 533 mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); 534 } 535 536 /** 537 * @return The date that has accessibility focus, or {@code null} if no date 538 * has focus 539 */ getAccessibilityFocus()540 Calendar getAccessibilityFocus() { 541 final int day = mTouchHelper.getFocusedVirtualView(); 542 Calendar date = null; 543 if (day >= 0) { 544 date = Calendar.getInstance(); 545 date.set(mYear, mMonth, day); 546 } 547 return date; 548 } 549 550 /** 551 * Clears accessibility focus within the view. No-op if the view does not 552 * contain accessibility focus. 553 */ clearAccessibilityFocus()554 public void clearAccessibilityFocus() { 555 mTouchHelper.clearFocusedVirtualView(); 556 } 557 558 /** 559 * Attempts to restore accessibility focus to the specified date. 560 * 561 * @param day The date which should receive focus 562 * @return {@code false} if the date is not valid for this month view, or 563 * {@code true} if the date received focus 564 */ restoreAccessibilityFocus(Calendar day)565 boolean restoreAccessibilityFocus(Calendar day) { 566 if ((day.get(Calendar.YEAR) != mYear) || (day.get(Calendar.MONTH) != mMonth) || 567 (day.get(Calendar.DAY_OF_MONTH) > mNumCells)) { 568 return false; 569 } 570 mTouchHelper.setFocusedVirtualView(day.get(Calendar.DAY_OF_MONTH)); 571 return true; 572 } 573 574 /** 575 * Provides a virtual view hierarchy for interfacing with an accessibility 576 * service. 577 */ 578 private class MonthViewTouchHelper extends ExploreByTouchHelper { 579 private static final String DATE_FORMAT = "dd MMMM yyyy"; 580 581 private final Rect mTempRect = new Rect(); 582 private final Calendar mTempCalendar = Calendar.getInstance(); 583 MonthViewTouchHelper(View host)584 public MonthViewTouchHelper(View host) { 585 super(host); 586 } 587 setFocusedVirtualView(int virtualViewId)588 public void setFocusedVirtualView(int virtualViewId) { 589 getAccessibilityNodeProvider(SimpleMonthView.this).performAction( 590 virtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 591 } 592 clearFocusedVirtualView()593 public void clearFocusedVirtualView() { 594 final int focusedVirtualView = getFocusedVirtualView(); 595 if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { 596 getAccessibilityNodeProvider(SimpleMonthView.this).performAction( 597 focusedVirtualView, 598 AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, 599 null); 600 } 601 } 602 603 @Override getVirtualViewAt(float x, float y)604 protected int getVirtualViewAt(float x, float y) { 605 final int day = getDayFromLocation(x, y); 606 if (day >= 0) { 607 return day; 608 } 609 return ExploreByTouchHelper.INVALID_ID; 610 } 611 612 @Override getVisibleVirtualViews(List<Integer> virtualViewIds)613 protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { 614 for (int day = 1; day <= mNumCells; day++) { 615 virtualViewIds.add(day); 616 } 617 } 618 619 @Override onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event)620 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 621 event.setContentDescription(getItemDescription(virtualViewId)); 622 } 623 624 @Override onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node)625 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { 626 getItemBounds(virtualViewId, mTempRect); 627 628 node.setContentDescription(getItemDescription(virtualViewId)); 629 node.setBoundsInParent(mTempRect); 630 node.addAction(AccessibilityNodeInfo.ACTION_CLICK); 631 632 if (virtualViewId == mSelectedDay) { 633 node.setSelected(true); 634 } 635 636 } 637 638 @Override onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments)639 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 640 Bundle arguments) { 641 switch (action) { 642 case AccessibilityNodeInfo.ACTION_CLICK: 643 onDayClick(virtualViewId); 644 return true; 645 } 646 647 return false; 648 } 649 650 /** 651 * Calculates the bounding rectangle of a given time object. 652 * 653 * @param day The day to calculate bounds for 654 * @param rect The rectangle in which to store the bounds 655 */ getItemBounds(int day, Rect rect)656 private void getItemBounds(int day, Rect rect) { 657 final int offsetX = mPadding; 658 final int offsetY = mMonthHeaderSize; 659 final int cellHeight = mRowHeight; 660 final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays); 661 final int index = ((day - 1) + findDayOffset()); 662 final int row = (index / mNumDays); 663 final int column = (index % mNumDays); 664 final int x = (offsetX + (column * cellWidth)); 665 final int y = (offsetY + (row * cellHeight)); 666 667 rect.set(x, y, (x + cellWidth), (y + cellHeight)); 668 } 669 670 /** 671 * Generates a description for a given time object. Since this 672 * description will be spoken, the components are ordered by descending 673 * specificity as DAY MONTH YEAR. 674 * 675 * @param day The day to generate a description for 676 * @return A description of the time object 677 */ getItemDescription(int day)678 private CharSequence getItemDescription(int day) { 679 mTempCalendar.set(mYear, mMonth, day); 680 final CharSequence date = DateFormat.format(DATE_FORMAT, 681 mTempCalendar.getTimeInMillis()); 682 683 if (day == mSelectedDay) { 684 return getContext().getString(R.string.item_is_selected, date); 685 } 686 687 return date; 688 } 689 } 690 691 /** 692 * Handles callbacks when the user clicks on a time object. 693 */ 694 public interface OnDayClickListener { onDayClick(SimpleMonthView view, Calendar day)695 public void onDayClick(SimpleMonthView view, Calendar day); 696 } 697 } 698