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