Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit de466db1 authored by Tyler Freeman's avatar Tyler Freeman
Browse files

feat(non linear font scaling): interpolate between two tables if one cannot be...

feat(non linear font scaling): interpolate between two tables if one cannot be found for a given scale

If a new scale is defined in the OEM’s Settings XML that doesn't match
one of the hardcoded default tables, one will be generated by
interpolating between two default tables. This way the experience is
guaranteed for the user, even if the OEM forgets to define their tables
to match their font scaling settings.

Test: atest FrameworksCoreTests:android.content.res.FontScaleConverterFactoryTest
Bug: b/247861716

Change-Id: If9c7f87f6dd4b99afa8579cfa0b52e313d761beb
parent 0053ac90
Loading
Loading
Loading
Loading
+65 −5
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.content.res;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.MathUtils;
import android.util.SparseArray;

import com.android.internal.annotations.VisibleForTesting;
@@ -98,22 +99,81 @@ public class FontScaleConverterFactory {
    public static FontScaleConverter forScale(float fontScale) {
        if (fontScale <= 1) {
            // We don't need non-linear curves for shrinking text or for 100%.
            // Also, fontScale==0 should not have a curve either
            // Also, fontScale==0 should not have a curve either.
            // And ignore negative font scales; that's just silly.
            return null;
        }

        FontScaleConverter lookupTable = get(fontScale);
        // TODO(b/247861716): interpolate between two tables when null

        if (lookupTable != null) {
            return lookupTable;
        }

        // Didn't find an exact match: interpolate between two existing tables
        final int index = LOOKUP_TABLES.indexOfKey(getKey(fontScale));
        if (index >= 0) {
            // This should never happen, should have been covered by get() above.
            return LOOKUP_TABLES.valueAt(index);
        }
        // Didn't find an exact match: interpolate between two existing tables
        final int lowerIndex = -(index + 1) - 1;
        final int higherIndex = lowerIndex + 1;
        if (lowerIndex < 0 || higherIndex >= LOOKUP_TABLES.size()) {
            // We have gone beyond our bounds and have nothing to interpolate between. Just give
            // them a straight linear table instead.
            // This works because when FontScaleConverter encounters a size beyond its bounds, it
            // calculates a linear fontScale factor using the ratio of the last element pair.
            return new FontScaleConverter(new float[] {1f}, new float[] {fontScale});
        } else {
            float startScale = getScaleFromKey(LOOKUP_TABLES.keyAt(lowerIndex));
            float endScale = getScaleFromKey(LOOKUP_TABLES.keyAt(higherIndex));
            float interpolationPoint = MathUtils.constrainedMap(
                    /* rangeMin= */ 0f,
                    /* rangeMax= */ 1f,
                    startScale,
                    endScale,
                    fontScale
            );
            return createInterpolatedTableBetween(
                    LOOKUP_TABLES.valueAt(lowerIndex),
                    LOOKUP_TABLES.valueAt(higherIndex),
                    interpolationPoint);
        }
    }

    @NonNull
    private static FontScaleConverter createInterpolatedTableBetween(
            FontScaleConverter start,
            FontScaleConverter end,
            float interpolationPoint
    ) {
        float[] commonSpSizes = new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100f};
        float[] dpInterpolated = new float[commonSpSizes.length];

        for (int i = 0; i < commonSpSizes.length; i++) {
            float sp = commonSpSizes[i];
            float startDp = start.convertSpToDp(sp);
            float endDp = end.convertSpToDp(sp);
            dpInterpolated[i] = MathUtils.lerp(startDp, endDp, interpolationPoint);
        }

        return new FontScaleConverter(commonSpSizes, dpInterpolated);
    }

    private static int getKey(float fontScale) {
        return (int) (fontScale * SCALE_KEY_MULTIPLIER);
    }

    private static float getScaleFromKey(int key) {
        return (float) key / SCALE_KEY_MULTIPLIER;
    }

    private static void put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter) {
        LOOKUP_TABLES.put((int) (scaleKey * SCALE_KEY_MULTIPLIER), fontScaleConverter);
        LOOKUP_TABLES.put(getKey(scaleKey), fontScaleConverter);
    }

    @Nullable
    private static FontScaleConverter get(float scaleKey) {
        return LOOKUP_TABLES.get((int) (scaleKey * SCALE_KEY_MULTIPLIER));
        return LOOKUP_TABLES.get(getKey(scaleKey));
    }
}
+48 −6
Original line number Diff line number Diff line
@@ -43,14 +43,56 @@ class FontScaleConverterFactoryTest {
        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
    }

    @SmallTest
    fun missingLookupTableReturnsNull() {
        assertThat(FontScaleConverterFactory.forScale(3F)).isNull()
    @LargeTest
    @Test
    fun missingLookupTablePastEnd_returnsLinear() {
        val table = FontScaleConverterFactory.forScale(3F)!!
        generateSequenceOfFractions(-10000f..10000f, step = 0.01f)
            .map {
                assertThat(table.convertSpToDp(it)).isWithin(CONVERSION_TOLERANCE).of(it * 3f)
            }
        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(3f)
        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(24f)
        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(30f)
        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(15f)
        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
        assertThat(table.convertSpToDp(50F)).isWithin(CONVERSION_TOLERANCE).of(150f)
        assertThat(table.convertSpToDp(100F)).isWithin(CONVERSION_TOLERANCE).of(300f)
    }

    @SmallTest
    fun missingLookupTable105ReturnsNull() {
        assertThat(FontScaleConverterFactory.forScale(1.05F)).isNull()
    fun missingLookupTable110_returnsInterpolated() {
        val table = FontScaleConverterFactory.forScale(1.1F)!!

        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1.1f)
        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f * 1.1f)
        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(11f)
        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f * 1.1f)
        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
        assertThat(table.convertSpToDp(50F)).isLessThan(50f * 1.1f)
        assertThat(table.convertSpToDp(100F)).isLessThan(100f * 1.1f)
    }

    @Test
    fun missingLookupTable199_returnsInterpolated() {
        val table = FontScaleConverterFactory.forScale(1.9999F)!!
        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(2f)
        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(16f)
        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(20f)
        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f)
        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
    }

    @Test
    fun missingLookupTable160_returnsInterpolated() {
        val table = FontScaleConverterFactory.forScale(1.6F)!!
        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1f * 1.6F)
        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f * 1.6F)
        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(10f * 1.6F)
        assertThat(table.convertSpToDp(20F)).isLessThan(20f * 1.6F)
        assertThat(table.convertSpToDp(100F)).isLessThan(100f * 1.6F)
        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f * 1.6F)
        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
    }

    @SmallTest
@@ -83,7 +125,7 @@ class FontScaleConverterFactoryTest {
    @Test
    fun allFeasibleScalesAndConversionsDoNotCrash() {
        generateSequenceOfFractions(-10f..10f, step = 0.01f)
            .mapNotNull{ FontScaleConverterFactory.forScale(it) }
            .mapNotNull{ FontScaleConverterFactory.forScale(it) }!!
            .flatMap{ table ->
                generateSequenceOfFractions(-2000f..2000f, step = 0.01f)
                    .map{ Pair(table, it) }