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