1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.content.res;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.util.MathUtils;
22 import android.util.SparseArray;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 /**
27  * Stores lookup tables for creating {@link FontScaleConverter}s at various scales.
28  *
29  * @hide
30  */
31 public class FontScaleConverterFactory {
32     private static final float SCALE_KEY_MULTIPLIER = 100f;
33 
34     @VisibleForTesting
35     static final SparseArray<FontScaleConverter> LOOKUP_TABLES = new SparseArray<>();
36 
37     private static float sMinScaleBeforeCurvesApplied = 1.05f;
38 
39     static {
40         // These were generated by frameworks/base/tools/fonts/font-scaling-array-generator.js and
41         // manually tweaked for optimum readability.
42         put(
43                 /* scaleKey= */ 1.15f,
44                 new FontScaleConverter(
45                         /* fromSp= */
46                         new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
47                         /* toDp=   */
48                         new float[] { 9.2f, 11.5f, 13.8f, 16.4f, 19.8f, 21.8f, 25.2f,   30f,  100})
49         );
50 
51         put(
52                 /* scaleKey= */ 1.3f,
53                 new FontScaleConverter(
54                         /* fromSp= */
55                         new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
56                         /* toDp=   */
57                         new float[] {10.4f,   13f, 15.6f, 18.8f, 21.6f, 23.6f, 26.4f,   30f,  100})
58         );
59 
60         put(
61                 /* scaleKey= */ 1.5f,
62                 new FontScaleConverter(
63                         /* fromSp= */
64                         new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
65                         /* toDp=   */
66                         new float[] {  12f,   15f,   18f,   22f,   24f,   26f,   28f,   30f,  100})
67         );
68 
69         put(
70                 /* scaleKey= */ 1.8f,
71                 new FontScaleConverter(
72                         /* fromSp= */
73                         new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
74                         /* toDp=   */
75                         new float[] {14.4f,   18f, 21.6f, 24.4f, 27.6f, 30.8f, 32.8f, 34.8f,  100})
76         );
77 
78         put(
79                 /* scaleKey= */ 2f,
80                 new FontScaleConverter(
81                         /* fromSp= */
82                         new float[] {   8f,   10f,   12f,   14f,   18f,   20f,   24f,   30f,  100},
83                         /* toDp=   */
84                         new float[] {  16f,   20f,   24f,   26f,   30f,   34f,   36f,   38f,  100})
85         );
86 
87         sMinScaleBeforeCurvesApplied = getScaleFromKey(LOOKUP_TABLES.keyAt(0)) - 0.02f;
88         if (sMinScaleBeforeCurvesApplied <= 1.0f) {
89             throw new IllegalStateException(
90                     "You should only apply non-linear scaling to font scales > 1"
91             );
92         }
93     }
94 
FontScaleConverterFactory()95     private FontScaleConverterFactory() {}
96 
97     /**
98      * Returns true if non-linear font scaling curves would be in effect for the given scale, false
99      * if the scaling would follow a linear curve or for no scaling.
100      *
101      * <p>Example usage:
102      * <code>isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)</code>
103      *
104      * @hide
105      */
isNonLinearFontScalingActive(float fontScale)106     public static boolean isNonLinearFontScalingActive(float fontScale) {
107         return fontScale >= sMinScaleBeforeCurvesApplied;
108     }
109 
110     /**
111      * Finds a matching FontScaleConverter for the given fontScale factor.
112      *
113      * @param fontScale the scale factor, usually from {@link Configuration#fontScale}.
114      *
115      * @return a converter for the given scale, or null if non-linear scaling should not be used.
116      *
117      * @hide
118      */
119     @Nullable
forScale(float fontScale)120     public static FontScaleConverter forScale(float fontScale) {
121         if (!isNonLinearFontScalingActive(fontScale)) {
122             return null;
123         }
124 
125         FontScaleConverter lookupTable = get(fontScale);
126         if (lookupTable != null) {
127             return lookupTable;
128         }
129 
130         // Didn't find an exact match: interpolate between two existing tables
131         final int index = LOOKUP_TABLES.indexOfKey(getKey(fontScale));
132         if (index >= 0) {
133             // This should never happen, should have been covered by get() above.
134             return LOOKUP_TABLES.valueAt(index);
135         }
136         // Didn't find an exact match: interpolate between two existing tables
137         final int lowerIndex = -(index + 1) - 1;
138         final int higherIndex = lowerIndex + 1;
139         if (lowerIndex < 0 || higherIndex >= LOOKUP_TABLES.size()) {
140             // We have gone beyond our bounds and have nothing to interpolate between. Just give
141             // them a straight linear table instead.
142             // This works because when FontScaleConverter encounters a size beyond its bounds, it
143             // calculates a linear fontScale factor using the ratio of the last element pair.
144             return new FontScaleConverter(new float[] {1f}, new float[] {fontScale});
145         } else {
146             float startScale = getScaleFromKey(LOOKUP_TABLES.keyAt(lowerIndex));
147             float endScale = getScaleFromKey(LOOKUP_TABLES.keyAt(higherIndex));
148             float interpolationPoint = MathUtils.constrainedMap(
149                     /* rangeMin= */ 0f,
150                     /* rangeMax= */ 1f,
151                     startScale,
152                     endScale,
153                     fontScale
154             );
155             return createInterpolatedTableBetween(
156                     LOOKUP_TABLES.valueAt(lowerIndex),
157                     LOOKUP_TABLES.valueAt(higherIndex),
158                     interpolationPoint);
159         }
160     }
161 
162     @NonNull
createInterpolatedTableBetween( FontScaleConverter start, FontScaleConverter end, float interpolationPoint )163     private static FontScaleConverter createInterpolatedTableBetween(
164             FontScaleConverter start,
165             FontScaleConverter end,
166             float interpolationPoint
167     ) {
168         float[] commonSpSizes = new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100f};
169         float[] dpInterpolated = new float[commonSpSizes.length];
170 
171         for (int i = 0; i < commonSpSizes.length; i++) {
172             float sp = commonSpSizes[i];
173             float startDp = start.convertSpToDp(sp);
174             float endDp = end.convertSpToDp(sp);
175             dpInterpolated[i] = MathUtils.lerp(startDp, endDp, interpolationPoint);
176         }
177 
178         return new FontScaleConverter(commonSpSizes, dpInterpolated);
179     }
180 
getKey(float fontScale)181     private static int getKey(float fontScale) {
182         return (int) (fontScale * SCALE_KEY_MULTIPLIER);
183     }
184 
getScaleFromKey(int key)185     private static float getScaleFromKey(int key) {
186         return (float) key / SCALE_KEY_MULTIPLIER;
187     }
188 
put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter)189     private static void put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter) {
190         LOOKUP_TABLES.put(getKey(scaleKey), fontScaleConverter);
191     }
192 
193     @Nullable
get(float scaleKey)194     private static FontScaleConverter get(float scaleKey) {
195         return LOOKUP_TABLES.get(getKey(scaleKey));
196     }
197 }
198