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

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

Merge "Use linear progress for generating interpolation cache keys" into main

parents 8063950e 69b16289
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)
    }
}