Refactor animations in QSTileViewImpl
Now the color animations in the tile (minus the icon) happen all in a
single animator. That way, they are all in sync and we don't have race
conditions if two states are pushed really fast.
Test: manual
Test: atest com.android.systemui.qs
Fixes: 187459434
Change-Id: I1987b13c31c8ce695aa3199e41262a8f4ab80ab0
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 2d777a5..b3ec39f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -16,6 +16,8 @@
package com.android.systemui.qs.tileimpl
+import android.animation.ArgbEvaluator
+import android.animation.PropertyValuesHolder
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
@@ -43,6 +45,7 @@
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTile.BooleanState
import com.android.systemui.plugins.qs.QSTileView
+import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
import java.util.Objects
private const val TAG = "QSTileViewImpl"
@@ -54,6 +57,10 @@
companion object {
private const val INVALID = -1
+ private const val BACKGROUND_NAME = "background"
+ private const val LABEL_NAME = "label"
+ private const val SECONDARY_LABEL_NAME = "secondaryLabel"
+ private const val CHEVRON_NAME = "chevron"
}
override var heightOverride: Int = HeightOverrideable.NO_OVERRIDE
@@ -83,9 +90,19 @@
private lateinit var ripple: RippleDrawable
private lateinit var colorBackgroundDrawable: Drawable
private var paintColor: Int = 0
- private var paintAnimator: ValueAnimator? = null
- private var labelAnimator: ValueAnimator? = null
- private var secondaryLabelAnimator: ValueAnimator? = null
+ private val singleAnimator: ValueAnimator = ValueAnimator().apply {
+ setDuration(QS_ANIM_LENGTH)
+ addUpdateListener { animation ->
+ setAllColors(
+ // These casts will throw an exception if some property is missing. We should
+ // always have all properties.
+ animation.getAnimatedValue(BACKGROUND_NAME) as Int,
+ animation.getAnimatedValue(LABEL_NAME) as Int,
+ animation.getAnimatedValue(SECONDARY_LABEL_NAME) as Int,
+ animation.getAnimatedValue(CHEVRON_NAME) as Int
+ )
+ }
+ }
private var accessibilityClass: String? = null
private var stateDescriptionDeltas: CharSequence? = null
@@ -104,8 +121,7 @@
clipToPadding = false
isFocusable = true
background = createTileBackground()
- paintColor = getCircleColor(QSTile.State.DEFAULT_STATE)
- colorBackgroundDrawable.setTint(paintColor)
+ setColor(getBackgroundColorForState(QSTile.State.DEFAULT_STATE))
val padding = resources.getDimensionPixelSize(R.dimen.qs_tile_padding)
val startPadding = resources.getDimensionPixelSize(R.dimen.qs_tile_start_padding)
@@ -166,8 +182,8 @@
labelContainer.ignoreLastView = true
secondaryLabel.alpha = 0f
}
- label.setTextColor(getLabelColor(QSTile.State.DEFAULT_STATE))
- secondaryLabel.setTextColor(getSecondaryLabelColor(QSTile.State.DEFAULT_STATE))
+ setLabelColor(getLabelColorForState(QSTile.State.DEFAULT_STATE))
+ setSecondaryLabelColor(getSecondaryLabelColorForState(QSTile.State.DEFAULT_STATE))
addView(labelContainer)
}
@@ -176,6 +192,7 @@
.inflate(R.layout.qs_tile_side_icon, this, false) as ViewGroup
customDrawableView = sideView.requireViewById(R.id.customDrawable)
chevronView = sideView.requireViewById(R.id.chevron)
+ setChevronColor(getChevronColorForState(QSTile.State.DEFAULT_STATE))
addView(sideView)
}
@@ -322,19 +339,6 @@
icon.setIcon(state, allowAnimations)
contentDescription = state.contentDescription
- // Background color animation
- val newColor = getCircleColor(state.state)
- if (allowAnimations) {
- animateBackground(newColor)
- } else {
- clearBackgroundAnimator()
- colorBackgroundDrawable.setTintList(ColorStateList.valueOf(newColor)).also {
- paintColor = newColor
- }
- paintColor = newColor
- }
- //
-
// State handling and description
val stateDescription = StringBuilder()
val stateText = getStateText(state)
@@ -383,23 +387,80 @@
}
}
- if (allowAnimations) {
- animateLabelColor(getLabelColor(state.state))
- animateSecondaryLabelColor(getSecondaryLabelColor(state.state))
- } else {
- label.setTextColor(getLabelColor(state.state))
- secondaryLabel.setTextColor(getSecondaryLabelColor(state.state))
+ // Colors
+ if (state.state != lastState) {
+ singleAnimator.cancel()
+ if (allowAnimations) {
+ singleAnimator.setValues(
+ colorValuesHolder(
+ BACKGROUND_NAME,
+ paintColor,
+ getBackgroundColorForState(state.state)
+ ),
+ colorValuesHolder(
+ LABEL_NAME,
+ label.currentTextColor,
+ getLabelColorForState(state.state)
+ ),
+ colorValuesHolder(
+ SECONDARY_LABEL_NAME,
+ label.currentTextColor,
+ getSecondaryLabelColorForState(state.state)
+ ),
+ colorValuesHolder(
+ CHEVRON_NAME,
+ chevronView.imageTintList?.defaultColor ?: 0,
+ getChevronColorForState(state.state)
+ )
+ )
+ singleAnimator.start()
+ } else {
+ setAllColors(
+ getBackgroundColorForState(state.state),
+ getLabelColorForState(state.state),
+ getLabelColorForState(state.state),
+ getChevronColorForState(state.state)
+ )
+ }
}
// Right side icon
loadSideViewDrawableIfNecessary(state)
- chevronView.imageTintList = ColorStateList.valueOf(getSecondaryLabelColor(state.state))
label.isEnabled = !state.disabledByPolicy
lastState = state.state
}
+ private fun setAllColors(
+ backgroundColor: Int,
+ labelColor: Int,
+ secondaryLabelColor: Int,
+ chevronColor: Int
+ ) {
+ setColor(backgroundColor)
+ setLabelColor(labelColor)
+ setSecondaryLabelColor(secondaryLabelColor)
+ setChevronColor(chevronColor)
+ }
+
+ private fun setColor(color: Int) {
+ colorBackgroundDrawable.setTint(color)
+ paintColor = color
+ }
+
+ private fun setLabelColor(color: Int) {
+ label.setTextColor(color)
+ }
+
+ private fun setSecondaryLabelColor(color: Int) {
+ secondaryLabel.setTextColor(color)
+ }
+
+ private fun setChevronColor(color: Int) {
+ chevronView.imageTintList = ColorStateList.valueOf(color)
+ }
+
private fun loadSideViewDrawableIfNecessary(state: QSTile.State) {
if (state.sideViewCustomDrawable != null) {
customDrawableView.setImageDrawable(state.sideViewCustomDrawable)
@@ -446,63 +507,7 @@
return locInScreen.get(1) >= -height
}
- private fun animateBackground(newBackgroundColor: Int) {
- if (newBackgroundColor != paintColor) {
- clearBackgroundAnimator()
- paintAnimator = ValueAnimator.ofArgb(paintColor, newBackgroundColor)
- .setDuration(QSIconViewImpl.QS_ANIM_LENGTH).apply {
- addUpdateListener { animation: ValueAnimator ->
- val c = animation.animatedValue as Int
- colorBackgroundDrawable.setTintList(ColorStateList.valueOf(c)).also {
- paintColor = c
- }
- }
- start()
- }
- }
- }
-
- private fun animateLabelColor(color: Int) {
- val currentColor = label.textColors.defaultColor
- if (currentColor != color) {
- clearLabelAnimator()
- labelAnimator = ValueAnimator.ofArgb(currentColor, color)
- .setDuration(QSIconViewImpl.QS_ANIM_LENGTH).apply {
- addUpdateListener {
- label.setTextColor(it.animatedValue as Int)
- }
- start()
- }
- }
- }
-
- private fun animateSecondaryLabelColor(color: Int) {
- val currentColor = secondaryLabel.textColors.defaultColor
- if (currentColor != color) {
- clearSecondaryLabelAnimator()
- secondaryLabelAnimator = ValueAnimator.ofArgb(currentColor, color)
- .setDuration(QSIconViewImpl.QS_ANIM_LENGTH).apply {
- addUpdateListener {
- secondaryLabel.setTextColor(it.animatedValue as Int)
- }
- start()
- }
- }
- }
-
- private fun clearBackgroundAnimator() {
- paintAnimator?.cancel()?.also { paintAnimator = null }
- }
-
- private fun clearLabelAnimator() {
- labelAnimator?.cancel()?.also { labelAnimator = null }
- }
-
- private fun clearSecondaryLabelAnimator() {
- secondaryLabelAnimator?.cancel()?.also { secondaryLabelAnimator = null }
- }
-
- private fun getCircleColor(state: Int): Int {
+ private fun getBackgroundColorForState(state: Int): Int {
return when (state) {
Tile.STATE_ACTIVE -> colorActive
Tile.STATE_INACTIVE -> colorInactive
@@ -514,7 +519,7 @@
}
}
- private fun getLabelColor(state: Int): Int {
+ private fun getLabelColorForState(state: Int): Int {
return when (state) {
Tile.STATE_ACTIVE -> colorLabelActive
Tile.STATE_INACTIVE -> colorLabelInactive
@@ -526,7 +531,7 @@
}
}
- private fun getSecondaryLabelColor(state: Int): Int {
+ private fun getSecondaryLabelColorForState(state: Int): Int {
return when (state) {
Tile.STATE_ACTIVE -> colorLabelActive
Tile.STATE_INACTIVE, Tile.STATE_UNAVAILABLE -> colorLabelUnavailable
@@ -536,4 +541,12 @@
}
}
}
+
+ private fun getChevronColorForState(state: Int): Int = getSecondaryLabelColorForState(state)
+}
+
+private fun colorValuesHolder(name: String, vararg values: Int): PropertyValuesHolder {
+ return PropertyValuesHolder.ofInt(name, *values).apply {
+ setEvaluator(ArgbEvaluator.getInstance())
+ }
}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
index e5e2e53..126dca5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
@@ -20,6 +20,7 @@
import android.graphics.drawable.Drawable
import android.service.quicksettings.Tile
import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
import android.text.TextUtils
import android.view.View
import androidx.test.filters.SmallTest
@@ -36,6 +37,7 @@
@RunWith(AndroidTestingRunner::class)
@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
class QSTileViewImplTest : SysuiTestCase() {
@Mock