/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.keyguard import android.graphics.fonts.Font import android.graphics.fonts.FontVariationAxis import android.util.MathUtils private const val TAG_WGHT = "wght" private const val TAG_ITAL = "ital" private const val FONT_WEIGHT_MAX = 1000f private const val FONT_WEIGHT_MIN = 0f private const val FONT_WEIGHT_ANIMATION_STEP = 10f private const val FONT_WEIGHT_DEFAULT_VALUE = 400f private const val FONT_ITALIC_MAX = 1f private const val FONT_ITALIC_MIN = 0f private const val FONT_ITALIC_ANIMATION_STEP = 0.1f private const val FONT_ITALIC_DEFAULT_VALUE = 0f /** * Provide interpolation of two fonts by adjusting font variation settings. */ class FontInterpolator { /** * Cache key for the interpolated font. * * This class is mutable for recycling. */ private data class InterpKey(var l: Font?, var r: Font?, var progress: Float) { fun set(l: Font, r: Font, progress: Float) { this.l = l this.r = r this.progress = progress } } /** * Cache key for the font that has variable font. * * This class is mutable for recycling. */ private data class VarFontKey( var sourceId: Int, var index: Int, val sortedAxes: MutableList ) { constructor(font: Font, axes: List): this(font.sourceIdentifier, font.ttcIndex, axes.toMutableList().apply { sortBy { it.tag } } ) fun set(font: Font, axes: List) { sourceId = font.sourceIdentifier index = font.ttcIndex sortedAxes.clear() sortedAxes.addAll(axes) sortedAxes.sortBy { it.tag } } } // Font interpolator has two level caches: one for input and one for font with different // variation settings. No synchronization is needed since FontInterpolator is not designed to be // thread-safe and can be used only on UI thread. private val interpCache = hashMapOf() private val verFontCache = hashMapOf() // Mutable keys for recycling. private val tmpInterpKey = InterpKey(null, null, 0f) private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf()) /** * Linear interpolate the font variation settings. */ fun lerp(start: Font, end: Font, progress: Float): Font { if (progress == 0f) { return start } else if (progress == 1f) { return end } val startAxes = start.axes ?: EMPTY_AXES val endAxes = end.axes ?: EMPTY_AXES if (startAxes.isEmpty() && endAxes.isEmpty()) { return start } // Check we already know the result. This is commonly happens since we draws the different // text chunks with the same font. tmpInterpKey.set(start, end, progress) val cachedFont = interpCache[tmpInterpKey] if (cachedFont != null) { return cachedFont } // General axes interpolation takes O(N log N), this is came from sorting the axes. Usually // this doesn't take much time since the variation axes is usually up to 5. If we need to // support more number of axes, we may want to preprocess the font and store the sorted axes // and also pre-fill the missing axes value with default value from 'fvar' table. val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue -> when (tag) { // TODO: Good to parse 'fvar' table for retrieving default value. TAG_WGHT -> adjustWeight( MathUtils.lerp( startValue ?: FONT_WEIGHT_DEFAULT_VALUE, endValue ?: FONT_WEIGHT_DEFAULT_VALUE, progress)) TAG_ITAL -> adjustItalic( MathUtils.lerp( startValue ?: FONT_ITALIC_DEFAULT_VALUE, endValue ?: FONT_ITALIC_DEFAULT_VALUE, progress)) else -> { require(startValue != null && endValue != null) { "Unable to interpolate due to unknown default axes value : $tag" } MathUtils.lerp(startValue, endValue, progress) } } } // Check if we already make font for this axes. This is typically happens if the animation // happens backward. tmpVarFontKey.set(start, newAxes) val axesCachedFont = verFontCache[tmpVarFontKey] if (axesCachedFont != null) { interpCache[InterpKey(start, end, progress)] = axesCachedFont return axesCachedFont } // This is the first time to make the font for the axes. Build and store it to the cache. // Font.Builder#build won't throw IOException since creating fonts from existing fonts will // not do any IO work. val newFont = Font.Builder(start) .setFontVariationSettings(newAxes.toTypedArray()) .build() interpCache[InterpKey(start, end, progress)] = newFont verFontCache[VarFontKey(start, newAxes)] = newFont return newFont } private fun lerp( start: Array, end: Array, filter: (tag: String, left: Float?, right: Float?) -> Float ): List { // Safe to modify result of Font#getAxes since it returns cloned object. start.sortBy { axis -> axis.tag } end.sortBy { axis -> axis.tag } val result = mutableListOf() var i = 0 var j = 0 while (i < start.size || j < end.size) { val tagA = if (i < start.size) start[i].tag else null val tagB = if (j < end.size) end[j].tag else null val comp = when { tagA == null -> 1 tagB == null -> -1 else -> tagA.compareTo(tagB) } val axis = when { comp == 0 -> { val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue) FontVariationAxis(tagA, v) } comp < 0 -> { val v = filter(tagA!!, start[i++].styleValue, null) FontVariationAxis(tagA, v) } else -> { // comp > 0 val v = filter(tagB!!, null, end[j++].styleValue) FontVariationAxis(tagB, v) } } result.add(axis) } return result } // For the performance reasons, we animate weight with FONT_WEIGHT_ANIMATION_STEP. This helps // Cache hit ratio in the Skia glyph cache. private fun adjustWeight(value: Float) = coerceInWithStep(value, FONT_WEIGHT_MIN, FONT_WEIGHT_MAX, FONT_WEIGHT_ANIMATION_STEP) // For the performance reasons, we animate italic with FONT_ITALIC_ANIMATION_STEP. This helps // Cache hit ratio in the Skia glyph cache. private fun adjustItalic(value: Float) = coerceInWithStep(value, FONT_ITALIC_MIN, FONT_ITALIC_MAX, FONT_ITALIC_ANIMATION_STEP) private fun coerceInWithStep(v: Float, min: Float, max: Float, step: Float) = (v.coerceIn(min, max) / step).toInt() * step companion object { private val EMPTY_AXES = arrayOf() // Returns true if given two font instance can be interpolated. fun canInterpolate(start: Font, end: Font) = start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier } }