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

Commit 8d27c177 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "feat(non linear font scaling): add TypedValue.deriveDimension()"

parents 2c672560 0f770686
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -48550,6 +48550,7 @@ package android.util {
    method public static int complexToDimensionPixelSize(int, android.util.DisplayMetrics);
    method public static float complexToFloat(int);
    method public static float complexToFraction(int, float, float);
    method public static float deriveDimension(int, float, @NonNull android.util.DisplayMetrics);
    method public int getComplexUnit();
    method public float getDimension(android.util.DisplayMetrics);
    method public final float getFloat();
+33 −15
Original line number Diff line number Diff line
@@ -65,21 +65,38 @@ public class FontScaleConverter {
        mToDpValues = toDp;
    }

    /**
     * Convert a dimension in "dp" back to "sp" using the lookup table.
     *
     * @hide
     */
    public float convertDpToSp(float dp) {
        return lookupAndInterpolate(dp, mToDpValues, mFromSpValues);
    }

    /**
     * Convert a dimension in "sp" to "dp" using the lookup table.
     *
     * @hide
     */
    public float convertSpToDp(float sp) {
        final float spPositive = Math.abs(sp);
        return lookupAndInterpolate(sp, mFromSpValues, mToDpValues);
    }

    private static float lookupAndInterpolate(
            float sourceValue,
            float[] sourceValues,
            float[] targetValues
    ) {
        final float sourceValuePositive = Math.abs(sourceValue);
        // TODO(b/247861374): find a match at a higher index?
        final float sign = Math.signum(sp);
        final float sign = Math.signum(sourceValue);
        // We search for exact matches only, even if it's just a little off. The interpolation will
        // handle any non-exact matches.
        final int index = Arrays.binarySearch(mFromSpValues, spPositive);
        final int index = Arrays.binarySearch(sourceValues, sourceValuePositive);
        if (index >= 0) {
            // exact match, return the matching dp
            return sign * mToDpValues[index];
            return sign * targetValues[index];
        } else {
            // must be a value in between index and index + 1: interpolate.
            final int lowerIndex = -(index + 1) - 1;
@@ -89,29 +106,30 @@ public class FontScaleConverter {
            final float startDp;
            final float endDp;

            if (lowerIndex >= mFromSpValues.length - 1) {
            if (lowerIndex >= sourceValues.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];
                startSp = sourceValues[sourceValues.length - 1];
                startDp = targetValues[sourceValues.length - 1];

                if (startSp == 0) return 0;

                final float scalingFactor = startDp / startSp;
                return sp * scalingFactor;
                return sourceValue * 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];
                endSp = sourceValues[0];
                endDp = targetValues[0];
            } else {
                startSp = mFromSpValues[lowerIndex];
                endSp = mFromSpValues[lowerIndex + 1];
                startDp = mToDpValues[lowerIndex];
                endDp = mToDpValues[lowerIndex + 1];
                startSp = sourceValues[lowerIndex];
                endSp = sourceValues[lowerIndex + 1];
                startDp = targetValues[lowerIndex];
                endDp = targetValues[lowerIndex + 1];
            }

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

+85 −17
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.annotation.AnyRes;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.content.pm.ActivityInfo.Config;

import java.lang.annotation.Retention;
@@ -101,6 +102,9 @@ public class TypedValue {
     *  defined below. */
    public static final int COMPLEX_UNIT_MASK = 0xf;

    private static final float INCHES_PER_PT = (1.0f / 72);
    private static final float INCHES_PER_MM = (1.0f / 25.4f);

    /** @hide **/
    @IntDef(prefix = "COMPLEX_UNIT_", value = {
            COMPLEX_UNIT_PX,
@@ -387,17 +391,20 @@ public class TypedValue {
     }

    /**
     * Converts an unpacked complex data value holding a dimension to its final floating 
     * point value. The two parameters <var>unit</var> and <var>value</var>
     * are as in {@link #TYPE_DIMENSION}.
     * Converts an unpacked complex data value holding a dimension to its final floating point pixel
     * value. The two parameters <var>unit</var> and <var>value</var> are as in {@link
     * #TYPE_DIMENSION}.
     *
     * <p>To convert the other way, e.g. from pixels to DP, use {@link #deriveDimension(int, float,
     * DisplayMetrics)}.
     *
     * @param unit The unit to convert from.
     * @param value The value to apply the unit to.
     * @param metrics Current display metrics to use in the conversion -- 
     *                supplies display density and scaling information.
     * 
     * @return The complex floating point value multiplied by the appropriate 
     * metrics depending on its unit. 
     * @return The equivalent pixel value—i.e. the complex floating point value multiplied by the
     * appropriate metrics depending on its unit—or zero if unit is not valid.
     */
    public static float applyDimension(@ComplexDimensionUnit int unit, float value,
                                       DisplayMetrics metrics)
@@ -417,14 +424,75 @@ public class TypedValue {
                    return value * metrics.scaledDensity;
                }
            case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
                return value * metrics.xdpi * INCHES_PER_PT;
            case COMPLEX_UNIT_IN:
                return value * metrics.xdpi;
            case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
                return value * metrics.xdpi * INCHES_PER_MM;
        }
        return 0;
    }


    /**
     * Converts a pixel value to the given dimension, e.g. PX to DP.
     *
     * <p>This is the inverse of {@link #applyDimension(int, float, DisplayMetrics)}
     *
     * @param unitToConvertTo The unit to convert to.
     * @param pixelValue The raw pixels value to convert from.
     * @param metrics Current display metrics to use in the conversion --
     *                supplies display density and scaling information.
     *
     * @return A dimension value equivalent to the given number of pixels
     * @throws IllegalArgumentException if unitToConvertTo is not valid.
     */
    public static float deriveDimension(
            @ComplexDimensionUnit int unitToConvertTo,
            float pixelValue,
            @NonNull DisplayMetrics metrics) {
        switch (unitToConvertTo) {
            case COMPLEX_UNIT_PX:
                return pixelValue;
            case COMPLEX_UNIT_DIP: {
                // Avoid divide-by-zero, and return 0 since that's what the inverse function will do
                if (metrics.density == 0) {
                    return 0;
                }
                return pixelValue / metrics.density;
            }
            case COMPLEX_UNIT_SP:
                if (metrics.fontScaleConverter != null) {
                    final float dpValue = deriveDimension(COMPLEX_UNIT_DIP, pixelValue, metrics);
                    return metrics.fontScaleConverter.convertDpToSp(dpValue);
                } else {
                    if (metrics.scaledDensity == 0) {
                        return 0;
                    }
                    return pixelValue / metrics.scaledDensity;
                }
            case COMPLEX_UNIT_PT: {
                if (metrics.xdpi == 0) {
                    return 0;
                }
                return pixelValue / metrics.xdpi / INCHES_PER_PT;
            }
            case COMPLEX_UNIT_IN: {
                if (metrics.xdpi == 0) {
                    return 0;
                }
                return pixelValue / metrics.xdpi;
            }
            case COMPLEX_UNIT_MM: {
                if (metrics.xdpi == 0) {
                    return 0;
                }
                return pixelValue / metrics.xdpi / INCHES_PER_MM;
            }
            default:
                throw new IllegalArgumentException("Invalid unitToConvertTo " + unitToConvertTo);
        }
    }

    /**
     * Return the data for this value as a dimension.  Only use for values 
+51 −35
Original line number Diff line number Diff line
@@ -17,7 +17,7 @@
package android.content.res

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Test
import org.junit.runner.RunWith

@@ -27,60 +27,60 @@ class FontScaleConverterTest {
    @Test
    fun straightInterpolation() {
        val table = createTable(8f to 8f, 10f to 10f, 20f to 20f)
        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1f)
        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f)
        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(10f)
        assertThat(table.convertSpToDp(30F)).isWithin(CONVERSION_TOLERANCE).of(30f)
        assertThat(table.convertSpToDp(20F)).isWithin(CONVERSION_TOLERANCE).of(20f)
        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f)
        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
        verifyConversionBothWays(table, 1f, 1F)
        verifyConversionBothWays(table, 8f, 8F)
        verifyConversionBothWays(table, 10f, 10F)
        verifyConversionBothWays(table, 30f, 30F)
        verifyConversionBothWays(table, 20f, 20F)
        verifyConversionBothWays(table, 5f, 5F)
        verifyConversionBothWays(table, 0f, 0F)
    }

    @Test
    fun interpolate200Percent() {
        val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
        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(30F)).isWithin(CONVERSION_TOLERANCE).of(60f)
        assertThat(table.convertSpToDp(20F)).isWithin(CONVERSION_TOLERANCE).of(40f)
        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f)
        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
        verifyConversionBothWays(table, 2f, 1F)
        verifyConversionBothWays(table, 16f, 8F)
        verifyConversionBothWays(table, 20f, 10F)
        verifyConversionBothWays(table, 60f, 30F)
        verifyConversionBothWays(table, 40f, 20F)
        verifyConversionBothWays(table, 10f, 5F)
        verifyConversionBothWays(table, 0f, 0F)
    }

    @Test
    fun interpolate150Percent() {
        val table = createTable(2f to 3f, 10f to 15f, 20f to 30f, 100f to 150f)
        assertThat(table.convertSpToDp(2F)).isWithin(CONVERSION_TOLERANCE).of(3f)
        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1.5f)
        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(12f)
        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(15f)
        assertThat(table.convertSpToDp(20F)).isWithin(CONVERSION_TOLERANCE).of(30f)
        assertThat(table.convertSpToDp(50F)).isWithin(CONVERSION_TOLERANCE).of(75f)
        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(7.5f)
        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
        verifyConversionBothWays(table, 3f, 2F)
        verifyConversionBothWays(table, 1.5f, 1F)
        verifyConversionBothWays(table, 12f, 8F)
        verifyConversionBothWays(table, 15f, 10F)
        verifyConversionBothWays(table, 30f, 20F)
        verifyConversionBothWays(table, 75f, 50F)
        verifyConversionBothWays(table, 7.5f, 5F)
        verifyConversionBothWays(table, 0f, 0F)
    }

    @Test
    fun pastEndsUsesLastScalingFactor() {
        val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
        assertThat(table.convertSpToDp(100F)).isWithin(CONVERSION_TOLERANCE).of(200f)
        assertThat(table.convertSpToDp(31F)).isWithin(CONVERSION_TOLERANCE).of(62f)
        assertThat(table.convertSpToDp(1000F)).isWithin(CONVERSION_TOLERANCE).of(2000f)
        assertThat(table.convertSpToDp(2000F)).isWithin(CONVERSION_TOLERANCE).of(4000f)
        assertThat(table.convertSpToDp(10000F)).isWithin(CONVERSION_TOLERANCE).of(20000f)
        verifyConversionBothWays(table, 200f, 100F)
        verifyConversionBothWays(table, 62f, 31F)
        verifyConversionBothWays(table, 2000f, 1000F)
        verifyConversionBothWays(table, 4000f, 2000F)
        verifyConversionBothWays(table, 20000f, 10000F)
    }

    @Test
    fun negativeSpIsNegativeDp() {
        val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
        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(-30F)).isWithin(CONVERSION_TOLERANCE).of(-60f)
        assertThat(table.convertSpToDp(-20F)).isWithin(CONVERSION_TOLERANCE).of(-40f)
        assertThat(table.convertSpToDp(-5F)).isWithin(CONVERSION_TOLERANCE).of(-10f)
        assertThat(table.convertSpToDp(-0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
        verifyConversionBothWays(table, -2f, -1F)
        verifyConversionBothWays(table, -16f, -8F)
        verifyConversionBothWays(table, -20f, -10F)
        verifyConversionBothWays(table, -60f, -30F)
        verifyConversionBothWays(table, -40f, -20F)
        verifyConversionBothWays(table, -10f, -5F)
        verifyConversionBothWays(table, 0f, -0F)
    }

    private fun createTable(vararg pairs: Pair<Float, Float>) =
@@ -89,6 +89,22 @@ class FontScaleConverterTest {
            pairs.map { it.second }.toFloatArray()
        )

    private fun verifyConversionBothWays(
        table: FontScaleConverter,
        expectedDp: Float,
        spToConvert: Float
    ) {
        assertWithMessage("convertSpToDp")
            .that(table.convertSpToDp(spToConvert))
            .isWithin(CONVERSION_TOLERANCE)
            .of(expectedDp)

        assertWithMessage("inverse: convertDpToSp")
            .that(table.convertDpToSp(expectedDp))
            .isWithin(CONVERSION_TOLERANCE)
            .of(spToConvert)
    }

    companion object {
        private const val CONVERSION_TOLERANCE = 0.05f
    }
+112 −3
Original line number Diff line number Diff line
@@ -16,17 +16,19 @@

package android.util

import android.content.res.FontScaleConverterFactory
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt

@RunWith(AndroidJUnit4::class)
class TypedValueTest {
@@ -160,6 +162,7 @@ class TypedValueTest {
        val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
        val fontScale = 2f
        metrics.density = 1f
        metrics.xdpi = 2f
        metrics.scaledDensity = fontScale * metrics.density
        metrics.fontScaleConverter = null

@@ -167,5 +170,111 @@ class TypedValueTest {
                .isEqualTo(20f)
        assertThat(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 50f, metrics))
                .isEqualTo(100f)
        assertThat(TypedValue.deriveDimension(TypedValue.COMPLEX_UNIT_SP, 20f, metrics))
                .isEqualTo(10f)
        assertThat(TypedValue.deriveDimension(TypedValue.COMPLEX_UNIT_SP, 100f, metrics))
                .isEqualTo(50f)
    }

    @LargeTest
    @Test
    fun testNonLinearFontScalingIsNull_deriveDimensionInversesApplyDimension() {
        val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
        val fontScale = 2f
        metrics.density = 1f
        metrics.xdpi = 2f
        metrics.scaledDensity = fontScale * metrics.density
        metrics.fontScaleConverter = null

        verifyRoundTripsForEachUnitType(metrics)
    }

    @LargeTest
    @Test
    fun testNonLinearFontScalingIs2_deriveDimensionInversesApplyDimension() {
        val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
        val fontScale = 2f
        metrics.density = 1f
        metrics.xdpi = 2f
        metrics.scaledDensity = fontScale * metrics.density
        metrics.fontScaleConverter = FontScaleConverterFactory.forScale(fontScale)

        verifyRoundTripsForEachUnitType(metrics)
    }

    @Test
    fun invalidUnitThrows() {
        val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
        val fontScale = 2f
        metrics.density = 1f
        metrics.xdpi = 2f
        metrics.scaledDensity = fontScale * metrics.density

        assertThrows(IllegalArgumentException::class.java) {
            TypedValue.deriveDimension(TypedValue.COMPLEX_UNIT_MM + 1, 23f, metrics)
        }
    }

    @Test
    fun density0_deriveDoesNotCrash() {
        val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
        metrics.density = 0f
        metrics.xdpi = 0f
        metrics.scaledDensity = 0f

        listOf(
            TypedValue.COMPLEX_UNIT_PX,
            TypedValue.COMPLEX_UNIT_DIP,
            TypedValue.COMPLEX_UNIT_SP,
            TypedValue.COMPLEX_UNIT_PT,
            TypedValue.COMPLEX_UNIT_IN,
            TypedValue.COMPLEX_UNIT_MM
        )
            .forEach { dimenType ->
                assertThat(TypedValue.deriveDimension(dimenType, 23f, metrics))
                    .isEqualTo(0)
            }
    }

    @Test
    fun scaledDensity0_deriveSpDoesNotCrash() {
        val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
        metrics.density = 1f
        metrics.xdpi = 2f
        metrics.scaledDensity = 0f

        assertThat(TypedValue.deriveDimension(TypedValue.COMPLEX_UNIT_SP, 23f, metrics))
            .isEqualTo(0)
    }

    private fun verifyRoundTripsForEachUnitType(metrics: DisplayMetrics) {
        listOf(
            TypedValue.COMPLEX_UNIT_PX,
            TypedValue.COMPLEX_UNIT_DIP,
            TypedValue.COMPLEX_UNIT_SP,
            TypedValue.COMPLEX_UNIT_PT,
            TypedValue.COMPLEX_UNIT_IN,
            TypedValue.COMPLEX_UNIT_MM
        )
            .forEach { dimenType ->
                // Test for every integer value in the range...
                for (i: Int in -(1 shl 23) until (1 shl 23)) {
                    assertRoundTripIsEqual(i.toFloat(), dimenType, metrics)
                    assertRoundTripIsEqual(i - .1f, dimenType, metrics)
                    assertRoundTripIsEqual(i + .5f, dimenType, metrics)
                }
            }
    }

    private fun assertRoundTripIsEqual(
        dimenValueToTest: Float,
        @TypedValue.ComplexDimensionUnit dimenType: Int,
        metrics: DisplayMetrics,
    ) {
        val actualPx = TypedValue.applyDimension(dimenType, dimenValueToTest, metrics)
        val actualDimenValue = TypedValue.deriveDimension(dimenType, actualPx, metrics)
        assertThat(dimenValueToTest)
            .isWithin(0.05f)
            .of(actualDimenValue)
    }
}