Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6009e7c2 authored by Fabian Kozynski's avatar Fabian Kozynski
Browse files

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
parent dd85a79e
Loading
Loading
Loading
Loading
+99 −86
Original line number Diff line number Diff line
@@ -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.QSIconView
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 @@ open class QSTileViewImpl @JvmOverloads constructor(

    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 @@ open class QSTileViewImpl @JvmOverloads constructor(
    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 @@ open class QSTileViewImpl @JvmOverloads constructor(
        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 @@ open class QSTileViewImpl @JvmOverloads constructor(
            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 @@ open class QSTileViewImpl @JvmOverloads constructor(
                .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 @@ open class QSTileViewImpl @JvmOverloads constructor(
        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 @@ open class QSTileViewImpl @JvmOverloads constructor(
            }
        }

        // Colors
        if (state.state != lastState) {
            singleAnimator.cancel()
            if (allowAnimations) {
            animateLabelColor(getLabelColor(state.state))
            animateSecondaryLabelColor(getSecondaryLabelColor(state.state))
                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 {
            label.setTextColor(getLabelColor(state.state))
            secondaryLabel.setTextColor(getSecondaryLabelColor(state.state))
                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 @@ open class QSTileViewImpl @JvmOverloads constructor(
        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 @@ open class QSTileViewImpl @JvmOverloads constructor(
        }
    }

    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 @@ open class QSTileViewImpl @JvmOverloads constructor(
        }
    }

    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 @@ open class QSTileViewImpl @JvmOverloads constructor(
            }
        }
    }

    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
+2 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.content.Context
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 @@ import org.mockito.MockitoAnnotations

@RunWith(AndroidTestingRunner::class)
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class QSTileViewImplTest : SysuiTestCase() {

    @Mock