1 /* 2 * Copyright (C) 2021 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 package com.android.calendar.month 17 18 import com.android.calendar.R 19 import com.android.calendar.Utils 20 import android.app.Service 21 import android.content.Context 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.drawable.Drawable 29 import android.text.format.DateUtils 30 import android.text.format.Time 31 import android.view.MotionEvent 32 import android.view.View 33 import android.view.accessibility.AccessibilityEvent 34 import android.view.accessibility.AccessibilityManager 35 import java.security.InvalidParameterException 36 import java.util.HashMap 37 38 /** 39 * 40 * 41 * This is a dynamic view for drawing a single week. It can be configured to 42 * display the week number, start the week on a given day, or show a reduced 43 * number of days. It is intended for use as a single view within a ListView. 44 * See [SimpleWeeksAdapter] for usage. 45 * 46 */ 47 open class SimpleWeekView(context: Context) : View(context) { 48 // affects the padding on the sides of this view 49 @JvmField protected var mPadding = 0 50 @JvmField protected var r: Rect = Rect() 51 @JvmField protected var p: Paint = Paint() 52 @JvmField protected var mMonthNumPaint: Paint = Paint() 53 @JvmField protected var mSelectedDayLine: Drawable 54 55 // Cache the number strings so we don't have to recompute them each time 56 @JvmField protected var mDayNumbers: Array<String?>? = null 57 58 // How many days to display 59 @JvmField protected var mNumDays = DEFAULT_NUM_DAYS 60 61 // The number of days + a spot for week number if it is displayed 62 @JvmField protected var mNumCells = mNumDays 63 64 // Quick lookup for checking which days are in the focus month 65 @JvmField protected var mFocusDay: BooleanArray = BooleanArray(mNumCells) 66 67 // Quick lookup for checking which days are in an odd month (to set a different background) 68 @JvmField protected var mOddMonth: BooleanArray = BooleanArray(mNumCells) 69 70 // The Julian day of the first day displayed by this item 71 @JvmField protected var mFirstJulianDay = -1 72 73 // The month of the first day in this week 74 @JvmField protected var firstMonth = -1 75 76 // The month of the last day in this week 77 @JvmField protected var lastMonth = -1 78 79 // The position of this week, equivalent to weeks since the week of Jan 1st, 80 // 1970 81 @JvmField var mWeek = -1 82 83 // Quick reference to the width of this view, matches parent 84 @JvmField protected var mWidth = 0 85 86 // The height this view should draw at in pixels, set by height param 87 @JvmField protected var mHeight = DEFAULT_HEIGHT 88 89 // Whether the week number should be shown 90 @JvmField protected var mShowWeekNum = false 91 92 // If this view contains the selected day 93 @JvmField protected var mHasSelectedDay = false 94 95 // If this view contains the today 96 open protected var mHasToday = false 97 98 // Which day is selected [0-6] or -1 if no day is selected 99 @JvmField protected var mSelectedDay = DEFAULT_SELECTED_DAY 100 101 // Which day is today [0-6] or -1 if no day is today 102 @JvmField protected var mToday: Int = DEFAULT_SELECTED_DAY 103 104 // Which day of the week to start on [0-6] 105 @JvmField protected var mWeekStart = DEFAULT_WEEK_START 106 107 // The left edge of the selected day 108 @JvmField protected var mSelectedLeft = -1 109 110 // The right edge of the selected day 111 @JvmField protected var mSelectedRight = -1 112 113 // The timezone to display times/dates in (used for determining when Today 114 // is) 115 @JvmField protected var mTimeZone: String = Time.getCurrentTimezone() 116 @JvmField protected var mBGColor: Int 117 @JvmField protected var mSelectedWeekBGColor: Int 118 @JvmField protected var mFocusMonthColor: Int 119 @JvmField protected var mOtherMonthColor: Int 120 @JvmField protected var mDaySeparatorColor: Int 121 @JvmField protected var mTodayOutlineColor: Int 122 @JvmField protected var mWeekNumColor: Int 123 124 /** 125 * Sets all the parameters for displaying this week. The only required 126 * parameter is the week number. Other parameters have a default value and 127 * will only update if a new value is included, except for focus month, 128 * which will always default to no focus month if no value is passed in. See 129 * [.VIEW_PARAMS_HEIGHT] for more info on parameters. 130 * 131 * @param params A map of the new parameters, see 132 * [.VIEW_PARAMS_HEIGHT] 133 * @param tz The time zone this view should reference times in 134 */ setWeekParamsnull135 open fun setWeekParams(params: HashMap<String?, Int?>, tz: String) { 136 if (!params.containsKey(VIEW_PARAMS_WEEK)) { 137 throw InvalidParameterException("You must specify the week number for this view") 138 } 139 setTag(params) 140 mTimeZone = tz 141 // We keep the current value for any params not present 142 if (params.containsKey(VIEW_PARAMS_HEIGHT)) { 143 mHeight = (params.get(VIEW_PARAMS_HEIGHT))!!.toInt() 144 if (mHeight < MIN_HEIGHT) { 145 mHeight = MIN_HEIGHT 146 } 147 } 148 if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { 149 mSelectedDay = (params.get(VIEW_PARAMS_SELECTED_DAY))!!.toInt() 150 } 151 mHasSelectedDay = mSelectedDay != -1 152 if (params.containsKey(VIEW_PARAMS_NUM_DAYS)) { 153 mNumDays = (params.get(VIEW_PARAMS_NUM_DAYS))!!.toInt() 154 } 155 if (params.containsKey(VIEW_PARAMS_SHOW_WK_NUM)) { 156 mShowWeekNum = 157 if (params.get(VIEW_PARAMS_SHOW_WK_NUM) != 0) { 158 true 159 } else { 160 false 161 } 162 } 163 mNumCells = if (mShowWeekNum) mNumDays + 1 else mNumDays 164 165 // Allocate space for caching the day numbers and focus values 166 mDayNumbers = arrayOfNulls(mNumCells) 167 mFocusDay = BooleanArray(mNumCells) 168 mOddMonth = BooleanArray(mNumCells) 169 mWeek = (params.get(VIEW_PARAMS_WEEK))!!.toInt() 170 val julianMonday: Int = Utils.getJulianMondayFromWeeksSinceEpoch(mWeek) 171 val time = Time(tz) 172 time.setJulianDay(julianMonday) 173 174 // If we're showing the week number calculate it based on Monday 175 var i = 0 176 if (mShowWeekNum) { 177 mDayNumbers!![0] = Integer.toString(time.getWeekNumber()) 178 i++ 179 } 180 if (params.containsKey(VIEW_PARAMS_WEEK_START)) { 181 mWeekStart = (params.get(VIEW_PARAMS_WEEK_START))!!.toInt() 182 } 183 184 // Now adjust our starting day based on the start day of the week 185 // If the week is set to start on a Saturday the first week will be 186 // Dec 27th 1969 -Jan 2nd, 1970 187 if (time.weekDay !== mWeekStart) { 188 var diff: Int = time.weekDay - mWeekStart 189 if (diff < 0) { 190 diff += 7 191 } 192 time.monthDay -= diff 193 time.normalize(true) 194 } 195 mFirstJulianDay = Time.getJulianDay(time.toMillis(true), time.gmtoff) 196 firstMonth = time.month 197 198 // Figure out what day today is 199 val today = Time(tz) 200 today.setToNow() 201 mHasToday = false 202 mToday = -1 203 val focusMonth = if (params.containsKey(VIEW_PARAMS_FOCUS_MONTH)) params.get( 204 VIEW_PARAMS_FOCUS_MONTH 205 ) else DEFAULT_FOCUS_MONTH 206 while (i < mNumCells) { 207 if (time.monthDay === 1) { 208 firstMonth = time.month 209 } 210 mOddMonth[i] = time.month % 2 === 1 211 if (time.month === focusMonth) { 212 mFocusDay[i] = true 213 } else { 214 mFocusDay[i] = false 215 } 216 if (time.year === today.year && time.yearDay === today.yearDay) { 217 mHasToday = true 218 mToday = i 219 } 220 mDayNumbers!![i] = Integer.toString(time.monthDay++) 221 time.normalize(true) 222 i++ 223 } 224 // We do one extra add at the end of the loop, if that pushed us to a 225 // new month undo it 226 if (time.monthDay === 1) { 227 time.monthDay-- 228 time.normalize(true) 229 } 230 lastMonth = time.month 231 updateSelectionPositions() 232 } 233 234 /** 235 * Sets up the text and style properties for painting. Override this if you 236 * want to use a different paint. 237 */ initViewnull238 protected open fun initView() { 239 p.setFakeBoldText(false) 240 p.setAntiAlias(true) 241 p.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE.toFloat()) 242 p.setStyle(Style.FILL) 243 mMonthNumPaint = Paint() 244 mMonthNumPaint.setFakeBoldText(true) 245 mMonthNumPaint.setAntiAlias(true) 246 mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE.toFloat()) 247 mMonthNumPaint.setColor(mFocusMonthColor) 248 mMonthNumPaint.setStyle(Style.FILL) 249 mMonthNumPaint.setTextAlign(Align.CENTER) 250 } 251 252 /** 253 * Returns the month of the first day in this week 254 * 255 * @return The month the first day of this view is in 256 */ getFirstMonthnull257 fun getFirstMonth(): Int { 258 return firstMonth 259 } 260 261 /** 262 * Returns the month of the last day in this week 263 * 264 * @return The month the last day of this view is in 265 */ getLastMonthnull266 fun getLastMonth(): Int { 267 return lastMonth 268 } 269 270 /** 271 * Returns the julian day of the first day in this view. 272 * 273 * @return The julian day of the first day in the view. 274 */ getFirstJulianDaynull275 fun getFirstJulianDay(): Int { 276 return mFirstJulianDay 277 } 278 279 /** 280 * Calculates the day that the given x position is in, accounting for week 281 * number. Returns a Time referencing that day or null if 282 * 283 * @param x The x position of the touch event 284 * @return A time object for the tapped day or null if the position wasn't 285 * in a day 286 */ getDayFromLocationnull287 open fun getDayFromLocation(x: Float): Time? { 288 val dayStart = 289 if (mShowWeekNum) (mWidth - mPadding * 2) / mNumCells + mPadding else mPadding 290 if (x < dayStart || x > mWidth - mPadding) { 291 return null 292 } 293 // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels 294 val dayPosition = ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)).toInt() 295 var day = mFirstJulianDay + dayPosition 296 val time = Time(mTimeZone) 297 if (mWeek == 0) { 298 // This week is weird... 299 if (day < Time.EPOCH_JULIAN_DAY) { 300 day++ 301 } else if (day == Time.EPOCH_JULIAN_DAY) { 302 time.set(1, 0, 1970) 303 time.normalize(true) 304 return time 305 } 306 } 307 time.setJulianDay(day) 308 return time 309 } 310 311 @Override onDrawnull312 protected override fun onDraw(canvas: Canvas) { 313 drawBackground(canvas) 314 drawWeekNums(canvas) 315 drawDaySeparators(canvas) 316 } 317 318 /** 319 * This draws the selection highlight if a day is selected in this week. 320 * Override this method if you wish to have a different background drawn. 321 * 322 * @param canvas The canvas to draw on 323 */ drawBackgroundnull324 protected open fun drawBackground(canvas: Canvas) { 325 if (mHasSelectedDay) { 326 p.setColor(mSelectedWeekBGColor) 327 p.setStyle(Style.FILL) 328 } else { 329 return 330 } 331 r.top = 1 332 r.bottom = mHeight - 1 333 r.left = mPadding 334 r.right = mSelectedLeft 335 canvas.drawRect(r, p) 336 r.left = mSelectedRight 337 r.right = mWidth - mPadding 338 canvas.drawRect(r, p) 339 } 340 341 /** 342 * Draws the week and month day numbers for this week. Override this method 343 * if you need different placement. 344 * 345 * @param canvas The canvas to draw on 346 */ drawWeekNumsnull347 protected open fun drawWeekNums(canvas: Canvas) { 348 val y = (mHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH 349 val nDays = mNumCells 350 var i = 0 351 val divisor = 2 * nDays 352 if (mShowWeekNum) { 353 p.setTextSize(MINI_WK_NUMBER_TEXT_SIZE.toFloat()) 354 p.setStyle(Style.FILL) 355 p.setTextAlign(Align.CENTER) 356 p.setAntiAlias(true) 357 p.setColor(mWeekNumColor) 358 val x = (mWidth - mPadding * 2) / divisor + mPadding 359 canvas.drawText(mDayNumbers!![0] as String, x.toFloat(), y.toFloat(), p) 360 i++ 361 } 362 var isFocusMonth = mFocusDay[i] 363 mMonthNumPaint.setColor(if (isFocusMonth) mFocusMonthColor else mOtherMonthColor) 364 mMonthNumPaint.setFakeBoldText(false) 365 while (i < nDays) { 366 if (mFocusDay[i] != isFocusMonth) { 367 isFocusMonth = mFocusDay[i] 368 mMonthNumPaint.setColor(if (isFocusMonth) mFocusMonthColor else mOtherMonthColor) 369 } 370 if (mHasToday && mToday == i) { 371 mMonthNumPaint.setTextSize(MINI_TODAY_NUMBER_TEXT_SIZE.toFloat()) 372 mMonthNumPaint.setFakeBoldText(true) 373 } 374 val x = (2 * i + 1) * (mWidth - mPadding * 2) / divisor + mPadding 375 canvas.drawText(mDayNumbers!![i] as String, x.toFloat(), y.toFloat(), 376 mMonthNumPaint as Paint) 377 if (mHasToday && mToday == i) { 378 mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE.toFloat()) 379 mMonthNumPaint.setFakeBoldText(false) 380 } 381 i++ 382 } 383 } 384 385 /** 386 * Draws a horizontal line for separating the weeks. Override this method if 387 * you want custom separators. 388 * 389 * @param canvas The canvas to draw on 390 */ drawDaySeparatorsnull391 protected open fun drawDaySeparators(canvas: Canvas) { 392 if (mHasSelectedDay) { 393 r.top = 1 394 r.bottom = mHeight - 1 395 r.left = mSelectedLeft + 1 396 r.right = mSelectedRight - 1 397 p.setStrokeWidth(MINI_TODAY_OUTLINE_WIDTH.toFloat()) 398 p.setStyle(Style.STROKE) 399 p.setColor(mTodayOutlineColor) 400 canvas.drawRect(r, p) 401 } 402 if (mShowWeekNum) { 403 p.setColor(mDaySeparatorColor) 404 p.setStrokeWidth(DAY_SEPARATOR_WIDTH.toFloat()) 405 val x = (mWidth - mPadding * 2) / mNumCells + mPadding 406 canvas.drawLine(x.toFloat(), 0f, x.toFloat(), mHeight.toFloat(), p) 407 } 408 } 409 410 @Override onSizeChangednull411 protected override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 412 mWidth = w 413 updateSelectionPositions() 414 } 415 416 /** 417 * This calculates the positions for the selected day lines. 418 */ updateSelectionPositionsnull419 protected open fun updateSelectionPositions() { 420 if (mHasSelectedDay) { 421 var selectedPosition = mSelectedDay - mWeekStart 422 if (selectedPosition < 0) { 423 selectedPosition += 7 424 } 425 if (mShowWeekNum) { 426 selectedPosition++ 427 } 428 mSelectedLeft = (selectedPosition * (mWidth - mPadding * 2) / mNumCells + 429 mPadding) 430 mSelectedRight = ((selectedPosition + 1) * (mWidth - mPadding * 2) / mNumCells + 431 mPadding) 432 } 433 } 434 435 @Override onMeasurenull436 protected override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 437 setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight) 438 } 439 440 @Override onHoverEventnull441 override fun onHoverEvent(event: MotionEvent): Boolean { 442 val context: Context = getContext() 443 // only send accessibility events if accessibility and exploration are 444 // on. 445 val am: AccessibilityManager = context 446 .getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager 447 if (!am.isEnabled() || !am.isTouchExplorationEnabled()) { 448 return super.onHoverEvent(event) 449 } 450 if (event.getAction() !== MotionEvent.ACTION_HOVER_EXIT) { 451 val hover: Time? = getDayFromLocation(event.getX()) 452 if (hover != null && 453 (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) !== 0) 454 ) { 455 val millis: Long = hover.toMillis(true) 456 val date: String? = Utils.formatDateRange( 457 context, millis, millis, 458 DateUtils.FORMAT_SHOW_DATE 459 ) 460 val accessEvent: AccessibilityEvent = 461 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) 462 accessEvent.getText().add(date) 463 sendAccessibilityEventUnchecked(accessEvent) 464 mLastHoverTime = hover 465 } 466 } 467 return true 468 } 469 470 @JvmField var mLastHoverTime: Time? = null 471 472 companion object { 473 private const val TAG = "MonthView" 474 /** 475 * These params can be passed into the view to control how it appears. 476 * [.VIEW_PARAMS_WEEK] is the only required field, though the default 477 * values are unlikely to fit most layouts correctly. 478 */ 479 /** 480 * This sets the height of this week in pixels 481 */ 482 const val VIEW_PARAMS_HEIGHT = "height" 483 484 /** 485 * This specifies the position (or weeks since the epoch) of this week, 486 * calculated using [Utils.getWeeksSinceEpochFromJulianDay] 487 */ 488 const val VIEW_PARAMS_WEEK = "week" 489 490 /** 491 * This sets one of the days in this view as selected [Time.SUNDAY] 492 * through [Time.SATURDAY]. 493 */ 494 const val VIEW_PARAMS_SELECTED_DAY = "selected_day" 495 496 /** 497 * Which day the week should start on. [Time.SUNDAY] through 498 * [Time.SATURDAY]. 499 */ 500 const val VIEW_PARAMS_WEEK_START = "week_start" 501 502 /** 503 * How many days to display at a time. Days will be displayed starting with 504 * [.mWeekStart]. 505 */ 506 const val VIEW_PARAMS_NUM_DAYS = "num_days" 507 508 /** 509 * Which month is currently in focus, as defined by [Time.month] 510 * [0-11]. 511 */ 512 const val VIEW_PARAMS_FOCUS_MONTH = "focus_month" 513 514 /** 515 * If this month should display week numbers. false if 0, true otherwise. 516 */ 517 const val VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num" 518 protected var DEFAULT_HEIGHT = 32 519 protected var MIN_HEIGHT = 10 520 protected const val DEFAULT_SELECTED_DAY = -1 521 protected val DEFAULT_WEEK_START: Int = Time.SUNDAY 522 protected const val DEFAULT_NUM_DAYS = 7 523 protected const val DEFAULT_SHOW_WK_NUM = 0 524 protected const val DEFAULT_FOCUS_MONTH = -1 525 protected var DAY_SEPARATOR_WIDTH = 1 526 protected var MINI_DAY_NUMBER_TEXT_SIZE = 14 527 protected var MINI_WK_NUMBER_TEXT_SIZE = 12 528 protected var MINI_TODAY_NUMBER_TEXT_SIZE = 18 529 protected var MINI_TODAY_OUTLINE_WIDTH = 2 530 protected var WEEK_NUM_MARGIN_BOTTOM = 4 531 532 // used for scaling to the device density 533 @JvmStatic protected var mScale = 0f 534 } 535 536 init { 537 val res: Resources = context.getResources() 538 mBGColor = res.getColor(R.color.month_bgcolor) 539 mSelectedWeekBGColor = res.getColor(R.color.month_selected_week_bgcolor) 540 mFocusMonthColor = res.getColor(R.color.month_mini_day_number) 541 mOtherMonthColor = res.getColor(R.color.month_other_month_day_number) 542 mDaySeparatorColor = res.getColor(R.color.month_grid_lines) 543 mTodayOutlineColor = res.getColor(R.color.mini_month_today_outline_color) 544 mWeekNumColor = res.getColor(R.color.month_week_num_color) 545 mSelectedDayLine = res.getDrawable(R.drawable.dayline_minical_holo_light) 546 if (mScale == 0f) { 547 mScale = context.getResources().getDisplayMetrics().density 548 if (mScale != 1f) { 549 DEFAULT_HEIGHT *= mScale.toInt() 550 MIN_HEIGHT *= mScale.toInt() 551 MINI_DAY_NUMBER_TEXT_SIZE *= mScale.toInt() 552 MINI_TODAY_NUMBER_TEXT_SIZE *= mScale.toInt() 553 MINI_TODAY_OUTLINE_WIDTH *= mScale.toInt() 554 WEEK_NUM_MARGIN_BOTTOM *= mScale.toInt() 555 DAY_SEPARATOR_WIDTH *= mScale.toInt() 556 MINI_WK_NUMBER_TEXT_SIZE *= mScale.toInt() 557 } 558 } 559 560 // Sets up any standard paints that will be used 561 initView() 562 } 563 }