diff options
| author | 2023-12-08 15:31:37 -0800 | |
|---|---|---|
| committer | 2023-12-08 15:31:37 -0800 | |
| commit | f23efb69cebaf3f51966fdfe7007d99dff2f0ab9 (patch) | |
| tree | 4a0157478d3357b73a4a9dcc3bee0fa215ba2c47 | |
| parent | 44366c963140081c76d02f15ff6d3f11ed5b3b03 (diff) | |
feat(non linear font scaling): make FontScaleConverter APIs public
This allows other UI frameworks (Compose, Flutter, etc) to use the
conversions directly without needing a Context or display density.
Moved some tests over to CTS.
Bug: 239736383
Flag: android.content.res.font_scale_converter_public
Test: atest FrameworksCoreTests:android.content.res.FontScaleConverterActivityTest
&& atest FrameworksCoreTests:android.content.res.FontScaleConverterTest
&& atest FrameworksCoreTests:android.content.res.FontScaleConverterFactoryTest
&& atest cts/tests/tests/content/src/android/content/res/cts/FontScaleConverterFactoryTest.kt
Change-Id: I824a9e7dbd8489bf316064eadd62c1c082a12de5
7 files changed, 219 insertions, 146 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index e8a6ac944076..4fd8cf045007 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -13578,6 +13578,16 @@ package android.content.res { field public int uiMode; } + @FlaggedApi("android.content.res.font_scale_converter_public") public interface FontScaleConverter { + method public float convertDpToSp(float); + method public float convertSpToDp(float); + } + + @FlaggedApi("android.content.res.font_scale_converter_public") public class FontScaleConverterFactory { + method @FlaggedApi("android.content.res.font_scale_converter_public") @Nullable public static android.content.res.FontScaleConverter forScale(float); + method @FlaggedApi("android.content.res.font_scale_converter_public") public static boolean isNonLinearFontScalingActive(float); + } + public class ObbInfo implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); diff --git a/core/java/android/content/res/FontScaleConverter.java b/core/java/android/content/res/FontScaleConverter.java index 939f85dc6c5f..088949e7eec2 100644 --- a/core/java/android/content/res/FontScaleConverter.java +++ b/core/java/android/content/res/FontScaleConverter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 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. @@ -16,15 +16,11 @@ package android.content.res; -import android.annotation.NonNull; -import android.util.MathUtils; -import com.android.internal.annotations.VisibleForTesting; - -import java.util.Arrays; +import android.annotation.FlaggedApi; /** - * A lookup table for non-linear font scaling. Converts font sizes given in "sp" dimensions to a + * A converter 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 @@ -32,134 +28,16 @@ import java.util.Arrays; * * <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 { - - /** @hide */ - @VisibleForTesting - public final float[] mFromSpValues; - - /** @hide */ - @VisibleForTesting - public 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; - } - +@FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) +public interface FontScaleConverter { /** - * Convert a dimension in "dp" back to "sp" using the lookup table. - * - * @hide + * Converts a dimension in "sp" to "dp". */ - public float convertDpToSp(float dp) { - return lookupAndInterpolate(dp, mToDpValues, mFromSpValues); - } + float convertSpToDp(float sp); /** - * Convert a dimension in "sp" to "dp" using the lookup table. - * - * @hide + * Converts a dimension in "dp" back to "sp". */ - public float convertSpToDp(float 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(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(sourceValues, sourceValuePositive); - if (index >= 0) { - // exact match, return the matching dp - return sign * targetValues[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 >= sourceValues.length - 1) { - // It's past our lookup table. Determine the last elements' scaling factor and use. - startSp = sourceValues[sourceValues.length - 1]; - startDp = targetValues[sourceValues.length - 1]; - - if (startSp == 0) return 0; - - final float scalingFactor = startDp / startSp; - 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 = sourceValues[0]; - endDp = targetValues[0]; - } else { - startSp = sourceValues[lowerIndex]; - endSp = sourceValues[lowerIndex + 1]; - startDp = targetValues[lowerIndex]; - endDp = targetValues[lowerIndex + 1]; - } - - return sign - * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, sourceValuePositive); - } - } - - @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) - + '}'; - } + float convertDpToSp(float dp); } diff --git a/core/java/android/content/res/FontScaleConverterFactory.java b/core/java/android/content/res/FontScaleConverterFactory.java index 5f90faf6c72f..df1cfe3aa9e6 100644 --- a/core/java/android/content/res/FontScaleConverterFactory.java +++ b/core/java/android/content/res/FontScaleConverterFactory.java @@ -16,6 +16,7 @@ package android.content.res; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.util.MathUtils; @@ -24,10 +25,14 @@ import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; /** - * Stores lookup tables for creating {@link FontScaleConverter}s at various scales. + * Creates {@link FontScaleConverter}s at various scales. * - * @hide + * Generally you shouldn't need this; you can use {@link + * android.util.TypedValue#applyDimension(int, float, DisplayMetrics)} directly and it will do the + * scaling conversion for you. But for UI frameworks or other situations where you need to do the + * conversion without an Android Context, you can use this class. */ +@FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) public class FontScaleConverterFactory { private static final float SCALE_KEY_MULTIPLIER = 100f; @@ -42,7 +47,7 @@ public class FontScaleConverterFactory { // manually tweaked for optimum readability. put( /* scaleKey= */ 1.15f, - new FontScaleConverter( + new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ @@ -51,7 +56,7 @@ public class FontScaleConverterFactory { put( /* scaleKey= */ 1.3f, - new FontScaleConverter( + new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ @@ -60,7 +65,7 @@ public class FontScaleConverterFactory { put( /* scaleKey= */ 1.5f, - new FontScaleConverter( + new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ @@ -69,7 +74,7 @@ public class FontScaleConverterFactory { put( /* scaleKey= */ 1.8f, - new FontScaleConverter( + new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ @@ -78,7 +83,7 @@ public class FontScaleConverterFactory { put( /* scaleKey= */ 2f, - new FontScaleConverter( + new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ @@ -101,9 +106,8 @@ public class FontScaleConverterFactory { * * <p>Example usage: * <code>isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)</code> - * - * @hide */ + @FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) public static boolean isNonLinearFontScalingActive(float fontScale) { return fontScale >= sMinScaleBeforeCurvesApplied; } @@ -114,9 +118,8 @@ public class FontScaleConverterFactory { * @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 */ + @FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) @Nullable public static FontScaleConverter forScale(float fontScale) { if (!isNonLinearFontScalingActive(fontScale)) { @@ -142,7 +145,7 @@ public class FontScaleConverterFactory { // 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}); + return new FontScaleConverterImpl(new float[] {1f}, new float[] {fontScale}); } else { float startScale = getScaleFromKey(LOOKUP_TABLES.keyAt(lowerIndex)); float endScale = getScaleFromKey(LOOKUP_TABLES.keyAt(higherIndex)); @@ -176,7 +179,7 @@ public class FontScaleConverterFactory { dpInterpolated[i] = MathUtils.lerp(startDp, endDp, interpolationPoint); } - return new FontScaleConverter(commonSpSizes, dpInterpolated); + return new FontScaleConverterImpl(commonSpSizes, dpInterpolated); } private static int getKey(float fontScale) { diff --git a/core/java/android/content/res/FontScaleConverterImpl.java b/core/java/android/content/res/FontScaleConverterImpl.java new file mode 100644 index 000000000000..1968c4e53109 --- /dev/null +++ b/core/java/android/content/res/FontScaleConverterImpl.java @@ -0,0 +1,164 @@ +/* + * 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 by interpolating values in a lookup table. + * + * {@see FontScaleConverter} + * + * @hide + */ +// Needs to be public so the Kotlin test can see it +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public class FontScaleConverterImpl implements FontScaleConverter { + + /** @hide */ + @VisibleForTesting + public final float[] mFromSpValues; + /** @hide */ + @VisibleForTesting + public 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 FontScaleConverterImpl(@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 "dp" back to "sp" using the lookup table. + * + * @hide + */ + @Override + public float convertDpToSp(float dp) { + return lookupAndInterpolate(dp, mToDpValues, mFromSpValues); + } + + /** + * Convert a dimension in "sp" to "dp" using the lookup table. + * + * @hide + */ + @Override + public float convertSpToDp(float 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(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(sourceValues, sourceValuePositive); + if (index >= 0) { + // exact match, return the matching dp + return sign * targetValues[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 >= sourceValues.length - 1) { + // It's past our lookup table. Determine the last elements' scaling factor and use. + startSp = sourceValues[sourceValues.length - 1]; + startDp = targetValues[sourceValues.length - 1]; + + if (startSp == 0) return 0; + + final float scalingFactor = startDp / startSp; + 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 = sourceValues[0]; + endDp = targetValues[0]; + } else { + startSp = sourceValues[lowerIndex]; + endSp = sourceValues[lowerIndex + 1]; + startDp = targetValues[lowerIndex]; + endDp = targetValues[lowerIndex + 1]; + } + + return sign + * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, sourceValuePositive); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if (!(o instanceof FontScaleConverterImpl)) return false; + FontScaleConverterImpl that = (FontScaleConverterImpl) 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) + + '}'; + } +} diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index 1b8eb0748737..c599adaa4f30 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -10,6 +10,15 @@ flag { } flag { + name: "font_scale_converter_public" + namespace: "accessibility" + description: "Enables the public API for FontScaleConverter, including enabling thread-safe caching." + bug: "239736383" + # fixed_read_only or device wont boot because of permission issues accessing flags during boot + is_fixed_read_only: true +} + +flag { name: "asset_file_descriptor_frro" namespace: "resource_manager" description: "Feature flag for passing in an AssetFileDescriptor to create an frro" diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index 8079dad7615e..9976fc25c806 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -28,8 +28,13 @@ import kotlin.math.ceil import kotlin.math.floor import org.junit.Test import org.junit.runner.RunWith +import java.lang.IllegalStateException import kotlin.random.Random.Default.nextFloat +/** + * Unit tests for FontScaleConverterFactory. Note that some similar tests are in + * cts/tests/tests/content/src/android/content/res/cts/FontScaleConverterFactoryTest.kt + */ @Presubmit @RunWith(AndroidJUnit4::class) class FontScaleConverterFactoryTest { @@ -102,6 +107,10 @@ class FontScaleConverterFactoryTest { @Test fun tablesMatchAndAreMonotonicallyIncreasing() { FontScaleConverterFactory.LOOKUP_TABLES.forEach { _, lookupTable -> + if (lookupTable !is FontScaleConverterImpl) { + throw IllegalStateException("Didn't return a FontScaleConverterImpl") + } + assertThat(lookupTable.mToDpValues).hasLength(lookupTable.mFromSpValues.size) assertThat(lookupTable.mToDpValues).isNotEmpty() diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt index bfa8c9ada911..2c614424a9a5 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterTest.kt @@ -86,13 +86,13 @@ class FontScaleConverterTest { } private fun createTable(vararg pairs: Pair<Float, Float>) = - FontScaleConverter( + FontScaleConverterImpl( pairs.map { it.first }.toFloatArray(), pairs.map { it.second }.toFloatArray() ) private fun verifyConversionBothWays( - table: FontScaleConverter, + table: FontScaleConverterImpl, expectedDp: Float, spToConvert: Float ) { |