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