blob: 3d341af3b3970a9b7b0d1010fce8ce32613ff48a [file] [log] [blame]
/*
* 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.systemui.animation
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<FontVariationAxis>
) {
constructor(font: Font, axes: List<FontVariationAxis>) :
this(font.sourceIdentifier,
font.ttcIndex,
axes.toMutableList().apply { sortBy { it.tag } }
)
fun set(font: Font, axes: List<FontVariationAxis>) {
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<InterpKey, Font>()
private val verFontCache = hashMapOf<VarFontKey, Font>()
// 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<FontVariationAxis>,
end: Array<FontVariationAxis>,
filter: (tag: String, left: Float?, right: Float?) -> Float
): List<FontVariationAxis> {
// 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<FontVariationAxis>()
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<FontVariationAxis>()
// Returns true if given two font instance can be interpolated.
fun canInterpolate(start: Font, end: Font) =
start.ttcIndex == end.ttcIndex && start.sourceIdentifier == end.sourceIdentifier
}
}