1 /* 2 * 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.keyguard 18 19 import android.graphics.fonts.Font 20 import android.graphics.fonts.FontVariationAxis 21 import android.util.MathUtils 22 23 private const val TAG_WGHT = "wght" 24 private const val TAG_ITAL = "ital" 25 26 private const val FONT_WEIGHT_MAX = 1000f 27 private const val FONT_WEIGHT_MIN = 0f 28 private const val FONT_WEIGHT_ANIMATION_STEP = 10f 29 private const val FONT_WEIGHT_DEFAULT_VALUE = 400f 30 31 private const val FONT_ITALIC_MAX = 1f 32 private const val FONT_ITALIC_MIN = 0f 33 private const val FONT_ITALIC_ANIMATION_STEP = 0.1f 34 private const val FONT_ITALIC_DEFAULT_VALUE = 0f 35 36 /** 37 * Provide interpolation of two fonts by adjusting font variation settings. 38 */ 39 class FontInterpolator { 40 41 /** 42 * Cache key for the interpolated font. 43 * 44 * This class is mutable for recycling. 45 */ 46 private data class InterpKey(var l: Font?, var r: Font?, var progress: Float) { 47 fun set(l: Font, r: Font, progress: Float) { 48 this.l = l 49 this.r = r 50 this.progress = progress 51 } 52 } 53 54 /** 55 * Cache key for the font that has variable font. 56 * 57 * This class is mutable for recycling. 58 */ 59 private data class VarFontKey( 60 var sourceId: Int, 61 var index: Int, 62 val sortedAxes: MutableList<FontVariationAxis> 63 ) { 64 constructor(font: Font, axes: List<FontVariationAxis>): 65 this(font.sourceIdentifier, 66 font.ttcIndex, 67 axes.toMutableList().apply { sortBy { it.tag } } 68 ) 69 70 fun set(font: Font, axes: List<FontVariationAxis>) { 71 sourceId = font.sourceIdentifier 72 index = font.ttcIndex 73 sortedAxes.clear() 74 sortedAxes.addAll(axes) 75 sortedAxes.sortBy { it.tag } 76 } 77 } 78 79 // Font interpolator has two level caches: one for input and one for font with different 80 // variation settings. No synchronization is needed since FontInterpolator is not designed to be 81 // thread-safe and can be used only on UI thread. 82 private val interpCache = hashMapOf<InterpKey, Font>() 83 private val verFontCache = hashMapOf<VarFontKey, Font>() 84 85 // Mutable keys for recycling. 86 private val tmpInterpKey = InterpKey(null, null, 0f) 87 private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf()) 88 89 /** 90 * Linear interpolate the font variation settings. 91 */ 92 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 tmpInterpKey.set(start, end, progress) 109 val cachedFont = interpCache[tmpInterpKey] 110 if (cachedFont != null) { 111 return cachedFont 112 } 113 114 // General axes interpolation takes O(N log N), this is came from sorting the axes. Usually 115 // this doesn't take much time since the variation axes is usually up to 5. If we need to 116 // support more number of axes, we may want to preprocess the font and store the sorted axes 117 // and also pre-fill the missing axes value with default value from 'fvar' table. 118 val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue -> 119 when (tag) { 120 // TODO: Good to parse 'fvar' table for retrieving default value. 121 TAG_WGHT -> adjustWeight( 122 MathUtils.lerp( 123 startValue ?: FONT_WEIGHT_DEFAULT_VALUE, 124 endValue ?: FONT_WEIGHT_DEFAULT_VALUE, 125 progress)) 126 TAG_ITAL -> adjustItalic( 127 MathUtils.lerp( 128 startValue ?: FONT_ITALIC_DEFAULT_VALUE, 129 endValue ?: FONT_ITALIC_DEFAULT_VALUE, 130 progress)) 131 else -> { 132 require(startValue != null && endValue != null) { 133 "Unable to interpolate due to unknown default axes value : $tag" 134 } 135 MathUtils.lerp(startValue, endValue, progress) 136 } 137 } 138 } 139 140 // Check if we already make font for this axes. This is typically happens if the animation 141 // happens backward. 142 tmpVarFontKey.set(start, newAxes) 143 val axesCachedFont = verFontCache[tmpVarFontKey] 144 if (axesCachedFont != null) { 145 interpCache[InterpKey(start, end, progress)] = axesCachedFont 146 return axesCachedFont 147 } 148 149 // This is the first time to make the font for the axes. Build and store it to the cache. 150 // Font.Builder#build won't throw IOException since creating fonts from existing fonts will 151 // not do any IO work. 152 val newFont = Font.Builder(start) 153 .setFontVariationSettings(newAxes.toTypedArray()) 154 .build() 155 interpCache[InterpKey(start, end, progress)] = newFont 156 verFontCache[VarFontKey(start, newAxes)] = newFont 157 return newFont 158 } 159 160 private fun lerp( 161 start: Array<FontVariationAxis>, 162 end: Array<FontVariationAxis>, 163 filter: (tag: String, left: Float?, right: Float?) -> Float 164 ): List<FontVariationAxis> { 165 // Safe to modify result of Font#getAxes since it returns cloned object. 166 start.sortBy { axis -> axis.tag } 167 end.sortBy { axis -> axis.tag } 168 169 val result = mutableListOf<FontVariationAxis>() 170 var i = 0 171 var j = 0 172 while (i < start.size || j < end.size) { 173 val tagA = if (i < start.size) start[i].tag else null 174 val tagB = if (j < end.size) end[j].tag else null 175 176 val comp = when { 177 tagA == null -> 1 178 tagB == null -> -1 179 else -> tagA.compareTo(tagB) 180 } 181 182 val axis = when { 183 comp == 0 -> { 184 val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue) 185 FontVariationAxis(tagA, v) 186 } 187 comp < 0 -> { 188 val v = filter(tagA!!, start[i++].styleValue, null) 189 FontVariationAxis(tagA, v) 190 } 191 else -> { // comp > 0 192 val v = filter(tagB!!, null, end[j++].styleValue) 193 FontVariationAxis(tagB, v) 194 } 195 } 196 197 result.add(axis) 198 } 199 return result 200 } 201 202 // For the performance reasons, we animate weight with FONT_WEIGHT_ANIMATION_STEP. This helps 203 // Cache hit ratio in the Skia glyph cache. 204 private fun adjustWeight(value: Float) = 205 coerceInWithStep(value, FONT_WEIGHT_MIN, FONT_WEIGHT_MAX, FONT_WEIGHT_ANIMATION_STEP) 206 207 // For the performance reasons, we animate italic with FONT_ITALIC_ANIMATION_STEP. This helps 208 // Cache hit ratio in the Skia glyph cache. 209 private fun adjustItalic(value: Float) = 210 coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP) 211 212 private fun coerceInWithStep(v: Float, min: Float, max: Float, step: Float) = 213 (v.coerceIn(min, max) / step).toInt() * step 214 215 companion object { 216 private val EMPTY_AXES = arrayOf<FontVariationAxis>() 217 218 // Returns true if given two font instance can be interpolated. 219 fun canInterpolate(start: Font, end: Font) = 220 start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier 221 } 222 } 223