• 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.LruCache
22 import android.util.MathUtils
23 import android.util.MathUtils.abs
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_WEIGHT_ANIMATION_FRAME_COUNT = 100
33 
34 private const val FONT_ITALIC_MAX = 1f
35 private const val FONT_ITALIC_MIN = 0f
36 private const val FONT_ITALIC_ANIMATION_STEP = 0.1f
37 private const val FONT_ITALIC_DEFAULT_VALUE = 0f
38 
39 // Benchmarked via Perfetto, difference between 10 and 50 entries is about 0.3ms in
40 // frame draw time on a Pixel 6.
41 @VisibleForTesting const val FONT_CACHE_MAX_ENTRIES = 10
42 
43 /** Provide interpolation of two fonts by adjusting font variation settings. */
44 class FontInterpolator {
45 
46     /**
47      * Cache key for the interpolated font.
48      *
49      * This class is mutable for recycling.
50      */
51     private data class InterpKey(var l: Font?, var r: Font?, var progress: Float) {
52         fun set(l: Font, r: Font, progress: Float) {
53             this.l = l
54             this.r = r
55             this.progress = progress
56         }
57     }
58 
59     /**
60      * Cache key for the font that has variable font.
61      *
62      * This class is mutable for recycling.
63      */
64     private data class VarFontKey(
65         var sourceId: Int,
66         var index: Int,
67         val sortedAxes: MutableList<FontVariationAxis>
68     ) {
69         constructor(
70             font: Font,
71             axes: List<FontVariationAxis>
72         ) : this(
73             font.sourceIdentifier,
74             font.ttcIndex,
75             axes.toMutableList().apply { sortBy { it.tag } }
76         )
77 
78         fun set(font: Font, axes: List<FontVariationAxis>) {
79             sourceId = font.sourceIdentifier
80             index = font.ttcIndex
81             sortedAxes.clear()
82             sortedAxes.addAll(axes)
83             sortedAxes.sortBy { it.tag }
84         }
85     }
86 
87     // Font interpolator has two level caches: one for input and one for font with different
88     // variation settings. No synchronization is needed since FontInterpolator is not designed to be
89     // thread-safe and can be used only on UI thread.
90     private val interpCache = LruCache<InterpKey, Font>(FONT_CACHE_MAX_ENTRIES)
91     private val verFontCache = LruCache<VarFontKey, Font>(FONT_CACHE_MAX_ENTRIES)
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             return cachedFont
118         }
119 
120         // General axes interpolation takes O(N log N), this is came from sorting the axes. Usually
121         // this doesn't take much time since the variation axes is usually up to 5. If we need to
122         // support more number of axes, we may want to preprocess the font and store the sorted axes
123         // and also pre-fill the missing axes value with default value from 'fvar' table.
124         val newAxes =
125             lerp(startAxes, endAxes) { tag, startValue, endValue ->
126                 when (tag) {
127                     // TODO: Good to parse 'fvar' table for retrieving default value.
128                     TAG_WGHT -> {
129                         adaptiveAdjustWeight(
130                             MathUtils.lerp(
131                                 startValue ?: FONT_WEIGHT_DEFAULT_VALUE,
132                                 endValue ?: FONT_WEIGHT_DEFAULT_VALUE,
133                                 progress
134                             ),
135                             startValue ?: FONT_WEIGHT_DEFAULT_VALUE,
136                             endValue ?: FONT_WEIGHT_DEFAULT_VALUE,
137                         )
138                     }
139                     TAG_ITAL ->
140                         adjustItalic(
141                             MathUtils.lerp(
142                                 startValue ?: FONT_ITALIC_DEFAULT_VALUE,
143                                 endValue ?: FONT_ITALIC_DEFAULT_VALUE,
144                                 progress
145                             )
146                         )
147                     else -> {
148                         require(startValue != null && endValue != null) {
149                             "Unable to interpolate due to unknown default axes value : $tag"
150                         }
151                         MathUtils.lerp(startValue, endValue, progress)
152                     }
153                 }
154             }
155 
156         // Check if we already make font for this axes. This is typically happens if the animation
157         // happens backward.
158         tmpVarFontKey.set(start, newAxes)
159         val axesCachedFont = verFontCache[tmpVarFontKey]
160         if (axesCachedFont != null) {
161             interpCache.put(InterpKey(start, end, progress), axesCachedFont)
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         return newFont
172     }
173 
174     private fun lerp(
175         start: Array<FontVariationAxis>,
176         end: Array<FontVariationAxis>,
177         filter: (tag: String, left: Float?, right: Float?) -> Float
178     ): List<FontVariationAxis> {
179         // Safe to modify result of Font#getAxes since it returns cloned object.
180         start.sortBy { axis -> axis.tag }
181         end.sortBy { axis -> axis.tag }
182 
183         val result = mutableListOf<FontVariationAxis>()
184         var i = 0
185         var j = 0
186         while (i < start.size || j < end.size) {
187             val tagA = if (i < start.size) start[i].tag else null
188             val tagB = if (j < end.size) end[j].tag else null
189 
190             val comp =
191                 when {
192                     tagA == null -> 1
193                     tagB == null -> -1
194                     else -> tagA.compareTo(tagB)
195                 }
196 
197             val axis =
198                 when {
199                     comp == 0 -> {
200                         val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue)
201                         FontVariationAxis(tagA, v)
202                     }
203                     comp < 0 -> {
204                         val v = filter(tagA!!, start[i++].styleValue, null)
205                         FontVariationAxis(tagA, v)
206                     }
207                     else -> { // comp > 0
208                         val v = filter(tagB!!, null, end[j++].styleValue)
209                         FontVariationAxis(tagB, v)
210                     }
211                 }
212 
213             result.add(axis)
214         }
215         return result
216     }
217 
218     // For the performance reasons, we animate weight with adaptive step. This helps
219     // Cache hit ratio in the Skia glyph cache.
220     // The reason we don't use fix step is because the range of weight axis is not normalized,
221     // some are from 50 to 100, others are from 0 to 1000, so we cannot give a constant proper step
222     private fun adaptiveAdjustWeight(value: Float, start: Float, end: Float): Float {
223         val step = max(abs(end - start) / FONT_WEIGHT_ANIMATION_FRAME_COUNT, 1F)
224         return coerceInWithStep(value, min(start, end), max(start, end), step)
225     }
226 
227     // For the performance reasons, we animate italic with FONT_ITALIC_ANIMATION_STEP. This helps
228     // Cache hit ratio in the Skia glyph cache.
229     private fun adjustItalic(value: Float) =
230         coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP)
231 
232     private fun coerceInWithStep(v: Float, min: Float, max: Float, step: Float) =
233         (v.coerceIn(min, max) / step).toInt() * step
234 
235     companion object {
236         private val EMPTY_AXES = arrayOf<FontVariationAxis>()
237 
238         // Returns true if given two font instance can be interpolated.
239         fun canInterpolate(start: Font, end: Font) =
240             start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier
241     }
242 }
243