Loading core/api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -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(); core/java/android/content/res/FontScaleConverter.java +33 −15 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); } } Loading core/java/android/util/TypedValue.java +85 −17 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading Loading @@ -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) Loading @@ -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 Loading core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt +51 −35 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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>) = Loading @@ -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 } Loading core/tests/coretests/src/android/util/TypedValueTest.kt +112 −3 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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 Loading @@ -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) } } Loading
core/api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -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();
core/java/android/content/res/FontScaleConverter.java +33 −15 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); } } Loading
core/java/android/util/TypedValue.java +85 −17 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading Loading @@ -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) Loading @@ -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 Loading
core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt +51 −35 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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>) = Loading @@ -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 } Loading
core/tests/coretests/src/android/util/TypedValueTest.kt +112 −3 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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 Loading @@ -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) } }