• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 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.animation
18 
19 import android.animation.ArgbEvaluator
20 import android.animation.ValueAnimator
21 import android.animation.ValueAnimator.AnimatorUpdateListener
22 import android.content.Context
23 import android.content.res.ColorStateList
24 import android.content.res.Configuration
25 import android.content.res.Configuration.UI_MODE_NIGHT_YES
26 import android.graphics.drawable.RippleDrawable
27 import com.android.internal.R
28 import com.android.internal.annotations.VisibleForTesting
29 import com.android.settingslib.Utils
30 import com.android.systemui.Flags
31 import com.android.systemui.media.controls.ui.view.MediaViewHolder
32 import com.android.systemui.monet.ColorScheme
33 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect
34 import com.android.systemui.surfaceeffects.ripple.MultiRippleController
35 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController
36 
37 /**
38  * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme]
39  * is triggered.
40  */
41 interface ColorTransition {
42     fun updateColorScheme(scheme: ColorScheme?): Boolean
43 }
44 
45 /**
46  * A [ColorTransition] that animates between two specific colors. It uses a ValueAnimator to execute
47  * the animation and interpolate between the source color and the target color.
48  *
49  * Selection of the target color from the scheme, and application of the interpolated color are
50  * delegated to callbacks.
51  */
52 open class AnimatingColorTransition(
53     private val defaultColor: Int,
54     private val extractColor: (ColorScheme) -> Int,
55     private val applyColor: (Int) -> Unit,
56 ) : AnimatorUpdateListener, ColorTransition {
57 
58     private val argbEvaluator = ArgbEvaluator()
59     private val valueAnimator = buildAnimator()
60     var sourceColor: Int = defaultColor
61     var currentColor: Int = defaultColor
62     var targetColor: Int = defaultColor
63 
onAnimationUpdatenull64     override fun onAnimationUpdate(animation: ValueAnimator) {
65         currentColor =
66             argbEvaluator.evaluate(animation.animatedFraction, sourceColor, targetColor) as Int
67         applyColor(currentColor)
68     }
69 
updateColorSchemenull70     override fun updateColorScheme(scheme: ColorScheme?): Boolean {
71         val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme)
72         if (newTargetColor != targetColor) {
73             sourceColor = currentColor
74             targetColor = newTargetColor
75             valueAnimator.cancel()
76             valueAnimator.start()
77             return true
78         }
79         return false
80     }
81 
82     init {
83         applyColor(defaultColor)
84     }
85 
86     @VisibleForTesting
buildAnimatornull87     open fun buildAnimator(): ValueAnimator {
88         val animator = ValueAnimator.ofFloat(0f, 1f)
89         animator.duration = 333
90         animator.addUpdateListener(this)
91         return animator
92     }
93 }
94 
95 typealias AnimatingColorTransitionFactory =
96     (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition
97 
98 /**
99  * ColorSchemeTransition constructs a ColorTransition for each color in the scheme that needs to be
100  * transitioned when changed. It also sets up the assignment functions for sending the sending the
101  * interpolated colors to the appropriate views.
102  */
103 class ColorSchemeTransition
104 internal constructor(
105     private val context: Context,
106     private val mediaViewHolder: MediaViewHolder,
107     private val multiRippleController: MultiRippleController,
108     private val turbulenceNoiseController: TurbulenceNoiseController,
109     animatingColorTransitionFactory: AnimatingColorTransitionFactory,
110 ) {
111     constructor(
112         context: Context,
113         mediaViewHolder: MediaViewHolder,
114         multiRippleController: MultiRippleController,
115         turbulenceNoiseController: TurbulenceNoiseController,
116     ) : this(
117         context,
118         mediaViewHolder,
119         multiRippleController,
120         turbulenceNoiseController,
121         ::AnimatingColorTransition,
122     )
123 
124     var loadingEffect: LoadingEffect? = null
125 
126     // Defaults may be briefly visible before loading a new player's colors
127     private val backgroundDefault = context.getColor(R.color.system_on_surface_light)
128     private val primaryDefault = context.getColor(R.color.system_primary_dark)
129     private val onPrimaryDefault = context.getColor(R.color.system_on_primary_dark)
130 
<lambda>null131     private val backgroundColor: AnimatingColorTransition by lazy {
132         animatingColorTransitionFactory(backgroundDefault, ::backgroundFromScheme) { color ->
133             mediaViewHolder.albumView.backgroundTintList = ColorStateList.valueOf(color)
134         }
135     }
136 
<lambda>null137     private val primaryColor: AnimatingColorTransition by lazy {
138         animatingColorTransitionFactory(primaryDefault, ::primaryFromScheme) { primaryColor ->
139             val primaryColorList = ColorStateList.valueOf(primaryColor)
140             mediaViewHolder.actionPlayPause.backgroundTintList = primaryColorList
141             mediaViewHolder.seamlessButton.backgroundTintList = primaryColorList
142             (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let {
143                 it.setColor(primaryColorList)
144                 it.effectColor = primaryColorList
145             }
146             mediaViewHolder.seekBar.progressBackgroundTintList = primaryColorList
147         }
148     }
149 
<lambda>null150     private val onPrimaryColor: AnimatingColorTransition by lazy {
151         animatingColorTransitionFactory(onPrimaryDefault, ::onPrimaryFromScheme) { onPrimaryColor ->
152             val onPrimaryColorList = ColorStateList.valueOf(onPrimaryColor)
153             mediaViewHolder.actionPlayPause.imageTintList = onPrimaryColorList
154             mediaViewHolder.seamlessText.setTextColor(onPrimaryColor)
155             mediaViewHolder.seamlessIcon.imageTintList = onPrimaryColorList
156         }
157     }
158 
159     // TODO(media_controls_a11y_colors): remove the below color definitions
160     private val bgColor =
161         context.getColor(com.google.android.material.R.color.material_dynamic_neutral20)
<lambda>null162     private val surfaceColor: AnimatingColorTransition by lazy {
163         animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor ->
164             val colorList = ColorStateList.valueOf(surfaceColor)
165             mediaViewHolder.seamlessIcon.imageTintList = colorList
166             mediaViewHolder.seamlessText.setTextColor(surfaceColor)
167             mediaViewHolder.albumView.backgroundTintList = colorList
168             mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor)
169         }
170     }
171 
<lambda>null172     private val accentPrimary: AnimatingColorTransition by lazy {
173         animatingColorTransitionFactory(
174             loadDefaultColor(R.attr.textColorPrimary),
175             ::accentPrimaryFromScheme,
176         ) { accentPrimary ->
177             val accentColorList = ColorStateList.valueOf(accentPrimary)
178             mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
179             mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
180             multiRippleController.updateColor(accentPrimary)
181             turbulenceNoiseController.updateNoiseColor(accentPrimary)
182             loadingEffect?.updateColor(accentPrimary)
183         }
184     }
185 
<lambda>null186     private val accentSecondary: AnimatingColorTransition by lazy {
187         animatingColorTransitionFactory(
188             loadDefaultColor(R.attr.textColorPrimary),
189             ::accentSecondaryFromScheme,
190         ) { accentSecondary ->
191             val colorList = ColorStateList.valueOf(accentSecondary)
192             (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let {
193                 it.setColor(colorList)
194                 it.effectColor = colorList
195             }
196         }
197     }
198 
<lambda>null199     private val colorSeamless: AnimatingColorTransition by lazy {
200         animatingColorTransitionFactory(
201             loadDefaultColor(R.attr.textColorPrimary),
202             { colorScheme: ColorScheme ->
203                 // A1-100 dark in dark theme, A1-200 in light theme
204                 if (
205                     context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
206                         UI_MODE_NIGHT_YES
207                 )
208                     colorScheme.accent1.s100
209                 else colorScheme.accent1.s200
210             },
211             { seamlessColor: Int ->
212                 val accentColorList = ColorStateList.valueOf(seamlessColor)
213                 mediaViewHolder.seamlessButton.backgroundTintList = accentColorList
214             },
215         )
216     }
217 
<lambda>null218     private val textPrimary: AnimatingColorTransition by lazy {
219         animatingColorTransitionFactory(
220             loadDefaultColor(R.attr.textColorPrimary),
221             ::textPrimaryFromScheme,
222         ) { textPrimary ->
223             mediaViewHolder.titleText.setTextColor(textPrimary)
224             val textColorList = ColorStateList.valueOf(textPrimary)
225             mediaViewHolder.seekBar.thumb.setTintList(textColorList)
226             mediaViewHolder.seekBar.progressTintList = textColorList
227             mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList)
228             mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList)
229             for (button in mediaViewHolder.getTransparentActionButtons()) {
230                 button.imageTintList = textColorList
231             }
232             mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary)
233         }
234     }
235 
<lambda>null236     private val textPrimaryInverse: AnimatingColorTransition by lazy {
237         animatingColorTransitionFactory(
238             loadDefaultColor(R.attr.textColorPrimaryInverse),
239             ::textPrimaryInverseFromScheme,
240         ) { textPrimaryInverse ->
241             mediaViewHolder.actionPlayPause.imageTintList =
242                 ColorStateList.valueOf(textPrimaryInverse)
243         }
244     }
245 
<lambda>null246     private val textSecondary: AnimatingColorTransition by lazy {
247         animatingColorTransitionFactory(
248             loadDefaultColor(R.attr.textColorSecondary),
249             ::textSecondaryFromScheme,
250         ) { textSecondary ->
251             mediaViewHolder.artistText.setTextColor(textSecondary)
252         }
253     }
254 
<lambda>null255     private val textTertiary: AnimatingColorTransition by lazy {
256         animatingColorTransitionFactory(
257             loadDefaultColor(R.attr.textColorTertiary),
258             ::textTertiaryFromScheme,
259         ) { textTertiary ->
260             mediaViewHolder.seekBar.progressBackgroundTintList =
261                 ColorStateList.valueOf(textTertiary)
262         }
263     }
264 
getDeviceIconColornull265     fun getDeviceIconColor(): Int {
266         if (Flags.mediaControlsA11yColors()) {
267             return onPrimaryColor.targetColor
268         }
269         return surfaceColor.targetColor
270     }
271 
getAppIconColornull272     fun getAppIconColor(): Int {
273         if (Flags.mediaControlsA11yColors()) {
274             return primaryColor.targetColor
275         }
276         return accentPrimary.targetColor
277     }
278 
getSurfaceEffectColornull279     fun getSurfaceEffectColor(): Int {
280         if (Flags.mediaControlsA11yColors()) {
281             return primaryColor.targetColor
282         }
283         return accentPrimary.targetColor
284     }
285 
getGutsTextColornull286     fun getGutsTextColor(): Int {
287         if (Flags.mediaControlsA11yColors()) {
288             return context.getColor(com.android.systemui.res.R.color.media_on_background)
289         }
290         return textPrimary.targetColor
291     }
292 
getColorTransitionsnull293     private fun getColorTransitions(): Array<AnimatingColorTransition> {
294         return if (Flags.mediaControlsA11yColors()) {
295             arrayOf(backgroundColor, primaryColor, onPrimaryColor)
296         } else {
297             arrayOf(
298                 surfaceColor,
299                 colorSeamless,
300                 accentPrimary,
301                 accentSecondary,
302                 textPrimary,
303                 textPrimaryInverse,
304                 textSecondary,
305                 textTertiary,
306             )
307         }
308     }
309 
loadDefaultColornull310     private fun loadDefaultColor(id: Int): Int {
311         return Utils.getColorAttr(context, id).defaultColor
312     }
313 
updateColorSchemenull314     fun updateColorScheme(colorScheme: ColorScheme?): Boolean {
315         var anyChanged = false
316         getColorTransitions().forEach {
317             val isChanged = it.updateColorScheme(colorScheme)
318 
319             // Ignore changes to colorSeamless, since that is expected when toggling dark mode
320             // TODO(media_controls_a11y_colors): remove, not necessary
321             if (it == colorSeamless) return@forEach
322 
323             anyChanged = isChanged || anyChanged
324         }
325         if (Flags.mediaControlsA11yColors()) {
326             getSurfaceEffectColor().let {
327                 multiRippleController.updateColor(it)
328                 turbulenceNoiseController.updateNoiseColor(it)
329                 loadingEffect?.updateColor(it)
330             }
331             mediaViewHolder.gutsViewHolder.setTextColor(getGutsTextColor())
332             colorScheme?.let { mediaViewHolder.gutsViewHolder.setColors(it) }
333         } else {
334             colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme }
335         }
336         return anyChanged
337     }
338 }
339