• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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.systemui.media.controls.models.player
18 
19 import android.animation.Animator
20 import android.animation.ObjectAnimator
21 import android.text.format.DateUtils
22 import androidx.annotation.UiThread
23 import androidx.lifecycle.Observer
24 import com.android.internal.annotations.VisibleForTesting
25 import com.android.systemui.R
26 import com.android.systemui.animation.Interpolators
27 import com.android.systemui.media.controls.ui.SquigglyProgress
28 
29 /**
30  * Observer for changes from SeekBarViewModel.
31  *
32  * <p>Updates the seek bar views in response to changes to the model.
33  */
34 open class SeekBarObserver(private val holder: MediaViewHolder) :
35     Observer<SeekBarViewModel.Progress> {
36 
37     companion object {
38         @JvmStatic val RESET_ANIMATION_DURATION_MS: Int = 750
39         @JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250
40     }
41 
42     val seekBarEnabledMaxHeight =
43         holder.seekBar.context.resources.getDimensionPixelSize(
44             R.dimen.qs_media_enabled_seekbar_height
45         )
46     val seekBarDisabledHeight =
47         holder.seekBar.context.resources.getDimensionPixelSize(
48             R.dimen.qs_media_disabled_seekbar_height
49         )
50     val seekBarEnabledVerticalPadding =
51         holder.seekBar.context.resources.getDimensionPixelSize(
52             R.dimen.qs_media_session_enabled_seekbar_vertical_padding
53         )
54     val seekBarDisabledVerticalPadding =
55         holder.seekBar.context.resources.getDimensionPixelSize(
56             R.dimen.qs_media_session_disabled_seekbar_vertical_padding
57         )
58     var seekBarResetAnimator: Animator? = null
59 
60     init {
61         val seekBarProgressWavelength =
62             holder.seekBar.context.resources
63                 .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength)
64                 .toFloat()
65         val seekBarProgressAmplitude =
66             holder.seekBar.context.resources
67                 .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude)
68                 .toFloat()
69         val seekBarProgressPhase =
70             holder.seekBar.context.resources
71                 .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase)
72                 .toFloat()
73         val seekBarProgressStrokeWidth =
74             holder.seekBar.context.resources
75                 .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width)
76                 .toFloat()
77         val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress
<lambda>null78         progressDrawable?.let {
79             it.waveLength = seekBarProgressWavelength
80             it.lineAmplitude = seekBarProgressAmplitude
81             it.phaseSpeed = seekBarProgressPhase
82             it.strokeWidth = seekBarProgressStrokeWidth
83         }
84     }
85 
86     /** Updates seek bar views when the data model changes. */
87     @UiThread
onChangednull88     override fun onChanged(data: SeekBarViewModel.Progress) {
89         val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress
90         if (!data.enabled) {
91             if (holder.seekBar.maxHeight != seekBarDisabledHeight) {
92                 holder.seekBar.maxHeight = seekBarDisabledHeight
93                 setVerticalPadding(seekBarDisabledVerticalPadding)
94             }
95             holder.seekBar.isEnabled = false
96             progressDrawable?.animate = false
97             holder.seekBar.thumb.alpha = 0
98             holder.seekBar.progress = 0
99             holder.seekBar.contentDescription = ""
100             holder.scrubbingElapsedTimeView.text = ""
101             holder.scrubbingTotalTimeView.text = ""
102             return
103         }
104 
105         holder.seekBar.thumb.alpha = if (data.seekAvailable) 255 else 0
106         holder.seekBar.isEnabled = data.seekAvailable
107         progressDrawable?.animate = data.playing && !data.scrubbing
108         progressDrawable?.transitionEnabled = !data.seekAvailable
109 
110         if (holder.seekBar.maxHeight != seekBarEnabledMaxHeight) {
111             holder.seekBar.maxHeight = seekBarEnabledMaxHeight
112             setVerticalPadding(seekBarEnabledVerticalPadding)
113         }
114 
115         holder.seekBar.setMax(data.duration)
116         val totalTimeString =
117             DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
118         if (data.scrubbing) {
119             holder.scrubbingTotalTimeView.text = totalTimeString
120         }
121 
122         data.elapsedTime?.let {
123             if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) {
124                 if (
125                     it <= RESET_ANIMATION_THRESHOLD_MS &&
126                         holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS
127                 ) {
128                     // This animation resets for every additional update to zero.
129                     val animator = buildResetAnimator(it)
130                     animator.start()
131                     seekBarResetAnimator = animator
132                 } else {
133                     holder.seekBar.progress = it
134                 }
135             }
136 
137             val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
138             if (data.scrubbing) {
139                 holder.scrubbingElapsedTimeView.text = elapsedTimeString
140             }
141 
142             holder.seekBar.contentDescription =
143                 holder.seekBar.context.getString(
144                     R.string.controls_media_seekbar_description,
145                     elapsedTimeString,
146                     totalTimeString
147                 )
148         }
149     }
150 
151     @VisibleForTesting
buildResetAnimatornull152     open fun buildResetAnimator(targetTime: Int): Animator {
153         val animator =
154             ObjectAnimator.ofInt(
155                 holder.seekBar,
156                 "progress",
157                 holder.seekBar.progress,
158                 targetTime + RESET_ANIMATION_DURATION_MS
159             )
160         animator.setAutoCancel(true)
161         animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
162         animator.interpolator = Interpolators.EMPHASIZED
163         return animator
164     }
165 
166     @UiThread
setVerticalPaddingnull167     fun setVerticalPadding(padding: Int) {
168         val leftPadding = holder.seekBar.paddingLeft
169         val rightPadding = holder.seekBar.paddingRight
170         val bottomPadding = holder.seekBar.paddingBottom
171         holder.seekBar.setPadding(leftPadding, padding, rightPadding, bottomPadding)
172     }
173 }
174