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.util.MathUtils;
21 
22 import com.android.internal.annotations.VisibleForTesting;
23 
24 import java.util.Arrays;
25 
26 /**
27  * A lookup table for non-linear font scaling. Converts font sizes given in "sp" dimensions to a
28  * "dp" dimension according to a non-linear curve.
29  *
30  * <p>This is meant to improve readability at larger font scales: larger fonts will scale up more
31  * slowly than smaller fonts, so we don't get ridiculously huge fonts that don't fit on the screen.
32  *
33  * <p>The thinking here is that large fonts are already big enough to read, but we still want to
34  * scale them slightly to preserve the visual hierarchy when compared to smaller fonts.
35  *
36  * @hide
37  */
38 public class FontScaleConverter {
39 
40     @VisibleForTesting
41     final float[] mFromSpValues;
42     @VisibleForTesting
43     final float[] mToDpValues;
44 
45     /**
46      * Creates a lookup table for the given conversions.
47      *
48      * <p>Any "sp" value not in the lookup table will be derived via linear interpolation.
49      *
50      * <p>The arrays must be sorted ascending and monotonically increasing.
51      *
52      * @param fromSp array of dimensions in SP
53      * @param toDp array of dimensions in DP that correspond to an SP value in fromSp
54      *
55      * @throws IllegalArgumentException if the array lengths don't match or are empty
56      * @hide
57      */
58     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
FontScaleConverter(@onNull float[] fromSp, @NonNull float[] toDp)59     public FontScaleConverter(@NonNull float[] fromSp, @NonNull float[] toDp) {
60         if (fromSp.length != toDp.length || fromSp.length == 0) {
61             throw new IllegalArgumentException("Array lengths must match and be nonzero");
62         }
63 
64         mFromSpValues = fromSp;
65         mToDpValues = toDp;
66     }
67 
68     /**
69      * Convert a dimension in "dp" back to "sp" using the lookup table.
70      *
71      * @hide
72      */
convertDpToSp(float dp)73     public float convertDpToSp(float dp) {
74         return lookupAndInterpolate(dp, mToDpValues, mFromSpValues);
75     }
76 
77     /**
78      * Convert a dimension in "sp" to "dp" using the lookup table.
79      *
80      * @hide
81      */
convertSpToDp(float sp)82     public float convertSpToDp(float sp) {
83         return lookupAndInterpolate(sp, mFromSpValues, mToDpValues);
84     }
85 
lookupAndInterpolate( float sourceValue, float[] sourceValues, float[] targetValues )86     private static float lookupAndInterpolate(
87             float sourceValue,
88             float[] sourceValues,
89             float[] targetValues
90     ) {
91         final float sourceValuePositive = Math.abs(sourceValue);
92         // TODO(b/247861374): find a match at a higher index?
93         final float sign = Math.signum(sourceValue);
94         // We search for exact matches only, even if it's just a little off. The interpolation will
95         // handle any non-exact matches.
96         final int index = Arrays.binarySearch(sourceValues, sourceValuePositive);
97         if (index >= 0) {
98             // exact match, return the matching dp
99             return sign * targetValues[index];
100         } else {
101             // must be a value in between index and index + 1: interpolate.
102             final int lowerIndex = -(index + 1) - 1;
103 
104             final float startSp;
105             final float endSp;
106             final float startDp;
107             final float endDp;
108 
109             if (lowerIndex >= sourceValues.length - 1) {
110                 // It's past our lookup table. Determine the last elements' scaling factor and use.
111                 startSp = sourceValues[sourceValues.length - 1];
112                 startDp = targetValues[sourceValues.length - 1];
113 
114                 if (startSp == 0) return 0;
115 
116                 final float scalingFactor = startDp / startSp;
117                 return sourceValue * scalingFactor;
118             } else if (lowerIndex == -1) {
119                 // It's smaller than the smallest value in our table. Interpolate from 0.
120                 startSp = 0;
121                 startDp = 0;
122                 endSp = sourceValues[0];
123                 endDp = targetValues[0];
124             } else {
125                 startSp = sourceValues[lowerIndex];
126                 endSp = sourceValues[lowerIndex + 1];
127                 startDp = targetValues[lowerIndex];
128                 endDp = targetValues[lowerIndex + 1];
129             }
130 
131             return sign
132                     * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, sourceValuePositive);
133         }
134     }
135 
136     @Override
equals(Object o)137     public boolean equals(Object o) {
138         if (this == o) return true;
139         if (o == null) return false;
140         if (!(o instanceof FontScaleConverter)) return false;
141         FontScaleConverter that = (FontScaleConverter) o;
142         return Arrays.equals(mFromSpValues, that.mFromSpValues)
143                 && Arrays.equals(mToDpValues, that.mToDpValues);
144     }
145 
146     @Override
hashCode()147     public int hashCode() {
148         int result = Arrays.hashCode(mFromSpValues);
149         result = 31 * result + Arrays.hashCode(mToDpValues);
150         return result;
151     }
152 
153     @Override
toString()154     public String toString() {
155         return "FontScaleConverter{"
156                 + "fromSpValues="
157                 + Arrays.toString(mFromSpValues)
158                 + ", toDpValues="
159                 + Arrays.toString(mToDpValues)
160                 + '}';
161     }
162 }
163