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

Commit 69b16289 authored by Hawkwood Glazier's avatar Hawkwood Glazier
Browse files

Use linear progress for generating interpolation cache keys

Bug: 394356998
Flag: NONE Text Animator Bugfix
Test: Manually checked new clock animation
Change-Id: I20ee5dc4009928e98a35fef805f0cab9d942e497
parent 200d30a9
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.util.MathUtils
import androidx.annotation.VisibleForTesting
import java.lang.Float.max
import java.lang.Float.min
import kotlin.math.roundToInt

private const val TAG_WGHT = "wght"
private const val TAG_ITAL = "ital"
@@ -89,7 +90,7 @@ class FontCacheImpl(override val animationFrameCount: Int = DEFAULT_FONT_CACHE_M
/** Provide interpolation of two fonts by adjusting font variation settings. */
class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) {
    /** Linear interpolate the font variation settings. */
    fun lerp(start: Font, end: Font, progress: Float): Font {
    fun lerp(start: Font, end: Font, progress: Float, linearProgress: Float): Font {
        if (progress == 0f) {
            return start
        } else if (progress == 1f) {
@@ -105,7 +106,8 @@ class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) {

        // Check we already know the result. This is commonly happens since we draws the different
        // text chunks with the same font.
        val iKey = InterpKey(start, end, (progress * fontCache.animationFrameCount).toInt())
        val iKey =
            InterpKey(start, end, (linearProgress * fontCache.animationFrameCount).roundToInt())
        fontCache.get(iKey)?.let {
            if (DEBUG) {
                Log.d(LOG_TAG, "[$progress] Interp. cache hit for $iKey")
+84 −58
Original line number Diff line number Diff line
@@ -25,9 +25,8 @@ import android.graphics.Typeface
import android.graphics.fonts.Font
import android.graphics.fonts.FontVariationAxis
import android.text.Layout
import android.util.LruCache
import kotlin.math.roundToInt
import android.util.Log
import android.util.LruCache

private const val DEFAULT_ANIMATION_DURATION: Long = 300
private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
@@ -37,6 +36,7 @@ typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit
interface TypefaceVariantCache {
    val fontCache: FontCache
    val animationFrameCount: Int

    fun getTypefaceForVariant(fvar: String?): Typeface?

    companion object {
@@ -45,24 +45,25 @@ interface TypefaceVariantCache {
                return baseTypeface
            }

            val axes = FontVariationAxis.fromFontVariationSettings(fVar)
                ?.toMutableList()
            val axes =
                FontVariationAxis.fromFontVariationSettings(fVar)?.toMutableList()
                    ?: mutableListOf()
            axes.removeIf { !baseTypeface.isSupportedAxes(it.getOpenTypeTagValue()) }

            if (axes.isEmpty()) {
                return baseTypeface
            }
            } else {
                return Typeface.createFromTypefaceWithVariation(baseTypeface, axes)
            }
        }
    }
}

class TypefaceVariantCacheImpl(
    var baseTypeface: Typeface,
    override val animationFrameCount: Int,
) : TypefaceVariantCache {
class TypefaceVariantCacheImpl(var baseTypeface: Typeface, override val animationFrameCount: Int) :
    TypefaceVariantCache {
    private val cache = LruCache<String, Typeface>(TYPEFACE_CACHE_MAX_ENTRIES)
    override val fontCache = FontCacheImpl(animationFrameCount)

    override fun getTypefaceForVariant(fvar: String?): Typeface? {
        if (fvar == null) {
            return baseTypeface
@@ -113,29 +114,20 @@ class TextAnimator(
        ValueAnimator.ofFloat(1f).apply {
            duration = DEFAULT_ANIMATION_DURATION
            addUpdateListener {
                textInterpolator.progress =
                    calculateProgress(it.animatedValue as Float, typefaceCache.animationFrameCount)
                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()

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

    private fun calculateProgress(animProgress: Float, numberOfAnimationSteps: Int?): Float {
        if (numberOfAnimationSteps != null) {
            // This clamps the progress to the nearest value of "numberOfAnimationSteps"
            // discrete values between 0 and 1f.
            return (animProgress * numberOfAnimationSteps).roundToInt() /
                numberOfAnimationSteps.toFloat()
        }

        return animProgress
    }

    sealed class PositionedGlyph {
        /** Mutable X coordinate of the glyph position relative from drawing offset. */
        var x: Float = 0f
@@ -209,12 +201,14 @@ class TextAnimator(
     *
     * Here is an example of font runs: "fin. 終わり"
     *
     * ```
     * Characters :    f      i      n      .      _      終     わ     り
     * Code Points: \u0066 \u0069 \u006E \u002E \u0020 \u7D42 \u308F \u308A
     * Font Runs  : <-- Roboto-Regular.ttf          --><-- NotoSans-CJK.otf -->
     *                  runStart = 0, runLength = 5        runStart = 5, runLength = 3
     * Glyph IDs  :      194        48     7      8     4367   1039   1002
     * Glyph Index:       0          1     2      3       0      1      2
     * ```
     *
     * In this example, the "fi" is converted into ligature form, thus the single glyph ID is
     * assigned for two characters, f and i.
@@ -246,15 +240,17 @@ class TextAnimator(
    /**
     * 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.
     * Bu passing -1 to duration, the default text animation, 1000ms, is used.
     * 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 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.
@@ -273,8 +269,20 @@ class TextAnimator(
        interpolator: TimeInterpolator? = null,
        delay: Long = 0,
        onAnimationEnd: Runnable? = null,
    ) = setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration,
        interpolator, delay, onAnimationEnd, updateLayoutOnFailure = true)
    ) {
        setTextStyleInternal(
            fvar,
            textSize,
            color,
            strokeWidth,
            animate,
            duration,
            interpolator,
            delay,
            onAnimationEnd,
            updateLayoutOnFailure = true,
        )
    }

    private fun setTextStyleInternal(
        fvar: String?,
@@ -310,24 +318,21 @@ class TextAnimator(

            if (animate) {
                animator.startDelay = delay
                animator.duration =
                    if (duration == -1L) {
                        DEFAULT_ANIMATION_DURATION
                    } else {
                        duration
                    }
                animator.duration = if (duration == -1L) DEFAULT_ANIMATION_DURATION else duration
                interpolator?.let { animator.interpolator = it }
                if (onAnimationEnd != null) {
                    val listener = object : AnimatorListenerAdapter() {
                    animator.addListener(
                        object : AnimatorListenerAdapter() {
                            override fun onAnimationEnd(animation: Animator) {
                                onAnimationEnd.run()
                                animator.removeListener(this)
                            }

                            override fun onAnimationCancel(animation: Animator) {
                                animator.removeListener(this)
                            }
                        }
                    animator.addListener(listener)
                    )
                }
                animator.start()
            } else {
@@ -338,11 +343,26 @@ class TextAnimator(
            }
        } catch (ex: IllegalArgumentException) {
            if (updateLayoutOnFailure) {
                Log.e(TAG, "setTextStyleInternal: Exception caught but retrying. This is usually" +
                    " due to the layout having changed unexpectedly without being notified.", ex)
                Log.e(
                    TAG,
                    "setTextStyleInternal: Exception caught but retrying. This is usually" +
                        " due to the layout having changed unexpectedly without being notified.",
                    ex,
                )

                updateLayout(textInterpolator.layout)
                setTextStyleInternal(fvar, textSize, color, strokeWidth, animate, duration,
                    interpolator, delay, onAnimationEnd, updateLayoutOnFailure = false)
                setTextStyleInternal(
                    fvar,
                    textSize,
                    color,
                    strokeWidth,
                    animate,
                    duration,
                    interpolator,
                    delay,
                    onAnimationEnd,
                    updateLayoutOnFailure = false,
                )
            } else {
                throw ex
            }
@@ -351,6 +371,8 @@ class TextAnimator(

    /**
     * Set text style with animation. Similar as
     *
     * ```
     * fun setTextStyle(
     *      fvar: String? = "",
     *      textSize: Float = -1f,
@@ -362,6 +384,7 @@ class TextAnimator(
     *      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.
@@ -380,9 +403,11 @@ class TextAnimator(
        duration: Long = -1L,
        interpolator: TimeInterpolator? = null,
        delay: Long = 0,
        onAnimationEnd: Runnable? = null
    ) = setTextStyleInternal(
            fvar = fontVariationUtils.updateFontVariation(
        onAnimationEnd: Runnable? = null,
    ) {
        setTextStyleInternal(
            fvar =
                fontVariationUtils.updateFontVariation(
                    weight = weight,
                    width = width,
                    opticalSize = opticalSize,
@@ -398,6 +423,7 @@ class TextAnimator(
            onAnimationEnd = onAnimationEnd,
            updateLayoutOnFailure = true,
        )
    }

    companion object {
        private val TAG = TextAnimator::class.simpleName!!
+10 −2
Original line number Diff line number Diff line
@@ -98,6 +98,9 @@ class TextInterpolator(layout: Layout, var typefaceCache: TypefaceVariantCache)
     */
    var progress: Float = 0f

    /** Linear progress value (not interpolated) */
    var linearProgress: Float = 0f

    /**
     * The layout used for drawing text.
     *
@@ -217,7 +220,12 @@ class TextInterpolator(layout: Layout, var typefaceCache: TypefaceVariantCache)
                }
                run.fontRuns.forEach { fontRun ->
                    fontRun.baseFont =
                        fontInterpolator.lerp(fontRun.baseFont, fontRun.targetFont, progress)
                        fontInterpolator.lerp(
                            fontRun.baseFont,
                            fontRun.targetFont,
                            progress,
                            linearProgress,
                        )
                    val fvar = FontVariationAxis.toFontVariationSettings(fontRun.baseFont.axes)
                    basePaint.typeface = typefaceCache.getTypefaceForVariant(fvar)
                }
@@ -358,7 +366,7 @@ class TextInterpolator(layout: Layout, var typefaceCache: TypefaceVariantCache)
    // Draws single font run.
    private fun drawFontRun(c: Canvas, line: Run, run: FontRun, lineNo: Int, paint: Paint) {
        var arrayIndex = 0
        val font = fontInterpolator.lerp(run.baseFont, run.targetFont, progress)
        val font = fontInterpolator.lerp(run.baseFont, run.targetFont, progress, linearProgress)

        val glyphFilter = glyphFilter
        if (glyphFilter == null) {
+2 −1
Original line number Diff line number Diff line
@@ -109,7 +109,8 @@ class DefaultClockProvider(

    companion object {
        // 750ms @ 120hz -> 90 frames of animation
        const val NUM_CLOCK_FONT_ANIMATION_STEPS = 90
        // In practice, 45 looks good enough
        const val NUM_CLOCK_FONT_ANIMATION_STEPS = 45

        val FLEX_TYPEFACE by lazy {
            // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
+14 −11
Original line number Diff line number Diff line
@@ -58,9 +58,12 @@ class FontInterpolatorTest : SysuiTestCase() {
            Font.Builder(sFont).setFontVariationSettings("'wght' 900, 'ital' 1, 'GRAD' 700").build()

        val interp = FontInterpolator()
        assertSameAxes(startFont, interp.lerp(startFont, endFont, 0f))
        assertSameAxes(endFont, interp.lerp(startFont, endFont, 1f))
        assertSameAxes("'wght' 500, 'ital' 0.5, 'GRAD' 450", interp.lerp(startFont, endFont, 0.5f))
        assertSameAxes(startFont, interp.lerp(startFont, endFont, 0f, 0f))
        assertSameAxes(endFont, interp.lerp(startFont, endFont, 1f, 1f))
        assertSameAxes(
            "'wght' 500, 'ital' 0.5, 'GRAD' 450",
            interp.lerp(startFont, endFont, 0.5f, 0.5f),
        )
    }

    @Test
@@ -69,7 +72,7 @@ class FontInterpolatorTest : SysuiTestCase() {
        val endFont = Font.Builder(sFont).setFontVariationSettings("'ital' 1").build()

        val interp = FontInterpolator()
        assertSameAxes("'wght' 250, 'ital' 0.5", interp.lerp(startFont, endFont, 0.5f))
        assertSameAxes("'wght' 250, 'ital' 0.5", interp.lerp(startFont, endFont, 0.5f, 0.5f))
    }

    @Test
@@ -78,8 +81,8 @@ class FontInterpolatorTest : SysuiTestCase() {
        val endFont = Font.Builder(sFont).setFontVariationSettings("'ital' 1").build()

        val interp = FontInterpolator()
        val resultFont = interp.lerp(startFont, endFont, 0.5f)
        val cachedFont = interp.lerp(startFont, endFont, 0.5f)
        val resultFont = interp.lerp(startFont, endFont, 0.5f, 0.5f)
        val cachedFont = interp.lerp(startFont, endFont, 0.5f, 0.5f)
        assertThat(resultFont).isSameInstanceAs(cachedFont)
    }

@@ -89,8 +92,8 @@ class FontInterpolatorTest : SysuiTestCase() {
        val endFont = Font.Builder(sFont).setFontVariationSettings("'ital' 1").build()

        val interp = FontInterpolator()
        val resultFont = interp.lerp(startFont, endFont, 0.5f)
        val reversedFont = interp.lerp(endFont, startFont, 0.5f)
        val resultFont = interp.lerp(startFont, endFont, 0.5f, 0.5f)
        val reversedFont = interp.lerp(endFont, startFont, 0.5f, 0.5f)
        assertThat(resultFont).isSameInstanceAs(reversedFont)
    }

@@ -100,14 +103,14 @@ class FontInterpolatorTest : SysuiTestCase() {

        val startFont = Font.Builder(sFont).setFontVariationSettings("'wght' 100").build()
        val endFont = Font.Builder(sFont).setFontVariationSettings("'wght' 1").build()
        val resultFont = interp.lerp(startFont, endFont, 0.5f)
        val resultFont = interp.lerp(startFont, endFont, 0.5f, 0.5f)
        for (i in 0..(interp.fontCache as FontCacheImpl).cacheMaxEntries + 1) {
            val f1 = Font.Builder(sFont).setFontVariationSettings("'wght' ${i * 100}").build()
            val f2 = Font.Builder(sFont).setFontVariationSettings("'wght' $i").build()
            interp.lerp(f1, f2, 0.5f)
            interp.lerp(f1, f2, 0.5f, 0.5f)
        }

        val cachedFont = interp.lerp(startFont, endFont, 0.5f)
        val cachedFont = interp.lerp(startFont, endFont, 0.5f, 0.5f)
        assertThat(resultFont).isNotSameInstanceAs(cachedFont)
    }
}