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