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