diff options
9 files changed, 223 insertions, 117 deletions
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt index f8bcb81dc073..bc75b1dad40c 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt @@ -22,19 +22,8 @@ import android.util.Log import android.util.LruCache import android.util.MathUtils import androidx.annotation.VisibleForTesting -import java.lang.Float.max -import java.lang.Float.min import kotlin.math.roundToInt -private const val TAG_WGHT = "wght" -private const val TAG_ITAL = "ital" - -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 - /** Caches for font interpolation */ interface FontCache { val animationFrameCount: Int @@ -91,11 +80,8 @@ class FontCacheImpl(override val animationFrameCount: Int = DEFAULT_FONT_CACHE_M class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) { /** Linear interpolate the font variation settings. */ fun lerp(start: Font, end: Font, progress: Float, linearProgress: Float): Font { - if (progress == 0f) { - return start - } else if (progress == 1f) { - return end - } + if (progress <= 0f) return start + if (progress >= 1f) return end val startAxes = start.axes ?: EMPTY_AXES val endAxes = end.axes ?: EMPTY_AXES @@ -110,7 +96,7 @@ class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) { InterpKey(start, end, (linearProgress * fontCache.animationFrameCount).roundToInt()) fontCache.get(iKey)?.let { if (DEBUG) { - Log.d(LOG_TAG, "[$progress] Interp. cache hit for $iKey") + Log.d(LOG_TAG, "[$progress, $linearProgress] Interp. cache hit for $iKey") } return it } @@ -121,37 +107,16 @@ class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) { // and also pre-fill the missing axes value with default value from 'fvar' table. val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue -> - when (tag) { - TAG_WGHT -> - 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) - } - } + MathUtils.lerp(startValue, endValue, progress) } // Check if we already make font for this axes. This is typically happens if the animation - // happens backward. + // happens backward and is being linearly interpolated. val vKey = VarFontKey(start, newAxes) fontCache.get(vKey)?.let { fontCache.put(iKey, it) if (DEBUG) { - Log.d(LOG_TAG, "[$progress] Axis cache hit for $vKey") + Log.d(LOG_TAG, "[$progress, $linearProgress] Axis cache hit for $vKey") } return it } @@ -164,14 +129,14 @@ class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) { fontCache.put(vKey, newFont) // Cache misses are likely to create memory leaks, so this is logged at error level. - Log.e(LOG_TAG, "[$progress] Cache MISS for $iKey / $vKey") + Log.e(LOG_TAG, "[$progress, $linearProgress] Cache MISS for $iKey / $vKey") return newFont } private fun lerp( start: Array<FontVariationAxis>, end: Array<FontVariationAxis>, - filter: (tag: String, left: Float?, right: Float?) -> Float, + 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 } @@ -191,39 +156,37 @@ class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) { else -> tagA.compareTo(tagB) } - val axis = + val tag = + when { + comp == 0 -> tagA!! + comp < 0 -> tagA!! + else -> tagB!! + } + + val axisDefinition = GSFAxes.getAxis(tag) + require(comp == 0 || axisDefinition != null) { + "Unable to interpolate due to unknown default axes value: $tag" + } + + val axisValue = 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) - } + comp == 0 -> filter(tag, start[i++].styleValue, end[j++].styleValue) + comp < 0 -> filter(tag, start[i++].styleValue, axisDefinition!!.defaultValue) + else -> filter(tag, axisDefinition!!.defaultValue, end[j++].styleValue) } - result.add(axis) + // Round axis value to valid intermediate steps. This improves the cache hit rate. + val step = axisDefinition?.animationStep ?: DEFAULT_ANIMATION_STEP + result.add(FontVariationAxis(tag, (axisValue / step).roundToInt() * step)) } return result } - // 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 const val LOG_TAG = "FontInterpolator" private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) private val EMPTY_AXES = arrayOf<FontVariationAxis>() + private const val DEFAULT_ANIMATION_STEP = 1f // Returns true if given two font instance can be interpolated. fun canInterpolate(start: Font, end: Font) = diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt index 9545bda80b2d..9a746870c6ff 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt @@ -1,12 +1,20 @@ -package com.android.systemui.animation +/* + * Copyright (C) 2024 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. + */ -object GSFAxes { - const val WEIGHT = "wght" - const val WIDTH = "wdth" - const val SLANT = "slnt" - const val ROUND = "ROND" - const val OPTICAL_SIZE = "opsz" -} +package com.android.systemui.animation class FontVariationUtils { private var mWeight = -1 @@ -46,20 +54,20 @@ class FontVariationUtils { } var resultString = "" if (mWeight >= 0) { - resultString += "'${GSFAxes.WEIGHT}' $mWeight" + resultString += "'${GSFAxes.WEIGHT.tag}' $mWeight" } if (mWidth >= 0) { resultString += - (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH}' $mWidth" + (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH.tag}' $mWidth" } if (mOpticalSize >= 0) { resultString += (if (resultString.isBlank()) "" else ", ") + - "'${GSFAxes.OPTICAL_SIZE}' $mOpticalSize" + "'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize" } if (mRoundness >= 0) { resultString += - (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND}' $mRoundness" + (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND.tag}' $mRoundness" } return if (isUpdated) resultString else "" } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt new file mode 100644 index 000000000000..f4e03613169a --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2025 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 + +data class AxisDefinition( + val tag: String, + val minValue: Float, + val defaultValue: Float, + val maxValue: Float, + val animationStep: Float, +) + +object GSFAxes { + val WEIGHT = + AxisDefinition( + tag = "wght", + minValue = 1f, + defaultValue = 400f, + maxValue = 1000f, + animationStep = 10f, + ) + + val WIDTH = + AxisDefinition( + tag = "wdth", + minValue = 25f, + defaultValue = 100f, + maxValue = 151f, + animationStep = 1f, + ) + + val SLANT = + AxisDefinition( + tag = "slnt", + minValue = 0f, + defaultValue = 0f, + maxValue = -10f, + animationStep = 0.1f, + ) + + val ROUND = + AxisDefinition( + tag = "ROND", + minValue = 0f, + defaultValue = 0f, + maxValue = 100f, + animationStep = 1f, + ) + + val GRADE = + AxisDefinition( + tag = "GRAD", + minValue = 0f, + defaultValue = 0f, + maxValue = 100f, + animationStep = 1f, + ) + + val OPTICAL_SIZE = + AxisDefinition( + tag = "opsz", + minValue = 6f, + defaultValue = 18f, + maxValue = 144f, + animationStep = 1f, + ) + + // Not a GSF Axis, but present for FontInterpolator compatibility + val ITALIC = + AxisDefinition( + tag = "ITAL", + minValue = 0f, + defaultValue = 0f, + maxValue = 1f, + animationStep = 0.1f, + ) + + private val AXIS_MAP = + listOf(WEIGHT, WIDTH, SLANT, ROUND, GRADE, OPTICAL_SIZE, ITALIC) + .map { def -> def.tag.toLowerCase() to def } + .toMap() + + fun getAxis(axis: String): AxisDefinition? = AXIS_MAP[axis.toLowerCase()] +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index 004d1aa1fe93..ac1c5a8dfaf3 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -130,39 +130,25 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController private val FONT_AXES = listOf( - ClockFontAxis( - key = GSFAxes.WEIGHT, + GSFAxes.WEIGHT.toClockAxis( type = AxisType.Float, - minValue = 25f, currentValue = 400f, - maxValue = 1000f, name = "Weight", description = "Glyph Weight", ), - ClockFontAxis( - key = GSFAxes.WIDTH, + GSFAxes.WIDTH.toClockAxis( type = AxisType.Float, - minValue = 25f, currentValue = 85f, - maxValue = 151f, name = "Width", description = "Glyph Width", ), - ClockFontAxis( - key = GSFAxes.ROUND, + GSFAxes.ROUND.toClockAxis( type = AxisType.Boolean, - minValue = 0f, - currentValue = 0f, - maxValue = 100f, name = "Round", description = "Glyph Roundness", ), - ClockFontAxis( - key = GSFAxes.SLANT, + GSFAxes.SLANT.toClockAxis( type = AxisType.Boolean, - minValue = 0f, - currentValue = 0f, - maxValue = -10f, name = "Slant", description = "Glyph Slant", ), @@ -170,10 +156,10 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController private val LEGACY_FLEX_SETTINGS = listOf( - ClockFontAxisSetting(GSFAxes.WEIGHT, 600f), - ClockFontAxisSetting(GSFAxes.WIDTH, 100f), - ClockFontAxisSetting(GSFAxes.ROUND, 100f), - ClockFontAxisSetting(GSFAxes.SLANT, 0f), + GSFAxes.WEIGHT.toClockAxisSetting(600f), + GSFAxes.WIDTH.toClockAxisSetting(100f), + GSFAxes.ROUND.toClockAxisSetting(100f), + GSFAxes.SLANT.toClockAxisSetting(0f), ) } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index b2dbd6552955..b4c2f5de290f 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -132,7 +132,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: if (!isLargeClock) { axes = axes.map { axis -> - if (axis.key == GSFAxes.WIDTH && axis.value > SMALL_CLOCK_MAX_WDTH) { + if (axis.key == GSFAxes.WIDTH.tag && axis.value > SMALL_CLOCK_MAX_WDTH) { axis.copy(value = SMALL_CLOCK_MAX_WDTH) } else { axis diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FontUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FontUtils.kt new file mode 100644 index 000000000000..212b1e29d1b8 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FontUtils.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 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.shared.clocks + +import com.android.systemui.animation.AxisDefinition +import com.android.systemui.plugins.clocks.AxisType +import com.android.systemui.plugins.clocks.ClockFontAxis +import com.android.systemui.plugins.clocks.ClockFontAxisSetting + +fun AxisDefinition.toClockAxis( + type: AxisType, + currentValue: Float? = null, + name: String, + description: String, +): ClockFontAxis { + return ClockFontAxis( + key = this.tag, + type = type, + maxValue = this.maxValue, + minValue = this.minValue, + currentValue = currentValue ?: this.defaultValue, + name = name, + description = description, + ) +} + +fun AxisDefinition.toClockAxisSetting(value: Float? = null): ClockFontAxisSetting { + return ClockFontAxisSetting(this.tag, value ?: this.defaultValue) +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index 8317aa39ef2b..5b0db225f9cd 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -49,6 +49,7 @@ import com.android.systemui.shared.clocks.DigitTranslateAnimator import com.android.systemui.shared.clocks.DimensionParser import com.android.systemui.shared.clocks.FLEX_CLOCK_ID import com.android.systemui.shared.clocks.FontTextStyle +import com.android.systemui.shared.clocks.toClockAxisSetting import java.lang.Thread import kotlin.math.max import kotlin.math.min @@ -594,25 +595,25 @@ open class SimpleDigitalClockTextView( val FIDGET_INTERPOLATOR = PathInterpolator(0.26873f, 0f, 0.45042f, 1f) val FIDGET_DISTS = mapOf( - GSFAxes.WEIGHT to Pair(200f, 500f), - GSFAxes.WIDTH to Pair(30f, 75f), - GSFAxes.ROUND to Pair(0f, 50f), - GSFAxes.SLANT to Pair(0f, -5f), + GSFAxes.WEIGHT.tag to Pair(200f, 500f), + GSFAxes.WIDTH.tag to Pair(30f, 75f), + GSFAxes.ROUND.tag to Pair(0f, 50f), + GSFAxes.SLANT.tag to Pair(0f, -5f), ) val AOD_COLOR = Color.WHITE - val LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 400f) - val AOD_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 200f) - val WIDTH_AXIS = ClockFontAxisSetting(GSFAxes.WIDTH, 85f) - val ROUND_AXIS = ClockFontAxisSetting(GSFAxes.ROUND, 0f) - val SLANT_AXIS = ClockFontAxisSetting(GSFAxes.SLANT, 0f) + val LS_WEIGHT_AXIS = GSFAxes.WEIGHT.toClockAxisSetting(400f) + val AOD_WEIGHT_AXIS = GSFAxes.WEIGHT.toClockAxisSetting(200f) + val WIDTH_AXIS = GSFAxes.WIDTH.toClockAxisSetting(85f) + val ROUND_AXIS = GSFAxes.ROUND.toClockAxisSetting(0f) + val SLANT_AXIS = GSFAxes.SLANT.toClockAxisSetting(0f) // Axes for Legacy version of the Flex Clock - val FLEX_LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 600f) - val FLEX_AOD_LARGE_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 74f) - val FLEX_AOD_SMALL_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 133f) - val FLEX_LS_WIDTH_AXIS = ClockFontAxisSetting(GSFAxes.WIDTH, 100f) - val FLEX_AOD_WIDTH_AXIS = ClockFontAxisSetting(GSFAxes.WIDTH, 43f) - val FLEX_ROUND_AXIS = ClockFontAxisSetting(GSFAxes.ROUND, 100f) + val FLEX_LS_WEIGHT_AXIS = GSFAxes.WEIGHT.toClockAxisSetting(600f) + val FLEX_AOD_LARGE_WEIGHT_AXIS = GSFAxes.WEIGHT.toClockAxisSetting(74f) + val FLEX_AOD_SMALL_WEIGHT_AXIS = GSFAxes.WEIGHT.toClockAxisSetting(133f) + val FLEX_LS_WIDTH_AXIS = GSFAxes.WIDTH.toClockAxisSetting(100f) + val FLEX_AOD_WIDTH_AXIS = GSFAxes.WIDTH.toClockAxisSetting(43f) + val FLEX_ROUND_AXIS = GSFAxes.ROUND.toClockAxisSetting(100f) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt index f44769d522eb..8d3640d8d809 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt @@ -21,7 +21,7 @@ class FontVariationUtilsTest : SysuiTestCase() { roundness = 100, ) Assert.assertEquals( - "'${GSFAxes.WEIGHT}' 100, '${GSFAxes.WIDTH}' 100, '${GSFAxes.ROUND}' 100", + "'${GSFAxes.WEIGHT.tag}' 100, '${GSFAxes.WIDTH.tag}' 100, '${GSFAxes.ROUND.tag}' 100", initFvar, ) val updatedFvar = @@ -32,7 +32,8 @@ class FontVariationUtilsTest : SysuiTestCase() { roundness = 100, ) Assert.assertEquals( - "'${GSFAxes.WEIGHT}' 200, '${GSFAxes.WIDTH}' 100, '${GSFAxes.OPTICAL_SIZE}' 0, '${GSFAxes.ROUND}' 100", + "'${GSFAxes.WEIGHT.tag}' 200, '${GSFAxes.WIDTH.tag}' 100," + + " '${GSFAxes.OPTICAL_SIZE.tag}' 0, '${GSFAxes.ROUND.tag}' 100", updatedFvar, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt index 2e634390679a..c2a495d13c02 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt @@ -64,6 +64,12 @@ class FontInterpolatorTest : SysuiTestCase() { "'wght' 500, 'ital' 0.5, 'GRAD' 450", interp.lerp(startFont, endFont, 0.5f, 0.5f), ) + + // Ensure axes rounded correctly to nearest step + assertSameAxes( + "'wght' 490, 'ital' 0.5, 'GRAD' 446", + interp.lerp(startFont, endFont, 0.492f, 0.492f), + ) } @Test |