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