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