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