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