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 }