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 by interpolating values in a lookup table.
29  *
30  * {@see FontScaleConverter}
31  *
32  * @hide
33  */
34 // Needs to be public so the Kotlin test can see it
35 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
36 public class FontScaleConverterImpl implements FontScaleConverter {
37 
38     /** @hide */
39     @VisibleForTesting
40     public final float[] mFromSpValues;
41     /** @hide */
42     @VisibleForTesting
43     public 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)
FontScaleConverterImpl(@onNull float[] fromSp, @NonNull float[] toDp)59     public FontScaleConverterImpl(@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      */
73     @Override
convertDpToSp(float dp)74     public float convertDpToSp(float dp) {
75         return lookupAndInterpolate(dp, mToDpValues, mFromSpValues);
76     }
77 
78     /**
79      * Convert a dimension in "sp" to "dp" using the lookup table.
80      *
81      * @hide
82      */
83     @Override
convertSpToDp(float sp)84     public float convertSpToDp(float sp) {
85         return lookupAndInterpolate(sp, mFromSpValues, mToDpValues);
86     }
87 
lookupAndInterpolate( float sourceValue, float[] sourceValues, float[] targetValues )88     private static float lookupAndInterpolate(
89             float sourceValue,
90             float[] sourceValues,
91             float[] targetValues
92     ) {
93         final float sourceValuePositive = Math.abs(sourceValue);
94         // TODO(b/247861374): find a match at a higher index?
95         final float sign = Math.signum(sourceValue);
96         // We search for exact matches only, even if it's just a little off. The interpolation will
97         // handle any non-exact matches.
98         final int index = Arrays.binarySearch(sourceValues, sourceValuePositive);
99         if (index >= 0) {
100             // exact match, return the matching dp
101             return sign * targetValues[index];
102         } else {
103             // must be a value in between index and index + 1: interpolate.
104             final int lowerIndex = -(index + 1) - 1;
105 
106             final float startSp;
107             final float endSp;
108             final float startDp;
109             final float endDp;
110 
111             if (lowerIndex >= sourceValues.length - 1) {
112                 // It's past our lookup table. Determine the last elements' scaling factor and use.
113                 startSp = sourceValues[sourceValues.length - 1];
114                 startDp = targetValues[sourceValues.length - 1];
115 
116                 if (startSp == 0) return 0;
117 
118                 final float scalingFactor = startDp / startSp;
119                 return sourceValue * scalingFactor;
120             } else if (lowerIndex == -1) {
121                 // It's smaller than the smallest value in our table. Interpolate from 0.
122                 startSp = 0;
123                 startDp = 0;
124                 endSp = sourceValues[0];
125                 endDp = targetValues[0];
126             } else {
127                 startSp = sourceValues[lowerIndex];
128                 endSp = sourceValues[lowerIndex + 1];
129                 startDp = targetValues[lowerIndex];
130                 endDp = targetValues[lowerIndex + 1];
131             }
132 
133             return sign
134                     * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, sourceValuePositive);
135         }
136     }
137 
138     @Override
equals(Object o)139     public boolean equals(Object o) {
140         if (this == o) return true;
141         if (o == null) return false;
142         if (!(o instanceof FontScaleConverterImpl)) return false;
143         FontScaleConverterImpl that = (FontScaleConverterImpl) o;
144         return Arrays.equals(mFromSpValues, that.mFromSpValues)
145                 && Arrays.equals(mToDpValues, that.mToDpValues);
146     }
147 
148     @Override
hashCode()149     public int hashCode() {
150         int result = Arrays.hashCode(mFromSpValues);
151         result = 31 * result + Arrays.hashCode(mToDpValues);
152         return result;
153     }
154 
155     @Override
toString()156     public String toString() {
157         return "FontScaleConverter{"
158                 + "fromSpValues="
159                 + Arrays.toString(mFromSpValues)
160                 + ", toDpValues="
161                 + Arrays.toString(mToDpValues)
162                 + '}';
163     }
164 }
165