• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.animation
18 
19 import android.graphics.fonts.Font
20 import android.graphics.fonts.FontVariationAxis
21 import android.util.Log
22 import android.util.LruCache
23 import android.util.MathUtils
24 import androidx.annotation.VisibleForTesting
25 import kotlin.math.roundToInt
26 
27 /** Caches for font interpolation */
28 interface FontCache {
29     val animationFrameCount: Int
30 
31     fun get(key: InterpKey): Font?
32 
33     fun get(key: VarFontKey): Font?
34 
35     fun put(key: InterpKey, font: Font)
36 
37     fun put(key: VarFontKey, font: Font)
38 }
39 
40 /** Cache key for the interpolated font. */
41 data class InterpKey(val start: Font?, val end: Font?, val frame: Int)
42 
43 /** Cache key for the font that has variable font. */
44 data class VarFontKey(val sourceId: Int, val index: Int, val sortedAxes: List<FontVariationAxis>) {
45     constructor(
46         font: Font,
47         axes: List<FontVariationAxis>,
<lambda>null48     ) : this(font.sourceIdentifier, font.ttcIndex, axes.sortedBy { it.tag })
49 }
50 
51 class FontCacheImpl(override val animationFrameCount: Int = DEFAULT_FONT_CACHE_MAX_ENTRIES / 2) :
52     FontCache {
53     // Font interpolator has two level caches: one for input and one for font with different
54     // variation settings. No synchronization is needed since FontInterpolator is not designed to be
55     // thread-safe and can be used only on UI thread.
56     val cacheMaxEntries = animationFrameCount * 2
57     private val interpCache = LruCache<InterpKey, Font>(cacheMaxEntries)
58     private val verFontCache = LruCache<VarFontKey, Font>(cacheMaxEntries)
59 
getnull60     override fun get(key: InterpKey): Font? = interpCache[key]
61 
62     override fun get(key: VarFontKey): Font? = verFontCache[key]
63 
64     override fun put(key: InterpKey, font: Font) {
65         interpCache.put(key, font)
66     }
67 
putnull68     override fun put(key: VarFontKey, font: Font) {
69         verFontCache.put(key, font)
70     }
71 
72     companion object {
73         // Benchmarked via Perfetto, difference between 10 and 50 entries is about 0.3ms in frame
74         // draw time on a Pixel 6.
75         @VisibleForTesting const val DEFAULT_FONT_CACHE_MAX_ENTRIES = 10
76     }
77 }
78 
79 /** Provide interpolation of two fonts by adjusting font variation settings. */
80 class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) {
81     /** Linear interpolate the font variation settings. */
lerpnull82     fun lerp(start: Font, end: Font, progress: Float, linearProgress: Float): Font {
83         if (progress <= 0f) return start
84         if (progress >= 1f) return end
85 
86         val startAxes = start.axes ?: EMPTY_AXES
87         val endAxes = end.axes ?: EMPTY_AXES
88 
89         if (startAxes.isEmpty() && endAxes.isEmpty()) {
90             return start
91         }
92 
93         // Check we already know the result. This is commonly happens since we draws the different
94         // text chunks with the same font.
95         val iKey =
96             InterpKey(start, end, (linearProgress * fontCache.animationFrameCount).roundToInt())
97         fontCache.get(iKey)?.let {
98             if (DEBUG) {
99                 Log.d(LOG_TAG, "[$progress, $linearProgress] Interp. cache hit for $iKey")
100             }
101             return it
102         }
103 
104         // General axes interpolation takes O(N log N), this is came from sorting the axes. Usually
105         // this doesn't take much time since the variation axes is usually up to 5. If we need to
106         // support more number of axes, we may want to preprocess the font and store the sorted axes
107         // and also pre-fill the missing axes value with default value from 'fvar' table.
108         val newAxes =
109             lerp(startAxes, endAxes) { tag, startValue, endValue ->
110                 MathUtils.lerp(startValue, endValue, progress)
111             }
112 
113         // Check if we already make font for this axes. This is typically happens if the animation
114         // happens backward and is being linearly interpolated.
115         val vKey = VarFontKey(start, newAxes)
116         fontCache.get(vKey)?.let {
117             fontCache.put(iKey, it)
118             if (DEBUG) {
119                 Log.d(LOG_TAG, "[$progress, $linearProgress] Axis cache hit for $vKey")
120             }
121             return it
122         }
123 
124         // This is the first time to make the font for the axes. Build and store it to the cache.
125         // Font.Builder#build won't throw IOException since creating fonts from existing fonts will
126         // not do any IO work.
127         val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build()
128         fontCache.put(iKey, newFont)
129         fontCache.put(vKey, newFont)
130 
131         // Cache misses are likely to create memory leaks, so this is logged at error level.
132         Log.e(LOG_TAG, "[$progress, $linearProgress] Cache MISS for $iKey / $vKey")
133         return newFont
134     }
135 
lerpnull136     private fun lerp(
137         start: Array<FontVariationAxis>,
138         end: Array<FontVariationAxis>,
139         filter: (tag: String, left: Float, right: Float) -> Float,
140     ): List<FontVariationAxis> {
141         // Safe to modify result of Font#getAxes since it returns cloned object.
142         start.sortBy { axis -> axis.tag }
143         end.sortBy { axis -> axis.tag }
144 
145         val result = mutableListOf<FontVariationAxis>()
146         var i = 0
147         var j = 0
148         while (i < start.size || j < end.size) {
149             val tagA = if (i < start.size) start[i].tag else null
150             val tagB = if (j < end.size) end[j].tag else null
151 
152             val comp =
153                 when {
154                     tagA == null -> 1
155                     tagB == null -> -1
156                     else -> tagA.compareTo(tagB)
157                 }
158 
159             val tag =
160                 when {
161                     comp == 0 -> tagA!!
162                     comp < 0 -> tagA!!
163                     else -> tagB!!
164                 }
165 
166             val axisDefinition = GSFAxes.getAxis(tag)
167             require(comp == 0 || axisDefinition != null) {
168                 "Unable to interpolate due to unknown default axes value: $tag"
169             }
170 
171             val axisValue =
172                 when {
173                     comp == 0 -> filter(tag, start[i++].styleValue, end[j++].styleValue)
174                     comp < 0 -> filter(tag, start[i++].styleValue, axisDefinition!!.defaultValue)
175                     else -> filter(tag, axisDefinition!!.defaultValue, end[j++].styleValue)
176                 }
177 
178             // Round axis value to valid intermediate steps. This improves the cache hit rate.
179             val step = axisDefinition?.animationStep ?: DEFAULT_ANIMATION_STEP
180             result.add(FontVariationAxis(tag, (axisValue / step).roundToInt() * step))
181         }
182         return result
183     }
184 
185     companion object {
186         private const val LOG_TAG = "FontInterpolator"
187         private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG)
188         private val EMPTY_AXES = arrayOf<FontVariationAxis>()
189         private const val DEFAULT_ANIMATION_STEP = 1f
190 
191         // Returns true if given two font instance can be interpolated.
canInterpolatenull192         fun canInterpolate(start: Font, end: Font) =
193             start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier
194     }
195 }
196