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.app.animation.Interpolators 25 import com.android.internal.annotations.VisibleForTesting 26 import com.android.systemui.R 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 var animationEnabled: Boolean = true 60 61 init { 62 val seekBarProgressWavelength = 63 holder.seekBar.context.resources 64 .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength) 65 .toFloat() 66 val seekBarProgressAmplitude = 67 holder.seekBar.context.resources 68 .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude) 69 .toFloat() 70 val seekBarProgressPhase = 71 holder.seekBar.context.resources 72 .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase) 73 .toFloat() 74 val seekBarProgressStrokeWidth = 75 holder.seekBar.context.resources 76 .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width) 77 .toFloat() 78 val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress 79 progressDrawable?.let { 80 it.waveLength = seekBarProgressWavelength 81 it.lineAmplitude = seekBarProgressAmplitude 82 it.phaseSpeed = seekBarProgressPhase 83 it.strokeWidth = seekBarProgressStrokeWidth 84 } 85 } 86 87 /** Updates seek bar views when the data model changes. */ 88 @UiThread 89 override fun onChanged(data: SeekBarViewModel.Progress) { 90 val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress 91 if (!data.enabled) { 92 if (holder.seekBar.maxHeight != seekBarDisabledHeight) { 93 holder.seekBar.maxHeight = seekBarDisabledHeight 94 setVerticalPadding(seekBarDisabledVerticalPadding) 95 } 96 holder.seekBar.isEnabled = false 97 progressDrawable?.animate = false 98 holder.seekBar.thumb.alpha = 0 99 holder.seekBar.progress = 0 100 holder.seekBar.contentDescription = "" 101 holder.scrubbingElapsedTimeView.text = "" 102 holder.scrubbingTotalTimeView.text = "" 103 return 104 } 105 106 holder.seekBar.thumb.alpha = if (data.seekAvailable) 255 else 0 107 holder.seekBar.isEnabled = data.seekAvailable 108 progressDrawable?.animate = data.playing && !data.scrubbing && animationEnabled 109 progressDrawable?.transitionEnabled = !data.seekAvailable 110 111 if (holder.seekBar.maxHeight != seekBarEnabledMaxHeight) { 112 holder.seekBar.maxHeight = seekBarEnabledMaxHeight 113 setVerticalPadding(seekBarEnabledVerticalPadding) 114 } 115 116 holder.seekBar.setMax(data.duration) 117 val totalTimeString = 118 DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS) 119 if (data.scrubbing) { 120 holder.scrubbingTotalTimeView.text = totalTimeString 121 } 122 123 data.elapsedTime?.let { 124 if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) { 125 if ( 126 it <= RESET_ANIMATION_THRESHOLD_MS && 127 holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS 128 ) { 129 // This animation resets for every additional update to zero. 130 val animator = buildResetAnimator(it) 131 animator.start() 132 seekBarResetAnimator = animator 133 } else { 134 holder.seekBar.progress = it 135 } 136 } 137 138 val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS) 139 if (data.scrubbing) { 140 holder.scrubbingElapsedTimeView.text = elapsedTimeString 141 } 142 143 holder.seekBar.contentDescription = 144 holder.seekBar.context.getString( 145 R.string.controls_media_seekbar_description, 146 elapsedTimeString, 147 totalTimeString 148 ) 149 } 150 } 151 152 @VisibleForTesting 153 open fun buildResetAnimator(targetTime: Int): Animator { 154 val animator = 155 ObjectAnimator.ofInt( 156 holder.seekBar, 157 "progress", 158 holder.seekBar.progress, 159 targetTime + RESET_ANIMATION_DURATION_MS 160 ) 161 animator.setAutoCancel(true) 162 animator.duration = RESET_ANIMATION_DURATION_MS.toLong() 163 animator.interpolator = Interpolators.EMPHASIZED 164 return animator 165 } 166 167 @UiThread 168 fun setVerticalPadding(padding: Int) { 169 val leftPadding = holder.seekBar.paddingLeft 170 val rightPadding = holder.seekBar.paddingRight 171 val bottomPadding = holder.seekBar.paddingBottom 172 holder.seekBar.setPadding(leftPadding, padding, rightPadding, bottomPadding) 173 } 174 } 175