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.AnyThread; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.ravenwood.annotation.RavenwoodKeepWholeClass; 23 import android.util.MathUtils; 24 import android.util.SparseArray; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 28 /** 29 * Creates {@link FontScaleConverter}s at various scales. 30 * 31 * Generally you shouldn't need this; you can use {@link 32 * android.util.TypedValue#applyDimension(int, float, DisplayMetrics)} directly and it will do the 33 * scaling conversion for you. But for UI frameworks or other situations where you need to do the 34 * conversion without an Android Context, you can use this class. 35 * 36 * @hide 37 */ 38 @RavenwoodKeepWholeClass 39 public class FontScaleConverterFactory { 40 private static final float SCALE_KEY_MULTIPLIER = 100f; 41 42 /** @hide */ 43 // GuardedBy("LOOKUP_TABLES_WRITE_LOCK") but only for writes! 44 @VisibleForTesting 45 @NonNull 46 public static volatile SparseArray<FontScaleConverter> sLookupTables = new SparseArray<>(); 47 48 /** 49 * This is a write lock only! We don't care about synchronization on reads; they can be a bit 50 * out of date. But all writes have to be atomic, so we use this similar to a 51 * CopyOnWriteArrayList. 52 */ 53 private static final Object LOOKUP_TABLES_WRITE_LOCK = new Object(); 54 55 private static float sMinScaleBeforeCurvesApplied = 1.05f; 56 57 static { 58 // These were generated by frameworks/base/tools/fonts/font-scaling-array-generator.js and 59 // manually tweaked for optimum readability. 60 synchronized (LOOKUP_TABLES_WRITE_LOCK) { putInto( sLookupTables, 1.05f, new FontScaleConverterImpl( new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, new float[] { 8.4f, 10.5f, 12.6f, 14.8f, 18.6f, 20.6f, 24.4f, 30f, 100}) )61 putInto( 62 sLookupTables, 63 /* scaleKey= */ 1.05f, 64 new FontScaleConverterImpl( 65 /* fromSp= */ 66 new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, 67 /* toDp= */ 68 new float[] { 8.4f, 10.5f, 12.6f, 14.8f, 18.6f, 20.6f, 24.4f, 30f, 100}) 69 ); 70 putInto( sLookupTables, 1.1f, new FontScaleConverterImpl( new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, new float[] { 8.8f, 11f, 13.2f, 15.6f, 19.2f, 21.2f, 24.8f, 30f, 100}) )71 putInto( 72 sLookupTables, 73 /* scaleKey= */ 1.1f, 74 new FontScaleConverterImpl( 75 /* fromSp= */ 76 new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, 77 /* toDp= */ 78 new float[] { 8.8f, 11f, 13.2f, 15.6f, 19.2f, 21.2f, 24.8f, 30f, 100}) 79 ); 80 putInto( sLookupTables, 1.15f, new FontScaleConverterImpl( new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, new float[] { 9.2f, 11.5f, 13.8f, 16.4f, 19.8f, 21.8f, 25.2f, 30f, 100}) )81 putInto( 82 sLookupTables, 83 /* scaleKey= */ 1.15f, 84 new FontScaleConverterImpl( 85 /* fromSp= */ 86 new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, 87 /* toDp= */ 88 new float[] { 9.2f, 11.5f, 13.8f, 16.4f, 19.8f, 21.8f, 25.2f, 30f, 100}) 89 ); 90 putInto( sLookupTables, 1.2f, new FontScaleConverterImpl( new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, new float[] { 9.6f, 12f, 14.4f, 17.2f, 20.4f, 22.4f, 25.6f, 30f, 100}) )91 putInto( 92 sLookupTables, 93 /* scaleKey= */ 1.2f, 94 new FontScaleConverterImpl( 95 /* fromSp= */ 96 new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, 97 /* toDp= */ 98 new float[] { 9.6f, 12f, 14.4f, 17.2f, 20.4f, 22.4f, 25.6f, 30f, 100}) 99 ); 100 putInto( sLookupTables, 1.3f, new FontScaleConverterImpl( new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, new float[] {10.4f, 13f, 15.6f, 18.8f, 21.6f, 23.6f, 26.4f, 30f, 100}) )101 putInto( 102 sLookupTables, 103 /* scaleKey= */ 1.3f, 104 new FontScaleConverterImpl( 105 /* fromSp= */ 106 new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, 107 /* toDp= */ 108 new float[] {10.4f, 13f, 15.6f, 18.8f, 21.6f, 23.6f, 26.4f, 30f, 100}) 109 ); 110 putInto( sLookupTables, 1.5f, new FontScaleConverterImpl( new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, new float[] { 12f, 15f, 18f, 22f, 24f, 26f, 28f, 30f, 100}) )111 putInto( 112 sLookupTables, 113 /* scaleKey= */ 1.5f, 114 new FontScaleConverterImpl( 115 /* fromSp= */ 116 new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, 117 /* toDp= */ 118 new float[] { 12f, 15f, 18f, 22f, 24f, 26f, 28f, 30f, 100}) 119 ); 120 putInto( sLookupTables, 1.8f, new FontScaleConverterImpl( new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, new float[] {14.4f, 18f, 21.6f, 24.4f, 27.6f, 30.8f, 32.8f, 34.8f, 100}) )121 putInto( 122 sLookupTables, 123 /* scaleKey= */ 1.8f, 124 new FontScaleConverterImpl( 125 /* fromSp= */ 126 new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, 127 /* toDp= */ 128 new float[] {14.4f, 18f, 21.6f, 24.4f, 27.6f, 30.8f, 32.8f, 34.8f, 100}) 129 ); 130 putInto( sLookupTables, 2f, new FontScaleConverterImpl( new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, new float[] { 16f, 20f, 24f, 26f, 30f, 34f, 36f, 38f, 100}) )131 putInto( 132 sLookupTables, 133 /* scaleKey= */ 2f, 134 new FontScaleConverterImpl( 135 /* fromSp= */ 136 new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, 137 /* toDp= */ 138 new float[] { 16f, 20f, 24f, 26f, 30f, 34f, 36f, 38f, 100}) 139 ); 140 } 141 142 sMinScaleBeforeCurvesApplied = getScaleFromKey(sLookupTables.keyAt(0)) - 0.01f; 143 if (sMinScaleBeforeCurvesApplied <= 1.0f) { 144 throw new IllegalStateException( 145 "You should only apply non-linear scaling to font scales > 1" 146 ); 147 } 148 } 149 FontScaleConverterFactory()150 private FontScaleConverterFactory() {} 151 152 /** 153 * Returns true if non-linear font scaling curves would be in effect for the given scale, false 154 * if the scaling would follow a linear curve or for no scaling. 155 * 156 * <p>Example usage: 157 * <code>isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)</code> 158 */ 159 @AnyThread isNonLinearFontScalingActive(float fontScale)160 public static boolean isNonLinearFontScalingActive(float fontScale) { 161 return fontScale >= sMinScaleBeforeCurvesApplied; 162 } 163 164 /** 165 * Finds a matching FontScaleConverter for the given fontScale factor. 166 * 167 * @param fontScale the scale factor, usually from {@link Configuration#fontScale}. 168 * 169 * @return a converter for the given scale, or null if non-linear scaling should not be used. 170 */ 171 @Nullable 172 @AnyThread forScale(float fontScale)173 public static FontScaleConverter forScale(float fontScale) { 174 if (!isNonLinearFontScalingActive(fontScale)) { 175 return null; 176 } 177 178 FontScaleConverter lookupTable = get(fontScale); 179 if (lookupTable != null) { 180 return lookupTable; 181 } 182 183 // Didn't find an exact match: interpolate between two existing tables 184 final int index = sLookupTables.indexOfKey(getKey(fontScale)); 185 if (index >= 0) { 186 // This should never happen, should have been covered by get() above. 187 return sLookupTables.valueAt(index); 188 } 189 // Didn't find an exact match: interpolate between two existing tables 190 final int lowerIndex = -(index + 1) - 1; 191 final int higherIndex = lowerIndex + 1; 192 if (lowerIndex < 0 || higherIndex >= sLookupTables.size()) { 193 // We have gone beyond our bounds and have nothing to interpolate between. Just give 194 // them a straight linear table instead. 195 // This works because when FontScaleConverter encounters a size beyond its bounds, it 196 // calculates a linear fontScale factor using the ratio of the last element pair. 197 FontScaleConverterImpl converter = new FontScaleConverterImpl( 198 new float[]{1f}, 199 new float[]{fontScale} 200 ); 201 202 if (Flags.fontScaleConverterPublic()) { 203 // Cache for next time. 204 put(fontScale, converter); 205 } 206 207 return converter; 208 } else { 209 float startScale = getScaleFromKey(sLookupTables.keyAt(lowerIndex)); 210 float endScale = getScaleFromKey(sLookupTables.keyAt(higherIndex)); 211 float interpolationPoint = MathUtils.constrainedMap( 212 /* rangeMin= */ 0f, 213 /* rangeMax= */ 1f, 214 startScale, 215 endScale, 216 fontScale 217 ); 218 FontScaleConverter converter = createInterpolatedTableBetween( 219 sLookupTables.valueAt(lowerIndex), 220 sLookupTables.valueAt(higherIndex), 221 interpolationPoint 222 ); 223 224 if (Flags.fontScaleConverterPublic()) { 225 // Cache for next time. 226 put(fontScale, converter); 227 } 228 229 return converter; 230 } 231 } 232 233 @NonNull createInterpolatedTableBetween( FontScaleConverter start, FontScaleConverter end, float interpolationPoint )234 private static FontScaleConverter createInterpolatedTableBetween( 235 FontScaleConverter start, 236 FontScaleConverter end, 237 float interpolationPoint 238 ) { 239 float[] commonSpSizes = new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100f}; 240 float[] dpInterpolated = new float[commonSpSizes.length]; 241 242 for (int i = 0; i < commonSpSizes.length; i++) { 243 float sp = commonSpSizes[i]; 244 float startDp = start.convertSpToDp(sp); 245 float endDp = end.convertSpToDp(sp); 246 dpInterpolated[i] = MathUtils.lerp(startDp, endDp, interpolationPoint); 247 } 248 249 return new FontScaleConverterImpl(commonSpSizes, dpInterpolated); 250 } 251 getKey(float fontScale)252 private static int getKey(float fontScale) { 253 return (int) (fontScale * SCALE_KEY_MULTIPLIER); 254 } 255 getScaleFromKey(int key)256 private static float getScaleFromKey(int key) { 257 return (float) key / SCALE_KEY_MULTIPLIER; 258 } 259 put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter)260 private static void put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter) { 261 // Dollar-store CopyOnWriteSparseArray, since this is the only write op we need. 262 synchronized (LOOKUP_TABLES_WRITE_LOCK) { 263 var newTable = sLookupTables.clone(); 264 putInto(newTable, scaleKey, fontScaleConverter); 265 sLookupTables = newTable; 266 } 267 } 268 putInto( SparseArray<FontScaleConverter> table, float scaleKey, @NonNull FontScaleConverter fontScaleConverter )269 private static void putInto( 270 SparseArray<FontScaleConverter> table, 271 float scaleKey, 272 @NonNull FontScaleConverter fontScaleConverter 273 ) { 274 table.put(getKey(scaleKey), fontScaleConverter); 275 } 276 277 @Nullable get(float scaleKey)278 private static FontScaleConverter get(float scaleKey) { 279 return sLookupTables.get(getKey(scaleKey)); 280 } 281 } 282