1 /* <lambda>null2 * Copyright (C) 2020 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 com.android.deskclock.stopwatch 18 19 import android.R.attr.state_activated 20 import android.R.attr.state_pressed 21 import android.annotation.SuppressLint 22 import android.app.Activity 23 import android.content.ActivityNotFoundException 24 import android.content.Context 25 import android.content.Intent 26 import android.content.res.ColorStateList 27 import android.content.res.Resources 28 import android.graphics.Canvas 29 import android.graphics.drawable.GradientDrawable 30 import android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM 31 import android.os.Bundle 32 import android.transition.TransitionManager 33 import android.view.LayoutInflater 34 import android.view.MotionEvent 35 import android.view.View 36 import android.view.View.GONE 37 import android.view.View.INVISIBLE 38 import android.view.View.VISIBLE 39 import android.view.ViewGroup 40 import android.view.WindowManager 41 import android.widget.Button 42 import android.widget.ImageView 43 import android.widget.TextView 44 import androidx.annotation.ColorInt 45 import androidx.core.graphics.ColorUtils 46 import androidx.recyclerview.widget.LinearLayoutManager 47 import androidx.recyclerview.widget.RecyclerView 48 import androidx.recyclerview.widget.SimpleItemAnimator 49 50 import com.android.deskclock.AnimatorUtils 51 import com.android.deskclock.DeskClockFragment 52 import com.android.deskclock.FabContainer 53 import com.android.deskclock.FabContainer.UpdateFabFlag 54 import com.android.deskclock.data.DataModel 55 import com.android.deskclock.data.Lap 56 import com.android.deskclock.data.Stopwatch 57 import com.android.deskclock.data.StopwatchListener 58 import com.android.deskclock.events.Events 59 import com.android.deskclock.LogUtils 60 import com.android.deskclock.R 61 import com.android.deskclock.StopwatchTextController 62 import com.android.deskclock.ThemeUtils 63 import com.android.deskclock.Utils 64 import com.android.deskclock.uidata.TabListener 65 import com.android.deskclock.uidata.UiDataModel 66 67 import kotlin.math.max 68 import kotlin.math.min 69 import kotlin.math.pow 70 import kotlin.math.roundToInt 71 72 /** 73 * Fragment that shows the stopwatch and recorded laps. 74 */ 75 class StopwatchFragment : DeskClockFragment(UiDataModel.Tab.STOPWATCH) { 76 77 /** Keep the screen on when this tab is selected. */ 78 private val mTabWatcher: TabListener = TabWatcher() 79 80 /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */ 81 private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable() 82 83 /** Updates the user interface in response to stopwatch changes. */ 84 private val mStopwatchWatcher: StopwatchListener = StopwatchWatcher() 85 86 /** Draws a gradient over the bottom of the [.mLapsList] to reduce clash with the fab. */ 87 private var mGradientItemDecoration: GradientItemDecoration? = null 88 89 /** The data source for [.mLapsList]. */ 90 private lateinit var mLapsAdapter: LapsAdapter 91 92 /** The layout manager for the [.mLapsAdapter]. */ 93 private lateinit var mLapsLayoutManager: LinearLayoutManager 94 95 /** Draws the reference lap while the stopwatch is running. */ 96 private var mTime: StopwatchCircleView? = null 97 98 /** The View containing both TextViews of the stopwatch. */ 99 private lateinit var mStopwatchWrapper: View 100 101 /** Displays the recorded lap times. */ 102 private lateinit var mLapsList: RecyclerView 103 104 /** Displays the current stopwatch time (seconds and above only). */ 105 private lateinit var mMainTimeText: TextView 106 107 /** Displays the current stopwatch time (hundredths only). */ 108 private lateinit var mHundredthsTimeText: TextView 109 110 /** Formats and displays the text in the stopwatch. */ 111 private lateinit var mStopwatchTextController: StopwatchTextController 112 113 override fun onCreateView( 114 inflater: LayoutInflater, 115 container: ViewGroup?, 116 state: Bundle? 117 ): View { 118 mLapsAdapter = LapsAdapter(requireActivity()) 119 mLapsLayoutManager = LinearLayoutManager(requireActivity()) 120 mGradientItemDecoration = GradientItemDecoration(requireActivity()) 121 122 val v: View = inflater.inflate(R.layout.stopwatch_fragment, container, false) 123 mTime = v.findViewById(R.id.stopwatch_circle) 124 mLapsList = v.findViewById(R.id.laps_list) as RecyclerView 125 (mLapsList.getItemAnimator() as SimpleItemAnimator).setSupportsChangeAnimations(false) 126 mLapsList.setLayoutManager(mLapsLayoutManager) 127 mLapsList.addItemDecoration(mGradientItemDecoration!!) 128 129 // In landscape layouts, the laps list can reach the top of the screen and thus can cause 130 // a drop shadow to appear. The same is not true for portrait landscapes. 131 if (Utils.isLandscape(requireActivity())) { 132 val scrollPositionWatcher = ScrollPositionWatcher() 133 mLapsList.addOnLayoutChangeListener(scrollPositionWatcher) 134 mLapsList.addOnScrollListener(scrollPositionWatcher) 135 } else { 136 setTabScrolledToTop(true) 137 } 138 mLapsList.setAdapter(mLapsAdapter) 139 140 // Timer text serves as a virtual start/stop button. 141 mMainTimeText = v.findViewById(R.id.stopwatch_time_text) as TextView 142 mHundredthsTimeText = v.findViewById(R.id.stopwatch_hundredths_text) as TextView 143 mStopwatchTextController = StopwatchTextController(mMainTimeText, mHundredthsTimeText) 144 mStopwatchWrapper = v.findViewById(R.id.stopwatch_time_wrapper) 145 146 DataModel.dataModel.addStopwatchListener(mStopwatchWatcher) 147 148 mStopwatchWrapper.setOnClickListener(TimeClickListener()) 149 if (mTime != null) { 150 mStopwatchWrapper.setOnTouchListener(CircleTouchListener()) 151 } 152 153 val c: Context = mMainTimeText.getContext() 154 val colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent) 155 val textColorPrimary = ThemeUtils.resolveColor(c, android.R.attr.textColorPrimary) 156 val timeTextColor = 157 ColorStateList( 158 arrayOf(intArrayOf(-state_activated, -state_pressed), intArrayOf()), 159 intArrayOf(textColorPrimary, colorAccent) 160 ) 161 mMainTimeText.setTextColor(timeTextColor) 162 mHundredthsTimeText.setTextColor(timeTextColor) 163 164 return v 165 } 166 167 override fun onStart() { 168 super.onStart() 169 170 val activity: Activity = requireActivity() 171 val intent: Intent? = activity.getIntent() 172 if (intent != null) { 173 val action: String? = intent.getAction() 174 if (StopwatchService.Companion.ACTION_START_STOPWATCH == action) { 175 DataModel.dataModel.startStopwatch() 176 // Consume the intent 177 activity.setIntent(null) 178 } else if (StopwatchService.Companion.ACTION_PAUSE_STOPWATCH == action) { 179 DataModel.dataModel.pauseStopwatch() 180 // Consume the intent 181 activity.setIntent(null) 182 } 183 } 184 185 // Conservatively assume the data in the adapter has changed while the fragment was paused. 186 mLapsAdapter.notifyDataSetChanged() 187 188 // Synchronize the user interface with the data model. 189 updateUI(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 190 191 // Start watching for page changes away from this fragment. 192 UiDataModel.uiDataModel.addTabListener(mTabWatcher) 193 } 194 195 override fun onStop() { 196 super.onStop() 197 198 // Stop all updates while the fragment is not visible. 199 stopUpdatingTime() 200 201 // Stop watching for page changes away from this fragment. 202 UiDataModel.uiDataModel.removeTabListener(mTabWatcher) 203 204 // Release the wake lock if it is currently held. 205 releaseWakeLock() 206 } 207 208 override fun onDestroyView() { 209 super.onDestroyView() 210 211 DataModel.dataModel.removeStopwatchListener(mStopwatchWatcher) 212 } 213 214 override fun onFabClick(fab: ImageView) { 215 toggleStopwatchState() 216 } 217 218 override fun onLeftButtonClick(left: Button) { 219 doReset() 220 } 221 222 override fun onRightButtonClick(right: Button) { 223 when (stopwatch.state) { 224 Stopwatch.State.RUNNING -> doAddLap() 225 Stopwatch.State.PAUSED -> doShare() 226 Stopwatch.State.RESET -> { 227 } 228 null -> { 229 } 230 } 231 } 232 233 private fun updateFab(fab: ImageView, animate: Boolean) { 234 if (stopwatch.isRunning) { 235 if (animate) { 236 fab.setImageResource(R.drawable.ic_play_pause_animation) 237 } else { 238 fab.setImageResource(R.drawable.ic_play_pause) 239 } 240 fab.setContentDescription(fab.getResources().getString(R.string.sw_pause_button)) 241 } else { 242 if (animate) { 243 fab.setImageResource(R.drawable.ic_pause_play_animation) 244 } else { 245 fab.setImageResource(R.drawable.ic_pause_play) 246 } 247 fab.setContentDescription(fab.getResources().getString(R.string.sw_start_button)) 248 } 249 fab.setVisibility(VISIBLE) 250 } 251 252 override fun onUpdateFab(fab: ImageView) { 253 updateFab(fab, false) 254 } 255 256 override fun onMorphFab(fab: ImageView) { 257 // Update the fab's drawable to match the current timer state. 258 updateFab(fab, Utils.isNOrLater) 259 // Animate the drawable. 260 AnimatorUtils.startDrawableAnimation(fab) 261 } 262 263 override fun onUpdateFabButtons(left: Button, right: Button) { 264 val resources: Resources = getResources() 265 left.setClickable(true) 266 left.setText(R.string.sw_reset_button) 267 left.setContentDescription(resources.getString(R.string.sw_reset_button)) 268 269 when (stopwatch.state) { 270 Stopwatch.State.RESET -> { 271 left.setVisibility(INVISIBLE) 272 right.setClickable(true) 273 right.setVisibility(INVISIBLE) 274 } 275 Stopwatch.State.RUNNING -> { 276 left.setVisibility(VISIBLE) 277 val canRecordLaps = canRecordMoreLaps() 278 right.setText(R.string.sw_lap_button) 279 right.setContentDescription(resources.getString(R.string.sw_lap_button)) 280 right.setClickable(canRecordLaps) 281 right.setVisibility(if (canRecordLaps) VISIBLE else INVISIBLE) 282 } 283 Stopwatch.State.PAUSED -> { 284 left.setVisibility(VISIBLE) 285 right.setClickable(true) 286 right.setVisibility(VISIBLE) 287 right.setText(R.string.sw_share_button) 288 right.setContentDescription(resources.getString(R.string.sw_share_button)) 289 } 290 null -> { 291 } 292 } 293 } 294 295 /** 296 * @param color the newly installed app window color 297 */ 298 override fun onAppColorChanged(@ColorInt color: Int) { 299 mGradientItemDecoration?.updateGradientColors(color) 300 mLapsList.invalidateItemDecorations() 301 } 302 303 /** 304 * Start the stopwatch. 305 */ 306 private fun doStart() { 307 Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock) 308 DataModel.dataModel.startStopwatch() 309 } 310 311 /** 312 * Pause the stopwatch. 313 */ 314 private fun doPause() { 315 Events.sendStopwatchEvent(R.string.action_pause, R.string.label_deskclock) 316 DataModel.dataModel.pauseStopwatch() 317 } 318 319 /** 320 * Reset the stopwatch. 321 */ 322 private fun doReset() { 323 val priorState = stopwatch.state 324 Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock) 325 DataModel.dataModel.resetStopwatch() 326 mMainTimeText.setAlpha(1f) 327 mHundredthsTimeText.setAlpha(1f) 328 if (priorState == Stopwatch.State.RUNNING) { 329 updateFab(FabContainer.FAB_MORPH) 330 } 331 } 332 333 /** 334 * Send stopwatch time and lap times to an external sharing application. 335 */ 336 private fun doShare() { 337 // Disable the fab buttons to avoid double-taps on the share button. 338 updateFab(FabContainer.BUTTONS_DISABLE) 339 340 val subjects: Array<String> = getResources().getStringArray(R.array.sw_share_strings) 341 val subject = subjects[(Math.random() * subjects.size).toInt()] 342 val text = mLapsAdapter.shareText 343 344 @SuppressLint("InlinedApi") 345 val shareIntent: Intent = Intent(Intent.ACTION_SEND) 346 .addFlags(if (Utils.isLOrLater) { 347 Intent.FLAG_ACTIVITY_NEW_DOCUMENT 348 } else { 349 Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET 350 }) 351 .putExtra(Intent.EXTRA_SUBJECT, subject) 352 .putExtra(Intent.EXTRA_TEXT, text) 353 .setType("text/plain") 354 355 val context: Context = requireActivity() 356 val title: String = context.getString(R.string.sw_share_button) 357 val shareChooserIntent: Intent = Intent.createChooser(shareIntent, title) 358 try { 359 context.startActivity(shareChooserIntent) 360 } catch (anfe: ActivityNotFoundException) { 361 LogUtils.e("Cannot share lap data because no suitable receiving Activity exists") 362 updateFab(FabContainer.BUTTONS_IMMEDIATE) 363 } 364 } 365 366 /** 367 * Record and add a new lap ending now. 368 */ 369 private fun doAddLap() { 370 Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock) 371 372 // Record a new lap. 373 val lap = mLapsAdapter.addLap() ?: return 374 375 // Update button states. 376 updateFab(FabContainer.BUTTONS_IMMEDIATE) 377 if (lap.lapNumber == 1) { 378 // Child views from prior lap sets hang around and blit to the screen when adding the 379 // first lap of the subsequent lap set. Remove those superfluous children here manually 380 // to ensure they aren't seen as the first lap is drawn. 381 mLapsList.removeAllViewsInLayout() 382 if (mTime != null) { 383 // Start animating the reference lap. 384 mTime!!.update() 385 } 386 387 // Recording the first lap transitions the UI to display the laps list. 388 showOrHideLaps(false) 389 } 390 391 // Ensure the newly added lap is visible on screen. 392 mLapsList.scrollToPosition(0) 393 } 394 395 /** 396 * Show or hide the list of laps. 397 */ 398 private fun showOrHideLaps(clearLaps: Boolean) { 399 val sceneRoot: ViewGroup = getView() as ViewGroup? ?: return 400 401 TransitionManager.beginDelayedTransition(sceneRoot) 402 403 if (clearLaps) { 404 mLapsAdapter.clearLaps() 405 } 406 407 val lapsVisible = mLapsAdapter.getItemCount() > 0 408 mLapsList.setVisibility(if (lapsVisible) VISIBLE else GONE) 409 410 if (Utils.isPortrait(requireActivity())) { 411 // When the lap list is visible, it includes the bottom padding. When it is absent the 412 // appropriate bottom padding must be applied to the container. 413 val res: Resources = getResources() 414 val bottom = if (lapsVisible) 0 else res.getDimensionPixelSize(R.dimen.fab_height) 415 val top: Int = sceneRoot.getPaddingTop() 416 val left: Int = sceneRoot.getPaddingLeft() 417 val right: Int = sceneRoot.getPaddingRight() 418 sceneRoot.setPadding(left, top, right, bottom) 419 } 420 } 421 422 private fun adjustWakeLock() { 423 val appInForeground = DataModel.dataModel.isApplicationInForeground 424 if (stopwatch.isRunning && isTabSelected && appInForeground) { 425 requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 426 } else { 427 releaseWakeLock() 428 } 429 } 430 431 private fun releaseWakeLock() { 432 requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 433 } 434 435 /** 436 * Either pause or start the stopwatch based on its current state. 437 */ 438 private fun toggleStopwatchState() { 439 if (stopwatch.isRunning) { 440 doPause() 441 } else { 442 doStart() 443 } 444 } 445 446 private val stopwatch: Stopwatch 447 get() = DataModel.dataModel.stopwatch 448 449 private fun canRecordMoreLaps(): Boolean = DataModel.dataModel.canAddMoreLaps() 450 451 /** 452 * Post the first runnable to update times within the UI. It will reschedule itself as needed. 453 */ 454 private fun startUpdatingTime() { 455 // Ensure only one copy of the runnable is ever scheduled by first stopping updates. 456 stopUpdatingTime() 457 mMainTimeText.post(mTimeUpdateRunnable) 458 } 459 460 /** 461 * Remove the runnable that updates times within the UI. 462 */ 463 private fun stopUpdatingTime() { 464 mMainTimeText.removeCallbacks(mTimeUpdateRunnable) 465 } 466 467 /** 468 * Update all time displays based on a single snapshot of the stopwatch progress. This includes 469 * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in 470 * the list of laps. 471 */ 472 private fun updateTime() { 473 // Compute the total time of the stopwatch. 474 val stopwatch = stopwatch 475 val totalTime = stopwatch.totalTime 476 mStopwatchTextController.setTimeString(totalTime) 477 478 // Update the current lap. 479 val currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0 480 if (!stopwatch.isReset && currentLapIsVisible) { 481 mLapsAdapter.updateCurrentLap(mLapsList, totalTime) 482 } 483 } 484 485 /** 486 * Synchronize the UI state with the model data. 487 */ 488 private fun updateUI(@UpdateFabFlag updateTypes: Int) { 489 adjustWakeLock() 490 491 // Draw the latest stopwatch and current lap times. 492 updateTime() 493 if (mTime != null) { 494 mTime!!.update() 495 } 496 val stopwatch = stopwatch 497 if (!stopwatch.isReset) { 498 startUpdatingTime() 499 } 500 501 // Adjust the visibility of the list of laps. 502 showOrHideLaps(stopwatch.isReset) 503 504 // Update button states. 505 updateFab(updateTypes) 506 } 507 508 /** 509 * This runnable periodically updates times throughout the UI. It stops these updates when the 510 * stopwatch is no longer running. 511 */ 512 private inner class TimeUpdateRunnable : Runnable { 513 override fun run() { 514 val startTime = Utils.now() 515 updateTime() 516 517 // Blink text iff the stopwatch is paused and not pressed. 518 val touchTarget: View = if (mTime != null) mTime!! else mStopwatchWrapper 519 val stopwatch = stopwatch 520 val blink = (stopwatch.isPaused && startTime % 1000 < 500 && !touchTarget.isPressed()) 521 522 if (blink) { 523 mMainTimeText.setAlpha(0f) 524 mHundredthsTimeText.setAlpha(0f) 525 } else { 526 mMainTimeText.setAlpha(1f) 527 mHundredthsTimeText.setAlpha(1f) 528 } 529 530 if (!stopwatch.isReset) { 531 val period = (if (stopwatch.isPaused) { 532 REDRAW_PERIOD_PAUSED 533 } else { 534 REDRAW_PERIOD_RUNNING 535 }).toLong() 536 val endTime = Utils.now() 537 val delay: Long = max(0, startTime + period - endTime).toLong() 538 mMainTimeText.postDelayed(this, delay) 539 } 540 } 541 } 542 543 /** 544 * Acquire or release the wake lock based on the tab state. 545 */ 546 private inner class TabWatcher : TabListener { 547 override fun selectedTabChanged( 548 oldSelectedTab: UiDataModel.Tab, 549 newSelectedTab: UiDataModel.Tab 550 ) { 551 adjustWakeLock() 552 } 553 } 554 555 /** 556 * Update the user interface in response to a stopwatch change. 557 */ 558 private inner class StopwatchWatcher : StopwatchListener { 559 override fun stopwatchUpdated(before: Stopwatch, after: Stopwatch) { 560 if (after.isReset) { 561 // Ensure the drop shadow is hidden when the stopwatch is reset. 562 setTabScrolledToTop(true) 563 if (DataModel.dataModel.isApplicationInForeground) { 564 updateUI(FabContainer.BUTTONS_IMMEDIATE) 565 } 566 return 567 } 568 if (DataModel.dataModel.isApplicationInForeground) { 569 updateUI(FabContainer.FAB_MORPH or FabContainer.BUTTONS_IMMEDIATE) 570 } 571 } 572 573 override fun lapAdded(lap: Lap) { 574 } 575 } 576 577 /** 578 * Toggles stopwatch state when user taps stopwatch. 579 */ 580 private inner class TimeClickListener : View.OnClickListener { 581 582 override fun onClick(view: View?) { 583 if (stopwatch.isRunning) { 584 DataModel.dataModel.pauseStopwatch() 585 } else { 586 DataModel.dataModel.startStopwatch() 587 } 588 } 589 } 590 591 /** 592 * Checks if the user is pressing inside of the stopwatch circle. 593 */ 594 private inner class CircleTouchListener : View.OnTouchListener { 595 596 override fun onTouch(view: View, event: MotionEvent): Boolean { 597 val actionMasked: Int = event.getActionMasked() 598 if (actionMasked != MotionEvent.ACTION_DOWN) { 599 return false 600 } 601 val rX: Float = view.getWidth() / 2f 602 val rY: Float = (view.getHeight() - view.getPaddingBottom()) / 2f 603 val r = min(rX, rY) 604 605 val x: Float = event.getX() - rX 606 val y: Float = event.getY() - rY 607 608 val inCircle = (x / r.toDouble()).pow(2.0) + (y / r.toDouble()).pow(2.0) <= 1.0 609 610 // Consume the event if it is outside the circle 611 return !inCircle 612 } 613 } 614 615 /** 616 * Updates the vertical scroll state of this tab in the [UiDataModel] as the user scrolls 617 * the recyclerview or when the size/position of elements within the recyclerview changes. 618 */ 619 private inner class ScrollPositionWatcher : 620 RecyclerView.OnScrollListener(), View.OnLayoutChangeListener { 621 622 override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 623 setTabScrolledToTop(Utils.isScrolledToTop(mLapsList)) 624 } 625 626 override fun onLayoutChange( 627 v: View?, 628 left: Int, 629 top: Int, 630 right: Int, 631 bottom: Int, 632 oldLeft: Int, 633 oldTop: Int, 634 oldRight: Int, 635 oldBottom: Int 636 ) { 637 setTabScrolledToTop(Utils.isScrolledToTop(mLapsList)) 638 } 639 } 640 641 /** 642 * Draws a tinting gradient over the bottom of the stopwatch laps list. This reduces the 643 * contrast between floating buttons and the laps list content. 644 */ 645 private class GradientItemDecoration internal constructor(context: Context) 646 : RecyclerView.ItemDecoration() { 647 648 /** 649 * A reusable array of control point colors that define the gradient. It is based on the 650 * background color of the window and thus recomputed each time that color is changed. 651 */ 652 private val mGradientColors = IntArray(ALPHAS.size) 653 654 /** The drawable that produces the tinting gradient effect of this decoration. */ 655 private val mGradient: GradientDrawable = GradientDrawable() 656 657 /** The height of the gradient; sized relative to the fab height. */ 658 private val mGradientHeight: Int 659 660 init { 661 mGradient.setOrientation(TOP_BOTTOM) 662 updateGradientColors(ThemeUtils.resolveColor(context, android.R.attr.windowBackground)) 663 664 val resources: Resources = context.getResources() 665 val fabHeight: Int = resources.getDimensionPixelSize(R.dimen.fab_height) 666 mGradientHeight = (fabHeight * 1.2f).roundToInt() 667 } 668 669 override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 670 super.onDrawOver(c, parent, state) 671 672 val w: Int = parent.getWidth() 673 val h: Int = parent.getHeight() 674 675 mGradient.setBounds(0, h - mGradientHeight, w, h) 676 mGradient.draw(c) 677 } 678 679 /** 680 * Given a `baseColor`, compute a gradient of tinted colors that define the fade 681 * effect to apply to the bottom of the lap list. 682 * 683 * @param baseColor a base color to which the gradient tint should be applied 684 */ 685 fun updateGradientColors(@ColorInt baseColor: Int) { 686 // Compute the tinted colors that form the gradient. 687 mGradientColors.indices.forEach { i -> 688 mGradientColors[i] = ColorUtils.setAlphaComponent(baseColor, ALPHAS[i]) 689 } 690 691 // Set the gradient colors into the drawable. 692 mGradient.setColors(mGradientColors) 693 } 694 695 companion object { 696 // 0% - 25% of gradient length -> opacity changes from 0% to 50% 697 // 25% - 90% of gradient length -> opacity changes from 50% to 100% 698 // 90% - 100% of gradient length -> opacity remains at 100% 699 private val ALPHAS = intArrayOf( 700 0x00, // 0% 701 0x1A, // 10% 702 0x33, // 20% 703 0x4D, // 30% 704 0x66, // 40% 705 0x80, // 50% 706 0x89, // 53.8% 707 0x93, // 57.6% 708 0x9D, // 61.5% 709 0xA7, // 65.3% 710 0xB1, // 69.2% 711 0xBA, // 73.0% 712 0xC4, // 76.9% 713 0xCE, // 80.7% 714 0xD8, // 84.6% 715 0xE2, // 88.4% 716 0xEB, // 92.3% 717 0xF5, // 96.1% 718 0xFF, // 100% 719 0xFF, // 100% 720 0xFF) 721 } 722 } 723 724 companion object { 725 /** Milliseconds between redraws while running. */ 726 private const val REDRAW_PERIOD_RUNNING = 25 727 728 /** Milliseconds between redraws while paused. */ 729 private const val REDRAW_PERIOD_PAUSED = 500 730 } 731 }