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

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

Merge "Reduce the number of Font instance creation" into main

parents f0286b47 de6d4442
Loading
Loading
Loading
Loading
+59 −67
Original line number Diff line number Diff line
@@ -34,66 +34,60 @@ private const val FONT_ITALIC_MIN = 0f
private const val FONT_ITALIC_ANIMATION_STEP = 0.1f
private const val FONT_ITALIC_DEFAULT_VALUE = 0f

// Benchmarked via Perfetto, difference between 10 and 50 entries is about 0.3ms in
// frame draw time on a Pixel 6.
@VisibleForTesting const val DEFAULT_FONT_CACHE_MAX_ENTRIES = 10
/** Caches for font interpolation */
interface FontCache {
    val animationFrameCount: Int

/** Provide interpolation of two fonts by adjusting font variation settings. */
class FontInterpolator(
    numberOfAnimationSteps: Int? = null,
) {
    /**
     * Cache key for the interpolated font.
     *
     * This class is mutable for recycling.
     */
    private data class InterpKey(var l: Font?, var r: Font?, var progress: Float) {
        fun set(l: Font, r: Font, progress: Float) {
            this.l = l
            this.r = r
            this.progress = progress
        }
    fun get(key: InterpKey): Font?

    fun get(key: VarFontKey): Font?

    fun put(key: InterpKey, font: Font)

    fun put(key: VarFontKey, font: Font)
}

    /**
     * Cache key for the font that has variable font.
     *
     * This class is mutable for recycling.
     */
    private data class VarFontKey(
        var sourceId: Int,
        var index: Int,
        val sortedAxes: MutableList<FontVariationAxis>
    ) {
/** Cache key for the interpolated font. */
data class InterpKey(val start: Font?, val end: Font?, val frame: Int)

/** Cache key for the font that has variable font. */
data class VarFontKey(val sourceId: Int, val index: Int, val sortedAxes: List<FontVariationAxis>) {
    constructor(
        font: Font,
            axes: List<FontVariationAxis>
        ) : this(
            font.sourceIdentifier,
            font.ttcIndex,
            axes.toMutableList().apply { sortBy { it.tag } }
        )

        fun set(font: Font, axes: List<FontVariationAxis>) {
            sourceId = font.sourceIdentifier
            index = font.ttcIndex
            sortedAxes.clear()
            sortedAxes.addAll(axes)
            sortedAxes.sortBy { it.tag }
        }
        axes: List<FontVariationAxis>,
    ) : this(font.sourceIdentifier, font.ttcIndex, axes.sortedBy { it.tag })
}

class FontCacheImpl(override val animationFrameCount: Int = DEFAULT_FONT_CACHE_MAX_ENTRIES / 2) :
    FontCache {
    // Font interpolator has two level caches: one for input and one for font with different
    // variation settings. No synchronization is needed since FontInterpolator is not designed to be
    // thread-safe and can be used only on UI thread.
    val cacheMaxEntries = numberOfAnimationSteps?.let { it * 2 } ?: DEFAULT_FONT_CACHE_MAX_ENTRIES
    val cacheMaxEntries = animationFrameCount * 2
    private val interpCache = LruCache<InterpKey, Font>(cacheMaxEntries)
    private val verFontCache = LruCache<VarFontKey, Font>(cacheMaxEntries)

    // Mutable keys for recycling.
    private val tmpInterpKey = InterpKey(null, null, 0f)
    private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf())
    override fun get(key: InterpKey): Font? = interpCache[key]

    override fun get(key: VarFontKey): Font? = verFontCache[key]

    override fun put(key: InterpKey, font: Font) {
        interpCache.put(key, font)
    }

    override fun put(key: VarFontKey, font: Font) {
        verFontCache.put(key, font)
    }

    companion object {
        // Benchmarked via Perfetto, difference between 10 and 50 entries is about 0.3ms in frame
        // draw time on a Pixel 6.
        @VisibleForTesting const val DEFAULT_FONT_CACHE_MAX_ENTRIES = 10
    }
}

/** 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 {
        if (progress == 0f) {
@@ -111,13 +105,12 @@ class FontInterpolator(

        // Check we already know the result. This is commonly happens since we draws the different
        // text chunks with the same font.
        tmpInterpKey.set(start, end, progress)
        val cachedFont = interpCache[tmpInterpKey]
        if (cachedFont != null) {
        val iKey = InterpKey(start, end, (progress * fontCache.animationFrameCount).toInt())
        fontCache.get(iKey)?.let {
            if (DEBUG) {
                Log.d(LOG_TAG, "[$progress] Interp. cache hit for $tmpInterpKey")
                Log.d(LOG_TAG, "[$progress] Interp. cache hit for $iKey")
            }
            return cachedFont
            return it
        }

        // General axes interpolation takes O(N log N), this is came from sorting the axes. Usually
@@ -131,14 +124,14 @@ class FontInterpolator(
                        MathUtils.lerp(
                            startValue ?: FONT_WEIGHT_DEFAULT_VALUE,
                            endValue ?: FONT_WEIGHT_DEFAULT_VALUE,
                            progress
                            progress,
                        )
                    TAG_ITAL ->
                        adjustItalic(
                            MathUtils.lerp(
                                startValue ?: FONT_ITALIC_DEFAULT_VALUE,
                                endValue ?: FONT_ITALIC_DEFAULT_VALUE,
                                progress
                                progress,
                            )
                        )
                    else -> {
@@ -152,32 +145,31 @@ class FontInterpolator(

        // Check if we already make font for this axes. This is typically happens if the animation
        // happens backward.
        tmpVarFontKey.set(start, newAxes)
        val axesCachedFont = verFontCache[tmpVarFontKey]
        if (axesCachedFont != null) {
            interpCache.put(InterpKey(start, end, progress), axesCachedFont)
        val vKey = VarFontKey(start, newAxes)
        fontCache.get(vKey)?.let {
            fontCache.put(iKey, it)
            if (DEBUG) {
                Log.d(LOG_TAG, "[$progress] Axis cache hit for $tmpVarFontKey")
                Log.d(LOG_TAG, "[$progress] Axis cache hit for $vKey")
            }
            return axesCachedFont
            return it
        }

        // This is the first time to make the font for the axes. Build and store it to the cache.
        // Font.Builder#build won't throw IOException since creating fonts from existing fonts will
        // not do any IO work.
        val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build()
        interpCache.put(InterpKey(start, end, progress), newFont)
        verFontCache.put(VarFontKey(start, newAxes), newFont)
        fontCache.put(iKey, newFont)
        fontCache.put(vKey, newFont)

        // Cache misses are likely to create memory leaks, so this is logged at error level.
        Log.e(LOG_TAG, "[$progress] Cache MISS for $tmpInterpKey / $tmpVarFontKey")
        Log.e(LOG_TAG, "[$progress] Cache MISS for $iKey / $vKey")
        return newFont
    }

    private fun lerp(
        start: Array<FontVariationAxis>,
        end: Array<FontVariationAxis>,
        filter: (tag: String, left: Float?, right: Float?) -> Float
        filter: (tag: String, left: Float?, right: Float?) -> Float,
    ): List<FontVariationAxis> {
        // Safe to modify result of Font#getAxes since it returns cloned object.
        start.sortBy { axis -> axis.tag }
+9 −13
Original line number Diff line number Diff line
@@ -35,6 +35,8 @@ private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit

interface TypefaceVariantCache {
    val fontCache: FontCache
    val animationFrameCount: Int
    fun getTypefaceForVariant(fvar: String?): Typeface?

    companion object {
@@ -57,8 +59,10 @@ interface 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
@@ -100,25 +104,17 @@ class TypefaceVariantCacheImpl(
 */
class TextAnimator(
    layout: Layout,
    numberOfAnimationSteps: Int? = null, // Only do this number of discrete animation steps.
    private val invalidateCallback: () -> Unit,
    private val typefaceCache: TypefaceVariantCache,
    private val invalidateCallback: () -> Unit = {},
) {
    var typefaceCache: TypefaceVariantCache = TypefaceVariantCacheImpl(layout.paint.typeface)
        get() = field
        set(value) {
            field = value
            textInterpolator.typefaceCache = value
        }

    // Following two members are for mutable for testing purposes.
    public var textInterpolator: TextInterpolator =
        TextInterpolator(layout, typefaceCache, numberOfAnimationSteps)
    public var animator: ValueAnimator =
    public var textInterpolator = TextInterpolator(layout, typefaceCache)
    public var animator =
        ValueAnimator.ofFloat(1f).apply {
            duration = DEFAULT_ANIMATION_DURATION
            addUpdateListener {
                textInterpolator.progress =
                    calculateProgress(it.animatedValue as Float, numberOfAnimationSteps)
                    calculateProgress(it.animatedValue as Float, typefaceCache.animationFrameCount)
                invalidateCallback()
            }
            addListener(
+12 −12
Original line number Diff line number Diff line
@@ -28,11 +28,7 @@ import com.android.internal.graphics.ColorUtils
import java.lang.Math.max

/** Provide text style linear interpolation for plain text. */
class TextInterpolator(
    layout: Layout,
    var typefaceCache: TypefaceVariantCache,
    numberOfAnimationSteps: Int? = null,
) {
class TextInterpolator(layout: Layout, var typefaceCache: TypefaceVariantCache) {
    /**
     * Returns base paint used for interpolation.
     *
@@ -66,7 +62,7 @@ class TextInterpolator(
        val start: Int, // inclusive
        val end: Int, // exclusive
        var baseFont: Font,
        var targetFont: Font
        var targetFont: Font,
    ) {
        val length: Int
            get() = end - start
@@ -79,14 +75,14 @@ class TextInterpolator(
        val baseY: FloatArray, // same length as glyphIds
        val targetX: FloatArray, // same length as glyphIds
        val targetY: FloatArray, // same length as glyphIds
        val fontRuns: List<FontRun>
        val fontRuns: List<FontRun>,
    )

    /** A class represents text layout of a single line. */
    private class Line(val runs: List<Run>)

    private var lines = listOf<Line>()
    private val fontInterpolator = FontInterpolator(numberOfAnimationSteps)
    private val fontInterpolator = FontInterpolator(typefaceCache.fontCache)

    // Recycling object for glyph drawing and tweaking.
    private val tmpPaint = TextPaint()
@@ -343,12 +339,16 @@ class TextInterpolator(
    private class MutablePositionedGlyph : TextAnimator.PositionedGlyph() {
        override var runStart: Int = 0
            public set

        override var runLength: Int = 0
            public set

        override var glyphIndex: Int = 0
            public set

        override lateinit var font: Font
            public set

        override var glyphId: Int = 0
            public set
    }
@@ -401,7 +401,7 @@ class TextInterpolator(
                    0,
                    i - prevStart,
                    font,
                    tmpPaintForGlyph
                    tmpPaintForGlyph,
                )
                prevStart = i
                arrayIndex = 0
@@ -418,13 +418,13 @@ class TextInterpolator(
            0,
            run.end - prevStart,
            font,
            tmpPaintForGlyph
            tmpPaintForGlyph,
        )
    }

    private fun updatePositionsAndFonts(
        layoutResult: List<List<PositionedGlyphs>>,
        updateBase: Boolean
        updateBase: Boolean,
    ) {
        // Update target positions with newly calculated text layout.
        check(layoutResult.size == lines.size) { "The new layout result has different line count." }
@@ -507,7 +507,7 @@ class TextInterpolator(
                lineStart,
                count,
                layout.textDirectionHeuristic,
                paint
                paint,
            ) { _, _, glyphs, _ ->
                runs.add(glyphs)
            }
+3 −1
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import com.android.app.animation.Interpolators
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.animation.GlyphCallback
import com.android.systemui.animation.TextAnimator
import com.android.systemui.animation.TypefaceVariantCacheImpl
import com.android.systemui.customization.R
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogcatOnlyMessageBuffer
@@ -98,7 +99,8 @@ constructor(

    @VisibleForTesting
    var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
        TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb)
        val cache = TypefaceVariantCacheImpl(layout.paint.typeface, NUM_CLOCK_FONT_ANIMATION_STEPS)
        TextAnimator(layout, cache, invalidateCb)
    }

    // Used by screenshot tests to provide stability
+0 −21
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.shared.clocks

object ClockAnimation {
    const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30
}
Loading