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