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

Commit 5f7036df authored by Tyler Freeman's avatar Tyler Freeman
Browse files

feat(non linear font scaling)!: add FontScaleConverter for non-linear font scaling.

Non-linear font scaling is meant to improve readability at larger font
scales: larger fonts will scale up more slowly than smaller fonts, so we
don't get ridiculously huge fonts that don't fit on the screen.

The thinking here is that large fonts are already big enough to read,
but we still want to scale them slightly to preserve the visual
hierarchy when compared to smaller fonts.

The FontScaleConverter converts SP dimensions to DP, to be used in
TypedValue.applyDimension() which will affect most TextViews and font
sizes automatically.

For now, all the lookup tables are hardcoded. A follow-up CL will make
them configurable via XML. The hardcoded arrays also default to a linear
curve, as to not break any tests. The non-linear curve will come in a
follow-up CL, to make it easier to roll-back if requested.

Also included is some Javascript to generate the hardcoded arrays, which
can later be manually tweaked and optimized.

Test: unit, CTS, and manual:
1. Run `adb shell settings put system font_scale 2.0`
2. Check different apps to see if they are readable.

Bug: b/237558231

Change-Id: I17d67252bf31f55e57e1f3e8a0f638770e6d2cfd
parent eb803fd5
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -563,6 +563,9 @@ public class CompatibilityInfo implements Parcelable {
        if (applyToSize) {
            inoutDm.widthPixels = (int) (inoutDm.widthPixels * invertedRatio + 0.5f);
            inoutDm.heightPixels = (int) (inoutDm.heightPixels * invertedRatio + 0.5f);

            float fontScale = inoutDm.scaledDensity / inoutDm.density;
            inoutDm.fontScaleConverter = FontScaleConverterFactory.forScale(fontScale);
        }
    }

+148 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 android.content.res;

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

import com.android.internal.annotations.VisibleForTesting;

import java.util.Arrays;

/**
 * A lookup table for non-linear font scaling. Converts font sizes given in "sp" dimensions to a
 * "dp" dimension according to a non-linear curve.
 *
 * <p>This is meant to improve readability at larger font scales: larger fonts will scale up more
 * slowly than smaller fonts, so we don't get ridiculously huge fonts that don't fit on the screen.
 *
 * <p>The thinking here is that large fonts are already big enough to read, but we still want to
 * scale them slightly to preserve the visual hierarchy when compared to smaller fonts.
 *
 * @hide
 */
public class FontScaleConverter {
    /**
     * How close the given SP should be to a canonical SP in the array before they are considered
     * the same for lookup purposes.
     */
    private static final float THRESHOLD_FOR_MATCHING_SP = 0.02f;

    @VisibleForTesting
    final float[] mFromSpValues;
    @VisibleForTesting
    final float[] mToDpValues;

    /**
     * Creates a lookup table for the given conversions.
     *
     * <p>Any "sp" value not in the lookup table will be derived via linear interpolation.
     *
     * <p>The arrays must be sorted ascending and monotonically increasing.
     *
     * @param fromSp array of dimensions in SP
     * @param toDp array of dimensions in DP that correspond to an SP value in fromSp
     *
     * @throws IllegalArgumentException if the array lengths don't match or are empty
     * @hide
     */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public FontScaleConverter(@NonNull float[] fromSp, @NonNull float[] toDp) {
        if (fromSp.length != toDp.length || fromSp.length == 0) {
            throw new IllegalArgumentException("Array lengths must match and be nonzero");
        }

        mFromSpValues = fromSp;
        mToDpValues = toDp;
    }

    /**
     * Convert a dimension in "sp" to "dp" using the lookup table.
     *
     * @hide
     */
    public float convertSpToDp(float sp) {
        final float spPositive = Math.abs(sp);
        // TODO(b/247861374): find a match at a higher index?
        final int spRounded = Math.round(spPositive);
        final float sign = Math.signum(sp);
        final int index = Arrays.binarySearch(mFromSpValues, spRounded);
        if (index >= 0 && Math.abs(spRounded - spPositive) < THRESHOLD_FOR_MATCHING_SP) {
            // exact match, return the matching dp
            return sign * mToDpValues[index];
        } else {
            // must be a value in between index and index + 1: interpolate.
            final int lowerIndex = -(index + 1) - 1;

            final float startSp;
            final float endSp;
            final float startDp;
            final float endDp;

            if (lowerIndex >= mFromSpValues.length - 1) {
                // It's past our lookup table. Determine the last elements' scaling factor and use.
                startSp = mFromSpValues[mFromSpValues.length - 1];
                startDp = mToDpValues[mFromSpValues.length - 1];

                if (startSp == 0) return 0;

                final float scalingFactor = startDp / startSp;
                return sp * scalingFactor;
            } else if (lowerIndex == -1) {
                // It's smaller than the smallest value in our table. Interpolate from 0.
                startSp = 0;
                startDp = 0;
                endSp = mFromSpValues[0];
                endDp = mToDpValues[0];
            } else {
                startSp = mFromSpValues[lowerIndex];
                endSp = mFromSpValues[lowerIndex + 1];
                startDp = mToDpValues[lowerIndex];
                endDp = mToDpValues[lowerIndex + 1];
            }

            return sign * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, spPositive);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (!(o instanceof FontScaleConverter)) return false;
        FontScaleConverter that = (FontScaleConverter) o;
        return Arrays.equals(mFromSpValues, that.mFromSpValues)
                && Arrays.equals(mToDpValues, that.mToDpValues);
    }

