SimpleMonthView.java revision cb3f2522609186db6239ad154af275957118295c
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.datetimepicker.date; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.graphics.Canvas; 22import android.graphics.Paint; 23import android.graphics.Paint.Align; 24import android.graphics.Paint.Style; 25import android.graphics.Rect; 26import android.graphics.Typeface; 27import android.os.Bundle; 28import android.support.v4.view.ViewCompat; 29import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 30import android.text.format.DateFormat; 31import android.text.format.DateUtils; 32import android.text.format.Time; 33import android.util.SparseArray; 34import android.view.MotionEvent; 35import android.view.View; 36import android.view.accessibility.AccessibilityEvent; 37import android.view.accessibility.AccessibilityNodeInfo; 38 39import com.android.datetimepicker.R; 40import com.android.datetimepicker.Utils; 41import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay; 42import com.googlecode.eyesfree.utils.TouchExplorationHelper; 43 44import java.security.InvalidParameterException; 45import java.util.Calendar; 46import java.util.Formatter; 47import java.util.HashMap; 48import java.util.List; 49import java.util.Locale; 50 51/** 52 * A calendar-like view displaying a specified month and the appropriate selectable day numbers 53 * within the specified month. 54 */ 55public class SimpleMonthView extends View { 56 private static final String TAG = "SimpleMonthView"; 57 58 /** 59 * These params can be passed into the view to control how it appears. 60 * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default 61 * values are unlikely to fit most layouts correctly. 62 */ 63 /** 64 * This sets the height of this week in pixels 65 */ 66 public static final String VIEW_PARAMS_HEIGHT = "height"; 67 /** 68 * This specifies the position (or weeks since the epoch) of this week, 69 * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay} 70 */ 71 public static final String VIEW_PARAMS_MONTH = "month"; 72 /** 73 * This specifies the position (or weeks since the epoch) of this week, 74 * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay} 75 */ 76 public static final String VIEW_PARAMS_YEAR = "year"; 77 /** 78 * This sets one of the days in this view as selected {@link Time#SUNDAY} 79 * through {@link Time#SATURDAY}. 80 */ 81 public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; 82 /** 83 * Which day the week should start on. {@link Time#SUNDAY} through 84 * {@link Time#SATURDAY}. 85 */ 86 public static final String VIEW_PARAMS_WEEK_START = "week_start"; 87 /** 88 * How many days to display at a time. Days will be displayed starting with 89 * {@link #mWeekStart}. 90 */ 91 public static final String VIEW_PARAMS_NUM_DAYS = "num_days"; 92 /** 93 * Which month is currently in focus, as defined by {@link Time#month} 94 * [0-11]. 95 */ 96 public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month"; 97 /** 98 * If this month should display week numbers. false if 0, true otherwise. 99 */ 100 public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num"; 101 102 protected static int DEFAULT_HEIGHT = 32; 103 protected static int MIN_HEIGHT = 10; 104 protected static final int DEFAULT_SELECTED_DAY = -1; 105 protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY; 106 protected static final int DEFAULT_NUM_DAYS = 7; 107 protected static final int DEFAULT_SHOW_WK_NUM = 0; 108 protected static final int DEFAULT_FOCUS_MONTH = -1; 109 protected static final int DEFAULT_NUM_ROWS = 6; 110 protected static final int MAX_NUM_ROWS = 6; 111 112 private static final int SELECTED_CIRCLE_ALPHA = 60; 113 114 protected static int DAY_SEPARATOR_WIDTH = 1; 115 protected static int MINI_DAY_NUMBER_TEXT_SIZE; 116 protected static int MONTH_LABEL_TEXT_SIZE; 117 protected static int MONTH_DAY_LABEL_TEXT_SIZE; 118 protected static int MONTH_HEADER_SIZE; 119 protected static int DAY_SELECTED_CIRCLE_SIZE; 120 121 // used for scaling to the device density 122 protected static float mScale = 0; 123 124 // affects the padding on the sides of this view 125 protected int mPadding = 0; 126 127 private String mDayOfWeekTypeface; 128 private String mMonthTitleTypeface; 129 130 protected Paint mMonthNumPaint; 131 protected Paint mMonthTitlePaint; 132 protected Paint mMonthTitleBGPaint; 133 protected Paint mSelectedCirclePaint; 134 protected Paint mMonthDayLabelPaint; 135 136 private final Formatter mFormatter; 137 private final StringBuilder mStringBuilder; 138 139 // The Julian day of the first day displayed by this item 140 protected int mFirstJulianDay = -1; 141 // The month of the first day in this week 142 protected int mFirstMonth = -1; 143 // The month of the last day in this week 144 protected int mLastMonth = -1; 145 146 protected int mMonth; 147 148 protected int mYear; 149 // Quick reference to the width of this view, matches parent 150 protected int mWidth; 151 // The height this view should draw at in pixels, set by height param 152 protected int mRowHeight = DEFAULT_HEIGHT; 153 // If this view contains the today 154 protected boolean mHasToday = false; 155 // Which day is selected [0-6] or -1 if no day is selected 156 protected int mSelectedDay = -1; 157 // Which day is today [0-6] or -1 if no day is today 158 protected int mToday = DEFAULT_SELECTED_DAY; 159 // Which day of the week to start on [0-6] 160 protected int mWeekStart = DEFAULT_WEEK_START; 161 // How many days to display 162 protected int mNumDays = DEFAULT_NUM_DAYS; 163 // The number of days + a spot for week number if it is displayed 164 protected int mNumCells = mNumDays; 165 // The left edge of the selected day 166 protected int mSelectedLeft = -1; 167 // The right edge of the selected day 168 protected int mSelectedRight = -1; 169 170 private final Calendar mCalendar; 171 private final Calendar mDayLabelCalendar; 172 private final MonthViewNodeProvider mNodeProvider; 173 174 private int mNumRows = DEFAULT_NUM_ROWS; 175 176 // Optional listener for handling day click actions 177 private OnDayClickListener mOnDayClickListener; 178 // Whether to prevent setting the accessibility delegate 179 private boolean mLockAccessibilityDelegate; 180 181 protected int mDayTextColor; 182 protected int mTodayNumberColor; 183 protected int mMonthTitleColor; 184 protected int mMonthTitleBGColor; 185 186 public SimpleMonthView(Context context) { 187 super(context); 188 189 Resources res = context.getResources(); 190 191 mDayLabelCalendar = Calendar.getInstance(); 192 mCalendar = Calendar.getInstance(); 193 194 mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface); 195 mMonthTitleTypeface = res.getString(R.string.sans_serif); 196 197 mDayTextColor = res.getColor(R.color.date_picker_text_normal); 198 mTodayNumberColor = res.getColor(R.color.blue); 199 mMonthTitleColor = res.getColor(R.color.white); 200 mMonthTitleBGColor = res.getColor(R.color.circle_background); 201 202 mStringBuilder = new StringBuilder(50); 203 mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); 204 205 MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size); 206 MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size); 207 MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size); 208 MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height); 209 DAY_SELECTED_CIRCLE_SIZE = res 210 .getDimensionPixelSize(R.dimen.day_number_select_circle_radius); 211 212 mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height) 213 - MONTH_HEADER_SIZE) / MAX_NUM_ROWS; 214 215 // Set up accessibility components. 216 mNodeProvider = new MonthViewNodeProvider(context, this); 217 ViewCompat.setAccessibilityDelegate(this, mNodeProvider.getAccessibilityDelegate()); 218 ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 219 mLockAccessibilityDelegate = true; 220 221 // Sets up any standard paints that will be used 222 initView(); 223 } 224 225 @Override 226 public void setAccessibilityDelegate(AccessibilityDelegate delegate) { 227 // Workaround for a JB MR1 issue where accessibility delegates on 228 // top-level ListView items are overwritten. 229 if (!mLockAccessibilityDelegate) { 230 super.setAccessibilityDelegate(delegate); 231 } 232 } 233 234 public void setOnDayClickListener(OnDayClickListener listener) { 235 mOnDayClickListener = listener; 236 } 237 238 @Override 239 public boolean onHoverEvent(MotionEvent event) { 240 // First right-of-refusal goes the touch exploration helper. 241 if (mNodeProvider.onHover(this, event)) { 242 return true; 243 } 244 return super.onHoverEvent(event); 245 } 246 247 @Override 248 public boolean onTouchEvent(MotionEvent event) { 249 switch (event.getAction()) { 250 case MotionEvent.ACTION_UP: 251 final CalendarDay day = getDayFromLocation(event.getX(), event.getY()); 252 if (day != null) { 253 onDayClick(day); 254 } 255 break; 256 } 257 return true; 258 } 259 260 /** 261 * Sets up the text and style properties for painting. Override this if you 262 * want to use a different paint. 263 */ 264 protected void initView() { 265 mMonthTitlePaint = new Paint(); 266 mMonthTitlePaint.setFakeBoldText(true); 267 mMonthTitlePaint.setAntiAlias(true); 268 mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE); 269 mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD)); 270 mMonthTitlePaint.setColor(mDayTextColor); 271 mMonthTitlePaint.setTextAlign(Align.CENTER); 272 mMonthTitlePaint.setStyle(Style.FILL); 273 274 mMonthTitleBGPaint = new Paint(); 275 mMonthTitleBGPaint.setFakeBoldText(true); 276 mMonthTitleBGPaint.setAntiAlias(true); 277 mMonthTitleBGPaint.setColor(mMonthTitleBGColor); 278 mMonthTitleBGPaint.setTextAlign(Align.CENTER); 279 mMonthTitleBGPaint.setStyle(Style.FILL); 280 281 mSelectedCirclePaint = new Paint(); 282 mSelectedCirclePaint.setFakeBoldText(true); 283 mSelectedCirclePaint.setAntiAlias(true); 284 mSelectedCirclePaint.setColor(mTodayNumberColor); 285 mSelectedCirclePaint.setTextAlign(Align.CENTER); 286 mSelectedCirclePaint.setStyle(Style.FILL); 287 mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA); 288 289 mMonthDayLabelPaint = new Paint(); 290 mMonthDayLabelPaint.setAntiAlias(true); 291 mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); 292 mMonthDayLabelPaint.setColor(mDayTextColor); 293 mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL)); 294 mMonthDayLabelPaint.setStyle(Style.FILL); 295 mMonthDayLabelPaint.setTextAlign(Align.CENTER); 296 mMonthDayLabelPaint.setFakeBoldText(true); 297 298 mMonthNumPaint = new Paint(); 299 mMonthNumPaint.setAntiAlias(true); 300 mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); 301 mMonthNumPaint.setStyle(Style.FILL); 302 mMonthNumPaint.setTextAlign(Align.CENTER); 303 mMonthNumPaint.setFakeBoldText(false); 304 } 305 306 @Override 307 protected void onDraw(Canvas canvas) { 308 drawMonthTitle(canvas); 309 drawMonthDayLabels(canvas); 310 drawMonthNums(canvas); 311 } 312 313 private int mDayOfWeekStart = 0; 314 315 /** 316 * Sets all the parameters for displaying this week. The only required 317 * parameter is the week number. Other parameters have a default value and 318 * will only update if a new value is included, except for focus month, 319 * which will always default to no focus month if no value is passed in. See 320 * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters. 321 * 322 * @param params A map of the new parameters, see 323 * {@link #VIEW_PARAMS_HEIGHT} 324 * @param tz The time zone this view should reference times in 325 */ 326 public void setMonthParams(HashMap<String, Integer> params) { 327 if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { 328 throw new InvalidParameterException("You must specify the month and year for this view"); 329 } 330 setTag(params); 331 // We keep the current value for any params not present 332 if (params.containsKey(VIEW_PARAMS_HEIGHT)) { 333 mRowHeight = params.get(VIEW_PARAMS_HEIGHT); 334 if (mRowHeight < MIN_HEIGHT) { 335 mRowHeight = MIN_HEIGHT; 336 } 337 } 338 if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { 339 mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY); 340 } 341 342 // Allocate space for caching the day numbers and focus values 343 mMonth = params.get(VIEW_PARAMS_MONTH); 344 mYear = params.get(VIEW_PARAMS_YEAR); 345 346 // Figure out what day today is 347 final Time today = new Time(Time.getCurrentTimezone()); 348 today.setToNow(); 349 mHasToday = false; 350 mToday = -1; 351 352 mCalendar.set(Calendar.MONTH, mMonth); 353 mCalendar.set(Calendar.YEAR, mYear); 354 mCalendar.set(Calendar.DAY_OF_MONTH, 1); 355 mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); 356 357 if (params.containsKey(VIEW_PARAMS_WEEK_START)) { 358 mWeekStart = params.get(VIEW_PARAMS_WEEK_START); 359 } else { 360 mWeekStart = mCalendar.getFirstDayOfWeek(); 361 } 362 363 mNumCells = Utils.getDaysInMonth(mMonth, mYear); 364 for (int i = 0; i < mNumCells; i++) { 365 final int day = i + 1; 366 if (sameDay(day, today)) { 367 mHasToday = true; 368 mToday = day; 369 } 370 } 371 mNumRows = calculateNumRows(); 372 373 // Invalidate cached accessibility information. 374 mNodeProvider.invalidateParent(); 375 } 376 377 public void reuse() { 378 mNumRows = DEFAULT_NUM_ROWS; 379 requestLayout(); 380 } 381 382 private int calculateNumRows() { 383 int offset = findDayOffset(); 384 int dividend = (offset + mNumCells) / mNumDays; 385 int remainder = (offset + mNumCells) % mNumDays; 386 return (dividend + (remainder > 0 ? 1 : 0)); 387 } 388 389 private boolean sameDay(int day, Time today) { 390 return mYear == today.year && 391 mMonth == today.month && 392 day == today.monthDay; 393 } 394 395 @Override 396 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 397 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows 398 + MONTH_HEADER_SIZE); 399 } 400 401 @Override 402 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 403 mWidth = w; 404 405 // Invalidate cached accessibility information. 406 mNodeProvider.invalidateParent(); 407 } 408 409 private String getMonthAndYearString() { 410 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR 411 | DateUtils.FORMAT_NO_MONTH_DAY; 412 mStringBuilder.setLength(0); 413 long millis = mCalendar.getTimeInMillis(); 414 return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags, 415 Time.getCurrentTimezone()).toString(); 416 } 417 418 private void drawMonthTitle(Canvas canvas) { 419 int x = (mWidth + 2 * mPadding) / 2; 420 int y = (MONTH_HEADER_SIZE - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3); 421 canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); 422 } 423 424 private void drawMonthDayLabels(Canvas canvas) { 425 int y = MONTH_HEADER_SIZE - (MONTH_DAY_LABEL_TEXT_SIZE / 2); 426 int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 427 428 for (int i = 0; i < mNumDays; i++) { 429 int calendarDay = (i + mWeekStart) % mNumDays; 430 int x = (2 * i + 1) * dayWidthHalf + mPadding; 431 mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); 432 canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, 433 Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y, 434 mMonthDayLabelPaint); 435 } 436 } 437 438 /** 439 * Draws the week and month day numbers for this week. Override this method 440 * if you need different placement. 441 * 442 * @param canvas The canvas to draw on 443 */ 444 protected void drawMonthNums(Canvas canvas) { 445 int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH) 446 + MONTH_HEADER_SIZE; 447 int dayWidthHalf = (mWidth - mPadding * 2) / (mNumDays * 2); 448 int j = findDayOffset(); 449 for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) { 450 int x = (2 * j + 1) * dayWidthHalf + mPadding; 451 if (mSelectedDay == dayNumber) { 452 canvas.drawCircle(x, y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE, 453 mSelectedCirclePaint); 454 } 455 456 if (mHasToday && mToday == dayNumber) { 457 mMonthNumPaint.setColor(mTodayNumberColor); 458 } else { 459 mMonthNumPaint.setColor(mDayTextColor); 460 } 461 canvas.drawText(String.format("%d", dayNumber), x, y, mMonthNumPaint); 462 j++; 463 if (j == mNumDays) { 464 j = 0; 465 y += mRowHeight; 466 } 467 } 468 } 469 470 private int findDayOffset() { 471 return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) 472 - mWeekStart; 473 } 474 475 476 /** 477 * Calculates the day that the given x position is in, accounting for week 478 * number. Returns a Time referencing that day or null if 479 * 480 * @param x The x position of the touch event 481 * @return A time object for the tapped day or null if the position wasn't 482 * in a day 483 */ 484 public CalendarDay getDayFromLocation(float x, float y) { 485 int dayStart = mPadding; 486 if (x < dayStart || x > mWidth - mPadding) { 487 return null; 488 } 489 // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels 490 int row = (int) (y - MONTH_HEADER_SIZE) / mRowHeight; 491 int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)); 492 493 int day = column - findDayOffset() + 1; 494 day += row * mNumDays; 495 if (day < 1 || day > mNumCells) { 496 return null; 497 } 498 return new CalendarDay(mYear, mMonth, day); 499 } 500 501 /** 502 * Called when the user clicks on a day. Handles callbacks to the 503 * {@link OnDayClickListener} if one is set. 504 * 505 * @param day A time object representing the day that was clicked 506 */ 507 private void onDayClick(CalendarDay day) { 508 if (mOnDayClickListener != null) { 509 mOnDayClickListener.onDayClick(this, day); 510 } 511 512 // This is a no-op if accessibility is turned off. 513 mNodeProvider.sendEventForItem(day, AccessibilityEvent.TYPE_VIEW_CLICKED); 514 } 515 516 /** 517 * @return The date that has accessibility focus, or {@code null} if no date 518 * has focus 519 */ 520 public CalendarDay getAccessibilityFocus() { 521 return mNodeProvider.getFocusedItem(); 522 } 523 524 /** 525 * Clears accessibility focus within the view. No-op if the view does not 526 * contain accessibility focus. 527 */ 528 public void clearAccessibilityFocus() { 529 mNodeProvider.clearFocusedItem(); 530 } 531 532 /** 533 * Attempts to restore accessibility focus to the specified date. 534 * 535 * @param day The date which should receive focus 536 * @return {@code false} if the date is not valid for this month view, or 537 * {@code true} if the date received focus 538 */ 539 public boolean restoreAccessibilityFocus(CalendarDay day) { 540 if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) { 541 return false; 542 } 543 544 mNodeProvider.setFocusedItem(day); 545 return true; 546 } 547 548 /** 549 * Provides a virtual view hierarchy for interfacing with an accessibility 550 * service. 551 */ 552 private class MonthViewNodeProvider extends TouchExplorationHelper<CalendarDay> { 553 private final SparseArray<CalendarDay> mCachedItems = new SparseArray<CalendarDay>(); 554 private final Rect mTempRect = new Rect(); 555 556 Calendar recycle; 557 558 public MonthViewNodeProvider(Context context, View parent) { 559 super(context, parent); 560 } 561 562 @Override 563 public void invalidateItem(CalendarDay item) { 564 super.invalidateItem(item); 565 mCachedItems.delete(getIdForItem(item)); 566 } 567 568 @Override 569 public void invalidateParent() { 570 super.invalidateParent(); 571 mCachedItems.clear(); 572 } 573 574 @Override 575 protected boolean performActionForItem(CalendarDay item, int action, Bundle arguments) { 576 switch (action) { 577 case AccessibilityNodeInfo.ACTION_CLICK: 578 onDayClick(item); 579 return true; 580 } 581 582 return false; 583 } 584 585 @Override 586 protected void populateEventForItem(CalendarDay item, AccessibilityEvent event) { 587 event.setContentDescription(getItemDescription(item)); 588 } 589 590 @Override 591 protected void populateNodeForItem(CalendarDay item, AccessibilityNodeInfoCompat node) { 592 getItemBounds(item, mTempRect); 593 594 node.setContentDescription(getItemDescription(item)); 595 node.setBoundsInParent(mTempRect); 596 node.addAction(AccessibilityNodeInfo.ACTION_CLICK); 597 598 if (item.day == mSelectedDay) { 599 node.setSelected(true); 600 } 601 } 602 603 @Override 604 protected void getVisibleItems(List<CalendarDay> items) { 605 // TODO: Optimize, only return items visible within parent bounds. 606 for (int day = 1; day <= mNumCells; day++) { 607 items.add(getItemForId(day)); 608 } 609 } 610 611 @Override 612 protected CalendarDay getItemAt(float x, float y) { 613 return getDayFromLocation(x, y); 614 } 615 616 @Override 617 protected int getIdForItem(CalendarDay item) { 618 return item.day; 619 } 620 621 @Override 622 protected CalendarDay getItemForId(int id) { 623 if ((id < 1) || (id > mNumCells)) { 624 return null; 625 } 626 627 final CalendarDay item; 628 if (mCachedItems.indexOfKey(id) >= 0) { 629 item = mCachedItems.get(id); 630 } else { 631 item = new CalendarDay(mYear, mMonth, id); 632 mCachedItems.put(id, item); 633 } 634 635 return item; 636 } 637 638 /** 639 * Calculates the bounding rectangle of a given time object. 640 * 641 * @param item The time object to calculate bounds for 642 * @param rect The rectangle in which to store the bounds 643 */ 644 private void getItemBounds(CalendarDay item, Rect rect) { 645 final int offsetX = mPadding; 646 final int offsetY = MONTH_HEADER_SIZE; 647 final int cellHeight = mRowHeight; 648 final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays); 649 final int index = ((item.day - 1) + findDayOffset()); 650 final int row = (index / mNumDays); 651 final int column = (index % mNumDays); 652 final int x = (offsetX + (column * cellWidth)); 653 final int y = (offsetY + (row * cellHeight)); 654 655 rect.set(x, y, (x + cellWidth), (y + cellHeight)); 656 } 657 658 /** 659 * Generates a description for a given time object. Since this 660 * description will be spoken, the components are ordered by descending 661 * specificity as DAY MONTH YEAR. 662 * 663 * @param item The time object to generate a description for 664 * @return A description of the time object 665 */ 666 private CharSequence getItemDescription(CalendarDay item) { 667 if (recycle == null) { 668 recycle = Calendar.getInstance(); 669 } 670 recycle.set(item.year, item.month, item.day); 671 CharSequence date = DateFormat.format("dd MMMM yyyy", recycle.getTimeInMillis()); 672 673 if (item.day == mSelectedDay) { 674 return getContext().getString(R.string.item_is_selected, date); 675 } 676 677 return date; 678 } 679 } 680 681 /** 682 * Handles callbacks when the user clicks on a time object. 683 */ 684 public interface OnDayClickListener { 685 public void onDayClick(SimpleMonthView view, CalendarDay day); 686 } 687} 688