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

Commit cb49af29 authored by Hawkwood Glazier's avatar Hawkwood Glazier Committed by Android (Google) Code Review
Browse files

Merge "Adjust Fidget animation parameters" into main

parents 12e6669c 6249a86e
Loading
Loading
Loading
Loading
+109 −188
Original line number Diff line number Diff line
@@ -27,9 +27,8 @@ import android.graphics.fonts.FontVariationAxis
import android.text.Layout
import android.util.Log
import android.util.LruCache

private const val DEFAULT_ANIMATION_DURATION: Long = 300
private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators

typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit

@@ -76,6 +75,10 @@ class TypefaceVariantCacheImpl(var baseTypeface: Typeface, override val animatio
            cache.put(fvar, it)
        }
    }

    companion object {
        private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
    }
}

/**
@@ -108,25 +111,12 @@ class TextAnimator(
    private val typefaceCache: TypefaceVariantCache,
    private val invalidateCallback: () -> Unit = {},
) {
    // Following two members are for mutable for testing purposes.
    public var textInterpolator = TextInterpolator(layout, typefaceCache)
    public var animator =
        ValueAnimator.ofFloat(1f).apply {
            duration = DEFAULT_ANIMATION_DURATION
            addUpdateListener {
                textInterpolator.progress = it.animatedValue as Float
                textInterpolator.linearProgress =
                    it.currentPlayTime.toFloat() / it.duration.toFloat()
                invalidateCallback()
            }
            addListener(
                object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animation: Animator) = textInterpolator.rebase()
    @VisibleForTesting var textInterpolator = TextInterpolator(layout, typefaceCache)
    @VisibleForTesting var createAnimator: () -> ValueAnimator = { ValueAnimator.ofFloat(1f) }

                    override fun onAnimationCancel(animation: Animator) = textInterpolator.rebase()
                }
            )
        }
    var animator: ValueAnimator? = null

    val fontVariationUtils = FontVariationUtils()

    sealed class PositionedGlyph {
        /** Mutable X coordinate of the glyph position relative from drawing offset. */