    @Override
    public int hashCode() {
        int result = Arrays.hashCode(mFromSpValues);
        result = 31 * result + Arrays.hashCode(mToDpValues);
        return result;
    }

    @Override
    public String toString() {
        return "FontScaleConverter{"
                + "fromSpValues="
                + Arrays.toString(mFromSpValues)
                + ", toDpValues="
                + Arrays.toString(mToDpValues)
                + '}';
    }
}
+119 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 android.content.res;

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

import com.android.internal.annotations.VisibleForTesting;

/**
 * Stores lookup tables for creating {@link FontScaleConverter}s at various scales.
 *
 * @hide
 */
public class FontScaleConverterFactory {
    private static final float SCALE_KEY_MULTIPLIER = 100f;

    @VisibleForTesting
    static final SparseArray<FontScaleConverter> LOOKUP_TABLES = new SparseArray<>();

    static {
        // These were generated by frameworks/base/tools/fonts/font-scaling-array-generator.js and
        // manually tweaked for optimum readability.
        put(
                /* scaleKey= */ 1.15f,
                new FontScaleConverter(
                        /* fromSp= */
                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
                        /* toDp=   */
                        new float[] { 9.2f, 11.5f, 13.8f, 16.1f, 20.7f,   23f, 27.6f, 34.5f,  115})
        );

        put(
                /* scaleKey= */ 1.3f,
                new FontScaleConverter(
                        /* fromSp= */
                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
                        /* toDp=   */
                        new float[] {10.4f,   13f, 15.6f, 18.2f, 23.4f,   26f, 31.2f,   39f,  130})
        );

        put(
                /* scaleKey= */ 1.5f,
                new FontScaleConverter(
                        /* fromSp= */
                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
                        /* toDp=   */
                        new float[] {  12f,   15f,   18f,   21f,   27f,   30f,   36f,   45f,  150})
        );

        put(
                /* scaleKey= */ 1.8f,
                new FontScaleConverter(
                        /* fromSp= */
                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
                        /* toDp=   */
                        new float[] {14.4f,   18f, 21.6f, 25.2f, 32.4f,   36f, 43.2f,   54f,  180})
        );

        put(
                /* scaleKey= */ 2f,
                new FontScaleConverter(
                        /* fromSp= */
                        new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
                        /* toDp=   */
                        new float[] {  16f,   20f,   24f,   28f,   36f,   40f,   48f,   60f,  200})
        );

    }

    private FontScaleConverterFactory() {}

    /**
     * Finds a matching FontScaleConverter for the given fontScale factor.
     *
     * @param fontScale the scale factor, usually from {@link Configuration#fontScale}.
     *
     * @return a converter for the given scale, or null if non-linear scaling should not be used.
     *
     * @hide
     */
    @Nullable
    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
            return null;
        }

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

        return lookupTable;
    }

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

    @Nullable
    private static FontScaleConverter get(float scaleKey) {
        return LOOKUP_TABLES.get((int) (scaleKey * SCALE_KEY_MULTIPLIER));
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -430,6 +430,8 @@ public class ResourcesImpl {
                // Protect against an unset fontScale.
                mMetrics.scaledDensity = mMetrics.density *
                        (mConfiguration.fontScale != 0 ? mConfiguration.fontScale : 1.0f);
                mMetrics.fontScaleConverter =
                        FontScaleConverterFactory.forScale(mConfiguration.fontScale);

                final int width, height;
                if (mMetrics.widthPixels >= mMetrics.heightPixels) {
+12 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.util;

import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.res.FontScaleConverter;
import android.os.SystemProperties;

/**
@@ -265,6 +266,15 @@ public class DisplayMetrics {
     * increments at runtime based on a user preference for the font size.
     */
    public float scaledDensity;

    /**
     * If non-null, this will be used to calculate font sizes instead of {@link #scaledDensity}.
     *
     * @hide
     */
    @Nullable
    public FontScaleConverter fontScaleConverter;

    /**
     * The exact physical pixels per inch of the screen in the X dimension.
     */
@@ -342,6 +352,7 @@ public class DisplayMetrics {
        noncompatScaledDensity = o.noncompatScaledDensity;
        noncompatXdpi = o.noncompatXdpi;
        noncompatYdpi = o.noncompatYdpi;
        fontScaleConverter = o.fontScaleConverter;
    }
    
    public void setToDefaults() {
@@ -359,6 +370,7 @@ public class DisplayMetrics {
        noncompatScaledDensity = scaledDensity;
        noncompatXdpi = xdpi;
        noncompatYdpi = ydpi;
        fontScaleConverter = null;
    }

    @Override
Loading