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