Merge "feat(non linear font scaling)!: add FontScaleConverter for non-linear font scaling."
This commit is contained in:
committed by
Android (Google) Code Review
commit
7f0fb8234f
@@ -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
core/java/android/content/res/FontScaleConverter.java
Normal file
148
core/java/android/content/res/FontScaleConverter.java
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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
core/java/android/content/res/FontScaleConverterFactory.java
Normal file
119
core/java/android/content/res/FontScaleConverterFactory.java
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
@@ -434,6 +434,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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -273,6 +274,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.
|
||||
*/
|
||||
@@ -350,6 +360,7 @@ public class DisplayMetrics {
|
||||
noncompatScaledDensity = o.noncompatScaledDensity;
|
||||
noncompatXdpi = o.noncompatXdpi;
|
||||
noncompatYdpi = o.noncompatYdpi;
|
||||
fontScaleConverter = o.fontScaleConverter;
|
||||
}
|
||||
|
||||
public void setToDefaults() {
|
||||
@@ -367,6 +378,7 @@ public class DisplayMetrics {
|
||||
noncompatScaledDensity = scaledDensity;
|
||||
noncompatXdpi = xdpi;
|
||||
noncompatYdpi = ydpi;
|
||||
fontScaleConverter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -408,7 +408,14 @@ public class TypedValue {
|
||||
case COMPLEX_UNIT_DIP:
|
||||
return value * metrics.density;
|
||||
case COMPLEX_UNIT_SP:
|
||||
return value * metrics.scaledDensity;
|
||||
if (metrics.fontScaleConverter != null) {
|
||||
return applyDimension(
|
||||
COMPLEX_UNIT_DIP,
|
||||
metrics.fontScaleConverter.convertSpToDp(value),
|
||||
metrics);
|
||||
} else {
|
||||
return value * metrics.scaledDensity;
|
||||
}
|
||||
case COMPLEX_UNIT_PT:
|
||||
return value * metrics.xdpi * (1.0f/72);
|
||||
case COMPLEX_UNIT_IN:
|
||||
|
||||
@@ -43,6 +43,7 @@ android_test {
|
||||
"mockwebserver",
|
||||
"guava",
|
||||
"androidx.core_core",
|
||||
"androidx.core_core-ktx",
|
||||
"androidx.test.espresso.core",
|
||||
"androidx.test.ext.junit",
|
||||
"androidx.test.runner",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 androidx.core.util.forEach
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class FontScaleConverterFactoryTest {
|
||||
|
||||
@Test
|
||||
fun scale200IsTwiceAtSmallSizes() {
|
||||
val table = FontScaleConverterFactory.forScale(2F)!!
|
||||
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)
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
fun missingLookupTableReturnsNull() {
|
||||
assertThat(FontScaleConverterFactory.forScale(3F)).isNull()
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
fun missingLookupTable105ReturnsNull() {
|
||||
assertThat(FontScaleConverterFactory.forScale(1.05F)).isNull()
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
fun missingLookupTableNegativeReturnsNull() {
|
||||
assertThat(FontScaleConverterFactory.forScale(-1F)).isNull()
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
fun unnecessaryFontScalesReturnsNull() {
|
||||
assertThat(FontScaleConverterFactory.forScale(0F)).isNull()
|
||||
assertThat(FontScaleConverterFactory.forScale(1F)).isNull()
|
||||
assertThat(FontScaleConverterFactory.forScale(0.85F)).isNull()
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
fun tablesMatchAndAreMonotonicallyIncreasing() {
|
||||
FontScaleConverterFactory.LOOKUP_TABLES.forEach { _, lookupTable ->
|
||||
assertThat(lookupTable.mToDpValues).hasLength(lookupTable.mFromSpValues.size)
|
||||
assertThat(lookupTable.mToDpValues).isNotEmpty()
|
||||
|
||||
assertThat(lookupTable.mFromSpValues.asList()).isInStrictOrder()
|
||||
assertThat(lookupTable.mToDpValues.asList()).isInStrictOrder()
|
||||
|
||||
assertThat(lookupTable.mFromSpValues.asList()).containsNoDuplicates()
|
||||
assertThat(lookupTable.mToDpValues.asList()).containsNoDuplicates()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONVERSION_TOLERANCE = 0.05f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
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)
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
private fun createTable(vararg pairs: Pair<Float, Float>) =
|
||||
FontScaleConverter(
|
||||
pairs.map { it.first }.toFloatArray(),
|
||||
pairs.map { it.second }.toFloatArray()
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val CONVERSION_TOLERANCE = 0.05f
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,17 @@
|
||||
|
||||
package android.util
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
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.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 {
|
||||
@@ -152,4 +153,19 @@ class TypedValueTest {
|
||||
val widthPx = TypedValue.complexToDimensionPixelSize(widthDimen, metrics)
|
||||
assertEquals(widthFloat.roundToInt(), widthPx)
|
||||
}
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
@Test
|
||||
fun testNonLinearFontScaling_nullLookupFallsBackToScaledDensity() {
|
||||
val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
|
||||
val fontScale = 2f
|
||||
metrics.density = 1f
|
||||
metrics.scaledDensity = fontScale * metrics.density
|
||||
metrics.fontScaleConverter = null
|
||||
|
||||
assertThat(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10f, metrics))
|
||||
.isEqualTo(20f)
|
||||
assertThat(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 50f, metrics))
|
||||
.isEqualTo(100f)
|
||||
}
|
||||
}
|
||||
|
||||
195
tools/fonts/font-scaling-array-generator.js
Normal file
195
tools/fonts/font-scaling-array-generator.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
Generates arrays for non-linear font scaling, to be pasted into
|
||||
frameworks/base/core/java/android/content/res/FontScaleConverterFactory.java
|
||||
|
||||
To use:
|
||||
`node font-scaling-array-generator.js`
|
||||
or just open a browser, open DevTools, and paste into the Console.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Modify this to match your packages/apps/Settings/res/arrays.xml#entryvalues_font_size
|
||||
* array so that all possible scales are generated.
|
||||
*/
|
||||
const scales = [1.15, 1.30, 1.5, 1.8, 2];
|
||||
|
||||
const commonSpSizes = [8, 10, 12, 14, 18, 20, 24, 30, 100];
|
||||
|
||||
/**
|
||||
* Enum for GENERATION_STYLE which determines how to generate the arrays.
|
||||
*/
|
||||
const GenerationStyle = {
|
||||
/**
|
||||
* Interpolates between hand-tweaked curves. This is the best option and
|
||||
* shouldn't require any additional tweaking.
|
||||
*/
|
||||
CUSTOM_TWEAKED: 'CUSTOM_TWEAKED',
|
||||
|
||||
/**
|
||||
* Uses a curve equation that is mostly correct, but will need manual tweaking
|
||||
* at some scales.
|
||||
*/
|
||||
CURVE: 'CURVE',
|
||||
|
||||
/**
|
||||
* Uses straight linear multiplication. Good starting point for manual
|
||||
* tweaking.
|
||||
*/
|
||||
LINEAR: 'LINEAR'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines how arrays are generated. Must be one of the GenerationStyle
|
||||
* values.
|
||||
*/
|
||||
const GENERATION_STYLE = GenerationStyle.CUSTOM_TWEAKED;
|
||||
|
||||
// These are hand-tweaked curves from which we will derive the other
|
||||
// interstitial curves using linear interpolation, in the case of using
|
||||
// GenerationStyle.CUSTOM_TWEAKED.
|
||||
const interpolationTargets = {
|
||||
1.0: commonSpSizes,
|
||||
1.5: [12, 15, 18, 22, 24, 26, 28, 30, 100],
|
||||
2.0: [16, 20, 24, 26, 30, 34, 36, 38, 100]
|
||||
};
|
||||
|
||||
/**
|
||||
* Interpolate a value with specified extrema, to a new value between new
|
||||
* extrema.
|
||||
*
|
||||
* @param value the current value
|
||||
* @param inputMin minimum the input value can reach
|
||||
* @param inputMax maximum the input value can reach
|
||||
* @param outputMin minimum the output value can reach
|
||||
* @param outputMax maximum the output value can reach
|
||||
*/
|
||||
function map(value, inputMin, inputMax, outputMin, outputMax) {
|
||||
return outputMin + (outputMax - outputMin) * ((value - inputMin) / (inputMax - inputMin));
|
||||
}
|
||||
|
||||
/***
|
||||
* Interpolate between values a and b.
|
||||
*/
|
||||
function lerp(a, b, fraction) {
|
||||
return (a * (1.0 - fraction)) + (b * fraction);
|
||||
}
|
||||
|
||||
function generateRatios(scale) {
|
||||
// Find the best two arrays to interpolate between.
|
||||
let startTarget, endTarget;
|
||||
let startTargetScale, endTargetScale;
|
||||
const targetScales = Object.keys(interpolationTargets).sort();
|
||||
for (let i = 0; i < targetScales.length - 1; i++) {
|
||||
const targetScaleKey = targetScales[i];
|
||||
const targetScale = parseFloat(targetScaleKey, 10);
|
||||
const startTargetScaleKey = targetScaleKey;
|
||||
const endTargetScaleKey = targetScales[i + 1];
|
||||
|
||||
if (scale < parseFloat(startTargetScaleKey, 10)) {
|
||||
break;
|
||||
}
|
||||
|
||||
startTargetScale = parseFloat(startTargetScaleKey, 10);
|
||||
endTargetScale = parseFloat(endTargetScaleKey, 10);
|
||||
startTarget = interpolationTargets[startTargetScaleKey];
|
||||
endTarget = interpolationTargets[endTargetScaleKey];
|
||||
}
|
||||
const interpolationProgress = map(scale, startTargetScale, endTargetScale, 0, 1);
|
||||
|
||||
return commonSpSizes.map((sp, i) => {
|
||||
const originalSizeDp = sp;
|
||||
let newSizeDp;
|
||||
switch (GENERATION_STYLE) {
|
||||
case GenerationStyle.CUSTOM_TWEAKED:
|
||||
newSizeDp = lerp(startTarget[i], endTarget[i], interpolationProgress);
|
||||
break;
|
||||
case GenerationStyle.CURVE: {
|
||||
let coeff1;
|
||||
let coeff2;
|
||||
if (scale < 1) {
|
||||
// \left(1.22^{-\left(x+5\right)}+0.5\right)\cdot x
|
||||
coeff1 = -5;
|
||||
coeff2 = scale;
|
||||
} else {
|
||||
// (1.22^{-\left(x-10\right)}+1\right)\cdot x
|
||||
coeff1 = map(scale, 1, 2, 2, 8);
|
||||
coeff2 = 1;
|
||||
}
|
||||
newSizeDp = ((Math.pow(1.22, (-(originalSizeDp - coeff1))) + coeff2) * originalSizeDp);
|
||||
break;
|
||||
}
|
||||
case GenerationStyle.LINEAR:
|
||||
newSizeDp = originalSizeDp * scale;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid GENERATION_STYLE');
|
||||
}
|
||||
return {
|
||||
fromSp: sp,
|
||||
toDp: newSizeDp
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const scaleArrays =
|
||||
scales
|
||||
.map(scale => {
|
||||
const scaleString = (scale * 100).toFixed(0);
|
||||
return {
|
||||
scale,
|
||||
name: `font_size_original_sp_to_scaled_dp_${scaleString}_percent`
|
||||
}
|
||||
})
|
||||
.map(scaleArray => {
|
||||
const items = generateRatios(scaleArray.scale);
|
||||
|
||||
return {
|
||||
...scaleArray,
|
||||
items
|
||||
}
|
||||
});
|
||||
|
||||
function formatDigit(d) {
|
||||
const twoSignificantDigits = Math.round(d * 100) / 100;
|
||||
return String(twoSignificantDigits).padStart(4, ' ');
|
||||
}
|
||||
|
||||
console.log(
|
||||
'' +
|
||||
scaleArrays.reduce(
|
||||
(previousScaleArray, currentScaleArray) => {
|
||||
const itemsFromSp = currentScaleArray.items.map(d => d.fromSp)
|
||||
.map(formatDigit)
|
||||
.join('f, ');
|
||||
const itemsToDp = currentScaleArray.items.map(d => d.toDp)
|
||||
.map(formatDigit)
|
||||
.join('f, ');
|
||||
|
||||
return previousScaleArray + `
|
||||
put(
|
||||
/* scaleKey= */ ${currentScaleArray.scale}f,
|
||||
new FontScaleConverter(
|
||||
/* fromSp= */
|
||||
new float[] {${itemsFromSp}},
|
||||
/* toDp= */
|
||||
new float[] {${itemsToDp}})
|
||||
);
|
||||
`;
|
||||
},
|
||||
''));
|
||||
Reference in New Issue
Block a user