/* * 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.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.util.MathUtils; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; /** * Creates {@link FontScaleConverter}s at various scales. * * 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. * * @hide */ public class FontScaleConverterFactory { private static final float SCALE_KEY_MULTIPLIER = 100f; /** @hide */ // GuardedBy("LOOKUP_TABLES_WRITE_LOCK") but only for writes! @VisibleForTesting @NonNull public static volatile SparseArray sLookupTables = new SparseArray<>(); /** * This is a write lock only! We don't care about synchronization on reads; they can be a bit * out of date. But all writes have to be atomic, so we use this similar to a * CopyOnWriteArrayList. */ private static final Object LOOKUP_TABLES_WRITE_LOCK = new Object(); private static float sMinScaleBeforeCurvesApplied = 1.05f; static { // These were generated by frameworks/base/tools/fonts/font-scaling-array-generator.js and // manually tweaked for optimum readability. synchronized (LOOKUP_TABLES_WRITE_LOCK) { putInto( sLookupTables, /* scaleKey= */ 1.05f, new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ new float[] { 8.4f, 10.5f, 12.6f, 14.8f, 18.6f, 20.6f, 24.4f, 30f, 100}) ); putInto( sLookupTables, /* scaleKey= */ 1.1f, new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ new float[] { 8.8f, 11f, 13.2f, 15.6f, 19.2f, 21.2f, 24.8f, 30f, 100}) ); putInto( sLookupTables, /* scaleKey= */ 1.15f, new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ new float[] { 9.2f, 11.5f, 13.8f, 16.4f, 19.8f, 21.8f, 25.2f, 30f, 100}) ); putInto( sLookupTables, /* scaleKey= */ 1.2f, new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ new float[] { 9.6f, 12f, 14.4f, 17.2f, 20.4f, 22.4f, 25.6f, 30f, 100}) ); putInto( sLookupTables, /* scaleKey= */ 1.3f, new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ new float[] {10.4f, 13f, 15.6f, 18.8f, 21.6f, 23.6f, 26.4f, 30f, 100}) ); putInto( sLookupTables, /* scaleKey= */ 1.5f, new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ new float[] { 12f, 15f, 18f, 22f, 24f, 26f, 28f, 30f, 100}) ); putInto( sLookupTables, /* scaleKey= */ 1.8f, new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ new float[] {14.4f, 18f, 21.6f, 24.4f, 27.6f, 30.8f, 32.8f, 34.8f, 100}) ); putInto( sLookupTables, /* scaleKey= */ 2f, new FontScaleConverterImpl( /* fromSp= */ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, /* toDp= */ new float[] { 16f, 20f, 24f, 26f, 30f, 34f, 36f, 38f, 100}) ); } sMinScaleBeforeCurvesApplied = getScaleFromKey(sLookupTables.keyAt(0)) - 0.01f; if (sMinScaleBeforeCurvesApplied <= 1.0f) { throw new IllegalStateException( "You should only apply non-linear scaling to font scales > 1" ); } } private FontScaleConverterFactory() {} /** * Returns true if non-linear font scaling curves would be in effect for the given scale, false * if the scaling would follow a linear curve or for no scaling. * *

Example usage: * isNonLinearFontScalingActive(getResources().getConfiguration().fontScale) */ @AnyThread public static boolean isNonLinearFontScalingActive(float fontScale) { return fontScale >= sMinScaleBeforeCurvesApplied; } /** * 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. */ @Nullable @AnyThread public static FontScaleConverter forScale(float fontScale) { if (!isNonLinearFontScalingActive(fontScale)) { return null; } FontScaleConverter lookupTable = get(fontScale); if (lookupTable != null) { return lookupTable; } // Didn't find an exact match: interpolate between two existing tables final int index = sLookupTables.indexOfKey(getKey(fontScale)); if (index >= 0) { // This should never happen, should have been covered by get() above. return sLookupTables.valueAt(index); } // Didn't find an exact match: interpolate between two existing tables final int lowerIndex = -(index + 1) - 1; final int higherIndex = lowerIndex + 1; if (lowerIndex < 0 || higherIndex >= sLookupTables.size()) { // We have gone beyond our bounds and have nothing to interpolate between. Just give // 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. FontScaleConverterImpl converter = new FontScaleConverterImpl( new float[]{1f}, new float[]{fontScale} ); if (Flags.fontScaleConverterPublic()) { // Cache for next time. put(fontScale, converter); } return converter; } else { float startScale = getScaleFromKey(sLookupTables.keyAt(lowerIndex)); float endScale = getScaleFromKey(sLookupTables.keyAt(higherIndex)); float interpolationPoint = MathUtils.constrainedMap( /* rangeMin= */ 0f, /* rangeMax= */ 1f, startScale, endScale, fontScale ); FontScaleConverter converter = createInterpolatedTableBetween( sLookupTables.valueAt(lowerIndex), sLookupTables.valueAt(higherIndex), interpolationPoint ); if (Flags.fontScaleConverterPublic()) { // Cache for next time. put(fontScale, converter); } return converter; } } @NonNull private static FontScaleConverter createInterpolatedTableBetween( FontScaleConverter start, FontScaleConverter end, float interpolationPoint ) { float[] commonSpSizes = new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100f}; float[] dpInterpolated = new float[commonSpSizes.length]; for (int i = 0; i < commonSpSizes.length; i++) { float sp = commonSpSizes[i]; float startDp = start.convertSpToDp(sp); float endDp = end.convertSpToDp(sp); dpInterpolated[i] = MathUtils.lerp(startDp, endDp, interpolationPoint); } return new FontScaleConverterImpl(commonSpSizes, dpInterpolated); } private static int getKey(float fontScale) { return (int) (fontScale * SCALE_KEY_MULTIPLIER); } private static float getScaleFromKey(int key) { return (float) key / SCALE_KEY_MULTIPLIER; } private static void put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter) { // Dollar-store CopyOnWriteSparseArray, since this is the only write op we need. synchronized (LOOKUP_TABLES_WRITE_LOCK) { var newTable = sLookupTables.clone(); putInto(newTable, scaleKey, fontScaleConverter); sLookupTables = newTable; } } private static void putInto( SparseArray table, float scaleKey, @NonNull FontScaleConverter fontScaleConverter ) { table.put(getKey(scaleKey), fontScaleConverter); } @Nullable private static FontScaleConverter get(float scaleKey) { return sLookupTables.get(getKey(scaleKey)); } }