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 java.lang.Float.max
26 import java.lang.Float.min
27 
28 private const val TAG_WGHT = "wght"
29 private const val TAG_ITAL = "ital"
30 
31 private const val FONT_WEIGHT_DEFAULT_VALUE = 400f
32 private const val FONT_ITALIC_MAX = 1f
33 private const val FONT_ITALIC_MIN = 0f
34 private const val FONT_ITALIC_ANIMATION_STEP = 0.1f
35 private const val FONT_ITALIC_DEFAULT_VALUE = 0f
36 
37 /** Caches for font interpolation */
38 interface FontCache {
39     val animationFrameCount: Int
40 
41     fun get(key: InterpKey): Font?
42 
43     fun get(key: VarFontKey): Font?
44 
45     fun put(key: InterpKey, font: Font)
46 
47     fun put(key: VarFontKey, font: Font)
48 }
49 
50 /** Cache key for the interpolated font. */
51 data class InterpKey(val start: Font?, val end: Font?, val frame: Int)
52 
53 /** Cache key for the font that has variable font. */
54 data class VarFontKey(val sourceId: Int, val index: Int, val sortedAxes: List<FontVariationAxis>) {
55     constructor(
56         font: Font,
57         axes: List<FontVariationAxis>,
<lambda>null58     ) : this(font.sourceIdentifier, font.ttcIndex, axes.sortedBy { it.tag })
59 }
60 
61 class FontCacheImpl(override val animationFrameCount: Int = DEFAULT_FONT_CACHE_MAX_ENTRIES / 2) :
62     FontCache {
63     // Font interpolator has two level caches: one for input and one for font with different
64     // variation settings. No synchronization is needed since FontInterpolator is not designed to be
65     // thread-safe and can be used only on UI thread.
66     val cacheMaxEntries = animationFrameCount * 2
67     private val interpCache = LruCache<InterpKey, Font>(cacheMaxEntries)
68     private val verFontCache = LruCache<VarFontKey, Font>(cacheMaxEntries)
69 
getnull70     override fun get(key: InterpKey): Font? = interpCache[key]
71 
72     override fun get(key: VarFontKey): Font? = verFontCache[key]
73 
74     override fun put(key: InterpKey, font: Font) {
75         interpCache.put(key, font)
76     }
77 
putnull78     override fun put(key: VarFontKey, font: Font) {
79         verFontCache.put(key, font)
80     }
81 
82     companion object {
83         // Benchmarked via Perfetto, difference between 10 and 50 entries is about 0.3ms in frame
84         // draw time on a Pixel 6.
85         @VisibleForTesting const val DEFAULT_FONT_CACHE_MAX_ENTRIES = 10
86     }
87 }
88 
89 /** Provide interpolation of two fonts by adjusting font variation settings. */
90 class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) {
91     /** Linear interpolate the font variation settings. */
lerpnull92     fun lerp(start: Font, end: Font, progress: Float): Font {
93         if (progress == 0f) {
94             return start
95         } else if (progress == 1f) {
96             return end
97         }
98 
99         val startAxes = start.axes ?: EMPTY_AXES
100         val endAxes = end.axes ?: EMPTY_AXES
101 
102         if (startAxes.isEmpty() && endAxes.isEmpty()) {
103             return start
104         }
105 
106         // Check we already know the result. This is commonly happens since we draws the different
107         // text chunks with the same font.
108         val iKey = InterpKey(start, end, (progress * fontCache.animationFrameCount).toInt())
109         fontCache.get(iKey)?.let {
110             if (DEBUG) {
111                 Log.d(LOG_TAG, "[$progress] Interp. cache hit for $iKey")
112             }
113             return it
114         }
115 
116         // General axes interpolation takes O(N log N), this is came from sorting the axes. Usually
117         // this doesn't take much time since the variation axes is usually up to 5. If we need to
118         // support more number of axes, we may want to preprocess the font and store the sorted axes
119         // and also pre-fill the missing axes value with default value from 'fvar' table.
120         val newAxes =
121             lerp(startAxes, endAxes) { tag, startValue, endValue ->
122                 when (tag) {
123                     TAG_WGHT ->
124                         MathUtils.lerp(
125                             startValue ?: FONT_WEIGHT_DEFAULT_VALUE,
126                             endValue ?: FONT_WEIGHT_DEFAULT_VALUE,
127                             progress,
128                         )
129                     TAG_ITAL ->
130                         adjustItalic(
131                             MathUtils.lerp(
132                                 startValue ?: FONT_ITALIC_DEFAULT_VALUE,
133                                 endValue ?: FONT_ITALIC_DEFAULT_VALUE,
134                                 progress,
135                             )
136                         )
137                     else -> {
138                         require(startValue != null && endValue != null) {
139                             "Unable to interpolate due to unknown default axes value : $tag"
140                         }
141                         MathUtils.lerp(startValue, endValue, progress)
142                     }
143                 }
144             }
145 
146         // Check if we already make font for this axes. This is typically happens if the animation
147         // happens backward.
148         val vKey = VarFontKey(start, newAxes)
149         fontCache.get(vKey)?.let {
150             fontCache.put(iKey, it)
151             if (DEBUG) {
152                 Log.d(LOG_TAG, "[$progress] Axis cache hit for $vKey")
153             }
154             return it
155         }
156 
157         // This is the first time to make the font for the axes. Build and store it to the cache.
158         // Font.Builder#build won't throw IOException since creating fonts from existing fonts will
159         // not do any IO work.
160         val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build()
161         fontCache.put(iKey, newFont)
162         fontCache.put(vKey, newFont)
163 
164         // Cache misses are likely to create memory leaks, so this is logged at error level.
165         Log.e(LOG_TAG, "[$progress] Cache MISS for $iKey / $vKey")
166         return newFont
167     }
168 
lerpnull169     private fun lerp(
170         start: Array<FontVariationAxis>,
171         end: Array<FontVariationAxis>,
172         filter: (tag: String, left: Float?, right: Float?) -> Float,
173     ): List<FontVariationAxis> {
174         // Safe to modify result of Font#getAxes since it returns cloned object.
175         start.sortBy { axis -> axis.tag }
176         end.sortBy { axis -> axis.tag }
177 
178         val result = mutableListOf<FontVariationAxis>()
179         var i = 0
180         var j = 0
181         while (i < start.size || j < end.size) {
182             val tagA = if (i < start.size) start[i].tag else null
183             val tagB = if (j < end.size) end[j].tag else null
184 
185             val comp =
186                 when {
187                     tagA == null -> 1
188                     tagB == null -> -1
189                     else -> tagA.compareTo(tagB)
190                 }
191 
192             val axis =
193                 when {
194                     comp == 0 -> {
195                         val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue)
196                         FontVariationAxis(tagA, v)
197                     }
198                     comp < 0 -> {
199                         val v = filter(tagA!!, start[i++].styleValue, null)
200                         FontVariationAxis(tagA, v)
201                     }
202                     else -> { // comp > 0
203                         val v = filter(tagB!!, null, end[j++].styleValue)
204                         FontVariationAxis(tagB, v)
205                     }
206                 }
207 
208             result.add(axis)
209         }
210         return result
211     }
212 
213     // For the performance reasons, we animate italic with FONT_ITALIC_ANIMATION_STEP. This helps
214     // Cache hit ratio in the Skia glyph cache.
adjustItalicnull215     private fun adjustItalic(value: Float) =
216         coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP)
217 
218     private fun coerceInWithStep(v: Float, min: Float, max: Float, step: Float) =
219         (v.coerceIn(min, max) / step).toInt() * step
220 
221     companion object {
222         private const val LOG_TAG = "FontInterpolator"
223         private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG)
224         private val EMPTY_AXES = arrayOf<FontVariationAxis>()
225 
226         // Returns true if given two font instance can be interpolated.
227         fun canInterpolate(start: Font, end: Font) =
228             start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier
229     }
230 }
231