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