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.media.controls.ui.view.MediaViewHolder
31 import com.android.systemui.monet.ColorScheme
32 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect
33 import com.android.systemui.surfaceeffects.ripple.MultiRippleController
34 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController
35 
36 /**
37  * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme]
38  * is triggered.
39  */
40 interface ColorTransition {
41     fun updateColorScheme(scheme: ColorScheme?): Boolean
42 }
43 
44 /**
45  * A [ColorTransition] that animates between two specific colors. It uses a ValueAnimator to execute
46  * the animation and interpolate between the source color and the target color.
47  *
48  * Selection of the target color from the scheme, and application of the interpolated color are
49  * delegated to callbacks.
50  */
51 open class AnimatingColorTransition(
52     private val defaultColor: Int,
53     private val extractColor: (ColorScheme) -> Int,
54     private val applyColor: (Int) -> Unit
55 ) : AnimatorUpdateListener, ColorTransition {
56 
57     private val argbEvaluator = ArgbEvaluator()
58     private val valueAnimator = buildAnimator()
59     var sourceColor: Int = defaultColor
60     var currentColor: Int = defaultColor
61     var targetColor: Int = defaultColor
62 
onAnimationUpdatenull63     override fun onAnimationUpdate(animation: ValueAnimator) {
64         currentColor =
65             argbEvaluator.evaluate(animation.animatedFraction, sourceColor, targetColor) as Int
66         applyColor(currentColor)
67     }
68 
updateColorSchemenull69     override fun updateColorScheme(scheme: ColorScheme?): Boolean {
70         val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme)
71         if (newTargetColor != targetColor) {
72             sourceColor = currentColor
73             targetColor = newTargetColor
74             valueAnimator.cancel()
75             valueAnimator.start()
76             return true
77         }
78         return false
79     }
80 
81     init {
82         applyColor(defaultColor)
83     }
84 
85     @VisibleForTesting
buildAnimatornull86     open fun buildAnimator(): ValueAnimator {
87         val animator = ValueAnimator.ofFloat(0f, 1f)
88         animator.duration = 333
89         animator.addUpdateListener(this)
90         return animator
91     }
92 }
93 
94 typealias AnimatingColorTransitionFactory =
95     (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition
96 
97 /**
98  * ColorSchemeTransition constructs a ColorTransition for each color in the scheme that needs to be
99  * transitioned when changed. It also sets up the assignment functions for sending the sending the
100  * interpolated colors to the appropriate views.
101  */
102 class ColorSchemeTransition
103 internal constructor(
104     private val context: Context,
105     private val mediaViewHolder: MediaViewHolder,
106     private val multiRippleController: MultiRippleController,
107     private val turbulenceNoiseController: TurbulenceNoiseController,
108     animatingColorTransitionFactory: AnimatingColorTransitionFactory
109 ) {
110     constructor(
111         context: Context,
112         mediaViewHolder: MediaViewHolder,
113         multiRippleController: MultiRippleController,
114         turbulenceNoiseController: TurbulenceNoiseController
115     ) : this(
116         context,
117         mediaViewHolder,
118         multiRippleController,
119         turbulenceNoiseController,
120         ::AnimatingColorTransition
121     )
122     var loadingEffect: LoadingEffect? = null
123 
124     val bgColor = context.getColor(com.google.android.material.R.color.material_dynamic_neutral20)
125     val surfaceColor =
surfaceColornull126         animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor ->
127             val colorList = ColorStateList.valueOf(surfaceColor)
128             mediaViewHolder.seamlessIcon.imageTintList = colorList
129             mediaViewHolder.seamlessText.setTextColor(surfaceColor)
130             mediaViewHolder.albumView.backgroundTintList = colorList
131             mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor)
132         }
133     val accentPrimary =
134         animatingColorTransitionFactory(
135             loadDefaultColor(R.attr.textColorPrimary),
136             ::accentPrimaryFromScheme
accentPrimarynull137         ) { accentPrimary ->
138             val accentColorList = ColorStateList.valueOf(accentPrimary)
139             mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
140             mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
141             multiRippleController.updateColor(accentPrimary)
142             turbulenceNoiseController.updateNoiseColor(accentPrimary)
143             loadingEffect?.updateColor(accentPrimary)
144         }
145 
146     val accentSecondary =
147         animatingColorTransitionFactory(
148             loadDefaultColor(R.attr.textColorPrimary),
149             ::accentSecondaryFromScheme
accentSecondarynull150         ) { accentSecondary ->
151             val colorList = ColorStateList.valueOf(accentSecondary)
152             (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let {
153                 it.setColor(colorList)
154                 it.effectColor = colorList
155             }
156         }
157 
158     val colorSeamless =
159         animatingColorTransitionFactory(
160             loadDefaultColor(R.attr.textColorPrimary),
colorSchemenull161             { colorScheme: ColorScheme ->
162                 // A1-100 dark in dark theme, A1-200 in light theme
163                 if (
164                     context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
165                         UI_MODE_NIGHT_YES
166                 )
167                     colorScheme.accent1.s100
168                 else colorScheme.accent1.s200
169             },
seamlessColornull170             { seamlessColor: Int ->
171                 val accentColorList = ColorStateList.valueOf(seamlessColor)
172                 mediaViewHolder.seamlessButton.backgroundTintList = accentColorList
173             }
174         )
175 
176     val textPrimary =
177         animatingColorTransitionFactory(
178             loadDefaultColor(R.attr.textColorPrimary),
179             ::textPrimaryFromScheme
textPrimarynull180         ) { textPrimary ->
181             mediaViewHolder.titleText.setTextColor(textPrimary)
182             val textColorList = ColorStateList.valueOf(textPrimary)
183             mediaViewHolder.seekBar.thumb.setTintList(textColorList)
184             mediaViewHolder.seekBar.progressTintList = textColorList
185             mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList)
186             mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList)
187             for (button in mediaViewHolder.getTransparentActionButtons()) {
188                 button.imageTintList = textColorList
189             }
190             mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary)
191         }
192 
193     val textPrimaryInverse =
194         animatingColorTransitionFactory(
195             loadDefaultColor(R.attr.textColorPrimaryInverse),
196             ::textPrimaryInverseFromScheme
textPrimaryInversenull197         ) { textPrimaryInverse ->
198             mediaViewHolder.actionPlayPause.imageTintList =
199                 ColorStateList.valueOf(textPrimaryInverse)
200         }
201 
202     val textSecondary =
203         animatingColorTransitionFactory(
204             loadDefaultColor(R.attr.textColorSecondary),
205             ::textSecondaryFromScheme
textSecondarynull206         ) { textSecondary ->
207             mediaViewHolder.artistText.setTextColor(textSecondary)
208         }
209 
210     val textTertiary =
211         animatingColorTransitionFactory(
212             loadDefaultColor(R.attr.textColorTertiary),
213             ::textTertiaryFromScheme
textTertiarynull214         ) { textTertiary ->
215             mediaViewHolder.seekBar.progressBackgroundTintList =
216                 ColorStateList.valueOf(textTertiary)
217         }
218 
219     val colorTransitions =
220         arrayOf(
221             surfaceColor,
222             colorSeamless,
223             accentPrimary,
224             accentSecondary,
225             textPrimary,
226             textPrimaryInverse,
227             textSecondary,
228             textTertiary,
229         )
230 
loadDefaultColornull231     private fun loadDefaultColor(id: Int): Int {
232         return Utils.getColorAttr(context, id).defaultColor
233     }
234 
updateColorSchemenull235     fun updateColorScheme(colorScheme: ColorScheme?): Boolean {
236         var anyChanged = false
237         colorTransitions.forEach {
238             val isChanged = it.updateColorScheme(colorScheme)
239 
240             // Ignore changes to colorSeamless, since that is expected when toggling dark mode
241             if (it == colorSeamless) return@forEach
242 
243             anyChanged = isChanged || anyChanged
244         }
245         colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme }
246         return anyChanged
247     }
248 }
249