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