@@ -165,8 +155,6 @@ class TextAnimator(
            protected set
    }

    private val fontVariationUtils = FontVariationUtils()

    fun updateLayout(layout: Layout, textSize: Float = -1f) {
        textInterpolator.layout = layout

@@ -178,9 +166,8 @@ class TextAnimator(
        }
    }

    fun isRunning(): Boolean {
        return animator.isRunning
    }
    val isRunning: Boolean
        get() = animator?.isRunning ?: false

    /**
     * GlyphFilter applied just before drawing to canvas for tweaking positions and text size.
@@ -237,110 +224,110 @@ class TextAnimator(

    fun draw(c: Canvas) = textInterpolator.draw(c)

    /**
     * Set text style with animation.
     *
     * ```
     * By passing -1 to weight, the view preserve the current weight.
     * By passing -1 to textSize, the view preserve the current text size.
     * By passing -1 to duration, the default text animation, 1000ms, is used.
     * By passing false to animate, the text will be updated without animation.
     * ```
     *
     * @param fvar an optional text fontVariationSettings.
     * @param textSize an optional font size.
     * @param colors an optional colors array that must be the same size as numLines passed to the
     *   TextInterpolator
     * @param strokeWidth an optional paint stroke width
     * @param animate an optional boolean indicating true for showing style transition as animation,
     *   false for immediate style transition. True by default.
     * @param duration an optional animation duration in milliseconds. This is ignored if animate is
     *   false.
     * @param interpolator an optional time interpolator. If null is passed, last set interpolator
     *   will be used. This is ignored if animate is false.
     */
    fun setTextStyle(
        fvar: String? = "",
        textSize: Float = -1f,
        color: Int? = null,
        strokeWidth: Float = -1f,
        animate: Boolean = true,
        duration: Long = -1L,
        interpolator: TimeInterpolator? = null,
        delay: Long = 0,
        onAnimationEnd: Runnable? = null,
    /** Style spec to use when rendering the font */
    data class Style(
        val fVar: String? = null,
        val textSize: Float? = null,
        val color: Int? = null,
        val strokeWidth: Float? = null,
    ) {
        setTextStyleInternal(
            fvar,
            textSize,
            color,
            strokeWidth,
            animate,
            duration,
            interpolator,
            delay,
            onAnimationEnd,
            updateLayoutOnFailure = true,
        fun withUpdatedFVar(
            fontVariationUtils: FontVariationUtils,
            weight: Int = -1,
            width: Int = -1,
            opticalSize: Int = -1,
            roundness: Int = -1,
        ): Style {
            return this.copy(
                fVar =
                    fontVariationUtils.updateFontVariation(
                        weight = weight,
                        width = width,
                        opticalSize = opticalSize,
                        roundness = roundness,
                    )
            )
        }

    private fun setTextStyleInternal(
        fvar: String?,
        textSize: Float,
        color: Int?,
        strokeWidth: Float,
        animate: Boolean,
        duration: Long,
        interpolator: TimeInterpolator?,
        delay: Long,
        onAnimationEnd: Runnable?,
        updateLayoutOnFailure: Boolean,
    ) {
        try {
            if (animate) {
                animator.cancel()
                textInterpolator.rebase()
            }

            if (textSize >= 0) {
                textInterpolator.targetPaint.textSize = textSize
            }
            if (!fvar.isNullOrBlank()) {
                textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar)
            }
            if (color != null) {
                textInterpolator.targetPaint.color = color
            }
            if (strokeWidth >= 0F) {
                textInterpolator.targetPaint.strokeWidth = strokeWidth
    }
            textInterpolator.onTargetPaintModified()

            if (animate) {
                animator.startDelay = delay
                animator.duration = if (duration == -1L) DEFAULT_ANIMATION_DURATION else duration
                interpolator?.let { animator.interpolator = it }
    /** Animation Spec for use when style changes should be animated */
    data class Animation(
        val animate: Boolean = true,
        val startDelay: Long = 0,
        val duration: Long = DEFAULT_ANIMATION_DURATION,
        val interpolator: TimeInterpolator = Interpolators.LINEAR,
        val onAnimationEnd: Runnable? = null,
    ) {
        fun configureAnimator(animator: Animator) {
            animator.startDelay = startDelay
            animator.duration = duration
            animator.interpolator = interpolator
            if (onAnimationEnd != null) {
                animator.addListener(
                    object : AnimatorListenerAdapter() {
                        override fun onAnimationEnd(animation: Animator) {
                            onAnimationEnd.run()
                                animator.removeListener(this)
                            }

                            override fun onAnimationCancel(animation: Animator) {
                                animator.removeListener(this)
                        }
                    }
                )
            }
                animator.start()
        }

        companion object {
            val DISABLED = Animation(animate = false)
        }
    }

    /** Sets the text style, optionally with animation */
    fun setTextStyle(style: Style, animation: Animation = Animation.DISABLED) {
        animator?.cancel()
        setTextStyleInternal(style, rebase = animation.animate)

        if (animation.animate) {
            animator = buildAnimator(animation).apply { start() }
        } else {
                // No animation is requested, thus set base and target state to the same state.
            textInterpolator.progress = 1f
            textInterpolator.rebase()
            invalidateCallback()
        }
    }

    /** Builds a ValueAnimator from the specified animation parameters */
    private fun buildAnimator(animation: Animation): ValueAnimator {
        return createAnimator().apply {
            duration = DEFAULT_ANIMATION_DURATION
            animation.configureAnimator(this)

            addUpdateListener {
                textInterpolator.progress = it.animatedValue as Float
                textInterpolator.linearProgress = it.currentPlayTime / it.duration.toFloat()
                invalidateCallback()
            }

            addListener(
                object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animator: Animator) = textInterpolator.rebase()

                    override fun onAnimationCancel(animator: Animator) = textInterpolator.rebase()
                }
            )
        }
    }

    private fun setTextStyleInternal(
        style: Style,
        rebase: Boolean,
        updateLayoutOnFailure: Boolean = true,
    ) {
        try {
            if (rebase) textInterpolator.rebase()
            style.color?.let { textInterpolator.targetPaint.color = it }
            style.textSize?.let { textInterpolator.targetPaint.textSize = it }
            style.strokeWidth?.let { textInterpolator.targetPaint.strokeWidth = it }
            style.fVar?.let {
                textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(it)
            }
            textInterpolator.onTargetPaintModified()
        } catch (ex: IllegalArgumentException) {
            if (updateLayoutOnFailure) {
                Log.e(
@@ -351,81 +338,15 @@ class TextAnimator(
                )

                updateLayout(textInterpolator.layout)
                setTextStyleInternal(
                    fvar,
                    textSize,
                    color,
                    strokeWidth,
                    animate,
                    duration,
                    interpolator,
                    delay,
                    onAnimationEnd,
                    updateLayoutOnFailure = false,
                )
                setTextStyleInternal(style, rebase, updateLayoutOnFailure = false)
            } else {
                throw ex
            }
        }
    }

    /**
     * Set text style with animation. Similar as
     *
     * ```
     * fun setTextStyle(
     *      fvar: String? = "",
     *      textSize: Float = -1f,
     *      color: Int? = null,
     *      strokeWidth: Float = -1f,
     *      animate: Boolean = true,
     *      duration: Long = -1L,
     *      interpolator: TimeInterpolator? = null,
     *      delay: Long = 0,
     *      onAnimationEnd: Runnable? = null
     * )
     * ```
     *
     * @param weight an optional style value for `wght` in fontVariationSettings.
     * @param width an optional style value for `wdth` in fontVariationSettings.
     * @param opticalSize an optional style value for `opsz` in fontVariationSettings.
     * @param roundness an optional style value for `ROND` in fontVariationSettings.
     */
    fun setTextStyle(
        weight: Int = -1,
        width: Int = -1,
        opticalSize: Int = -1,
        roundness: Int = -1,
        textSize: Float = -1f,
        color: Int? = null,
        strokeWidth: Float = -1f,
        animate: Boolean = true,
        duration: Long = -1L,
        interpolator: TimeInterpolator? = null,
        delay: Long = 0,
        onAnimationEnd: Runnable? = null,
    ) {
        setTextStyleInternal(
            fvar =
                fontVariationUtils.updateFontVariation(
                    weight = weight,
                    width = width,
                    opticalSize = opticalSize,
                    roundness = roundness,
                ),
            textSize = textSize,
            color = color,
            strokeWidth = strokeWidth,
            animate = animate,
            duration = duration,
            interpolator = interpolator,
            delay = delay,
            onAnimationEnd = onAnimationEnd,
            updateLayoutOnFailure = true,
        )
    }

    companion object {
        private val TAG = TextAnimator::class.simpleName!!
        const val DEFAULT_ANIMATION_DURATION = 300L
    }
}
+13 −14
Original line number Diff line number Diff line
@@ -366,7 +366,7 @@ constructor(

    fun animateCharge(isDozing: () -> Boolean) {
        // Skip charge animation if dozing animation is already playing.
        if (textAnimator == null || textAnimator!!.isRunning()) {
        if (textAnimator == null || textAnimator!!.isRunning) {
            return
        }

@@ -444,29 +444,28 @@ constructor(
        delay: Long,
        onAnimationEnd: Runnable?,
    ) {
        textAnimator?.let {
            it.setTextStyle(
                weight = weight,
                color = color,
        val style = TextAnimator.Style(color = color)
        val animation =
            TextAnimator.Animation(
                animate = animate && isAnimationEnabled,
                duration = duration,
                interpolator = interpolator,
                delay = delay,
                interpolator = interpolator ?: Interpolators.LINEAR,
                startDelay = delay,
                onAnimationEnd = onAnimationEnd,
            )
        textAnimator?.let {
            it.setTextStyle(
                style.withUpdatedFVar(it.fontVariationUtils, weight = weight),
                animation,
            )
            it.glyphFilter = glyphFilter
        }
            ?: run {
                // when the text animator is set, update its start values
                onTextAnimatorInitialized = { textAnimator ->
                    textAnimator.setTextStyle(
                        weight = weight,
                        color = color,
                        animate = false,
                        duration = duration,
                        interpolator = interpolator,
                        delay = delay,
                        onAnimationEnd = onAnimationEnd,
                        style.withUpdatedFVar(textAnimator.fontVariationUtils, weight = weight),
                        animation.copy(animate = false),
                    )
                    textAnimator.glyphFilter = glyphFilter
                }
+2 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.view.ViewGroup
import android.view.animation.Interpolator
import android.widget.RelativeLayout
import androidx.annotation.VisibleForTesting
import com.android.systemui.animation.TextAnimator
import com.android.systemui.customization.R
import com.android.systemui.log.core.Logger
import com.android.systemui.plugins.clocks.AlarmData
@@ -65,7 +66,7 @@ data class DigitalAlignment(
data class FontTextStyle(
    val lineHeight: Float? = null,
    val fontSizeScale: Float? = null,
    val transitionDuration: Long = -1L,
    val transitionDuration: Long = TextAnimator.DEFAULT_ANIMATION_DURATION,
    val transitionInterpolator: Interpolator? = null,
)

+107 −79

File changed.

Preview size limit exceeded, changes collapsed.

+27 −28
Original line number Diff line number Diff line
@@ -20,9 +20,10 @@ import android.view.LayoutInflater
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.app.animation.Interpolators
import com.android.systemui.customization.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.FontVariationUtils
import com.android.systemui.animation.TextAnimator
import com.android.systemui.customization.R
import com.android.systemui.util.mockito.any
import org.junit.Before
import org.junit.Rule
@@ -32,7 +33,9 @@ import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.eq

@RunWith(AndroidJUnit4::class)
@SmallTest
@@ -46,6 +49,7 @@ class AnimatableClockViewTest : SysuiTestCase() {
    @Before
    fun setUp() {
        val layoutInflater = LayoutInflater.from(context)
        whenever(mockTextAnimator.fontVariationUtils).thenReturn(FontVariationUtils())
        clockView =
            layoutInflater.inflate(R.layout.clock_default_small, null) as AnimatableClockView
        clockView.textAnimatorFactory = { _, _ -> mockTextAnimator }
@@ -57,18 +61,19 @@ class AnimatableClockViewTest : SysuiTestCase() {
        clockView.animateAppearOnLockscreen()
        clockView.measure(50, 50)

        verify(mockTextAnimator).fontVariationUtils
        verify(mockTextAnimator).glyphFilter = any()
        verify(mockTextAnimator)
            .setTextStyle(
                weight = 300,
                textSize = -1.0f,
                color = 200,
                strokeWidth = -1F,
                eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)),
                eq(
                    TextAnimator.Animation(
                        animate = false,
                        duration = 833L,
                        interpolator = Interpolators.EMPHASIZED_DECELERATE,
                delay = 0L,
                onAnimationEnd = null
                        onAnimationEnd = null,
                    )
                ),
            )
        verifyNoMoreInteractions(mockTextAnimator)
    }
@@ -79,30 +84,24 @@ class AnimatableClockViewTest : SysuiTestCase() {
        clockView.measure(50, 50)
        clockView.animateAppearOnLockscreen()

        verify(mockTextAnimator, times(2)).fontVariationUtils
        verify(mockTextAnimator, times(2)).glyphFilter = any()
        verify(mockTextAnimator)
            .setTextStyle(
                weight = 100,
                textSize = -1.0f,
                color = 200,
                strokeWidth = -1F,
                animate = false,
                duration = 0L,
                interpolator = null,
                delay = 0L,
                onAnimationEnd = null
                eq(TextAnimator.Style(fVar = "'wght' 100", color = 200)),
                eq(TextAnimator.Animation(animate = false, duration = 0)),
            )

        verify(mockTextAnimator)
            .setTextStyle(
                weight = 300,
                textSize = -1.0f,
                color = 200,
                strokeWidth = -1F,
                eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)),
                eq(
                    TextAnimator.Animation(
                        animate = true,
                        duration = 833L,
                        interpolator = Interpolators.EMPHASIZED_DECELERATE,
                delay = 0L,
                onAnimationEnd = null
                    )
                ),
            )
        verifyNoMoreInteractions(mockTextAnimator)
    }
Loading