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