1 /* 2 * Copyright 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 androidx.wear.protolayout.material; 18 19 import static android.os.Build.VERSION.SDK_INT; 20 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 21 22 import static androidx.annotation.Dimension.DP; 23 import static androidx.annotation.Dimension.SP; 24 import static androidx.wear.protolayout.DimensionBuilders.sp; 25 import static androidx.wear.protolayout.LayoutElementBuilders.FONT_VARIANT_BODY; 26 import static androidx.wear.protolayout.LayoutElementBuilders.FONT_VARIANT_TITLE; 27 import static androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_BOLD; 28 import static androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_MEDIUM; 29 import static androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_NORMAL; 30 import static androidx.wear.protolayout.materialcore.Helper.checkNotNull; 31 32 import android.annotation.SuppressLint; 33 import android.content.Context; 34 35 import androidx.annotation.Dimension; 36 import androidx.annotation.IntDef; 37 import androidx.annotation.RestrictTo; 38 import androidx.annotation.RestrictTo.Scope; 39 import androidx.wear.protolayout.DimensionBuilders; 40 import androidx.wear.protolayout.DimensionBuilders.SpProp; 41 import androidx.wear.protolayout.LayoutElementBuilders.FontStyle; 42 import androidx.wear.protolayout.LayoutElementBuilders.FontVariant; 43 import androidx.wear.protolayout.LayoutElementBuilders.FontWeight; 44 import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverter; 45 import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverterFactory; 46 47 import org.jspecify.annotations.NonNull; 48 49 import java.lang.annotation.Retention; 50 import java.lang.annotation.RetentionPolicy; 51 import java.util.HashMap; 52 import java.util.Map; 53 54 /** Typography styles, currently set up to match Wear's styling. */ 55 public class Typography { 56 /** Typography for large display text. */ 57 public static final int TYPOGRAPHY_DISPLAY1 = 1; 58 59 /** Typography for medium display text. */ 60 public static final int TYPOGRAPHY_DISPLAY2 = 2; 61 62 /** Typography for small display text. */ 63 public static final int TYPOGRAPHY_DISPLAY3 = 3; 64 65 /** Typography for large title text. */ 66 public static final int TYPOGRAPHY_TITLE1 = 4; 67 68 /** Typography for medium title text. */ 69 public static final int TYPOGRAPHY_TITLE2 = 5; 70 71 /** Typography for small title text. */ 72 public static final int TYPOGRAPHY_TITLE3 = 6; 73 74 /** Typography for large body text. */ 75 public static final int TYPOGRAPHY_BODY1 = 7; 76 77 /** Typography for medium body text. */ 78 public static final int TYPOGRAPHY_BODY2 = 8; 79 80 /** Typography for bold button text. */ 81 public static final int TYPOGRAPHY_BUTTON = 9; 82 83 /** Typography for large caption text. */ 84 public static final int TYPOGRAPHY_CAPTION1 = 10; 85 86 /** Typography for medium caption text. */ 87 public static final int TYPOGRAPHY_CAPTION2 = 11; 88 89 /** Typography for small caption text. */ 90 public static final int TYPOGRAPHY_CAPTION3 = 12; 91 92 @RestrictTo(Scope.LIBRARY) 93 @Retention(RetentionPolicy.SOURCE) 94 @IntDef({ 95 TYPOGRAPHY_DISPLAY1, 96 TYPOGRAPHY_DISPLAY2, 97 TYPOGRAPHY_DISPLAY3, 98 TYPOGRAPHY_TITLE1, 99 TYPOGRAPHY_TITLE2, 100 TYPOGRAPHY_TITLE3, 101 TYPOGRAPHY_BODY1, 102 TYPOGRAPHY_BODY2, 103 TYPOGRAPHY_BUTTON, 104 TYPOGRAPHY_CAPTION1, 105 TYPOGRAPHY_CAPTION2, 106 TYPOGRAPHY_CAPTION3 107 }) 108 @interface TypographyName {} 109 110 /** Mapping for line height for different typography. */ 111 private static final @NonNull Map<Integer, Float> TYPOGRAPHY_TO_LINE_HEIGHT_SP = 112 new HashMap<>(); 113 114 static { TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_DISPLAY1, 46f)115 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_DISPLAY1, 46f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_DISPLAY2, 40f)116 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_DISPLAY2, 40f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_DISPLAY3, 36f)117 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_DISPLAY3, 36f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_TITLE1, 28f)118 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_TITLE1, 28f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_TITLE2, 24f)119 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_TITLE2, 24f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_TITLE3, 20f)120 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_TITLE3, 20f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_BODY1, 20f)121 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_BODY1, 20f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_BODY2, 18f)122 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_BODY2, 18f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_BUTTON, 19f)123 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_BUTTON, 19f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION1, 18f)124 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION1, 18f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION2, 16f)125 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION2, 16f); TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION3, 14f)126 TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION3, 14f); 127 } 128 Typography()129 private Typography() {} 130 131 /** 132 * Returns the {@link FontStyle.Builder} for the given FontStyle code with the recommended size, 133 * weight and letter spacing. Font will be scalable. 134 */ getFontStyleBuilder( @ypographyName int fontStyleCode, @NonNull Context context)135 static FontStyle.@NonNull Builder getFontStyleBuilder( 136 @TypographyName int fontStyleCode, @NonNull Context context) { 137 return getFontStyleBuilder(fontStyleCode, context, true); 138 } 139 140 /** 141 * Returns the {@link FontStyle.Builder} for the given Typography code with the recommended 142 * size, weight and letter spacing, with the option to make this font not scalable. 143 */ getFontStyleBuilder( @ypographyName int typographyCode, @NonNull Context context, boolean isScalable)144 static FontStyle.@NonNull Builder getFontStyleBuilder( 145 @TypographyName int typographyCode, @NonNull Context context, boolean isScalable) { 146 switch (typographyCode) { 147 case TYPOGRAPHY_BODY1: 148 return body1(isScalable, context); 149 case TYPOGRAPHY_BODY2: 150 return body2(isScalable, context); 151 case TYPOGRAPHY_BUTTON: 152 return button(isScalable, context); 153 case TYPOGRAPHY_CAPTION1: 154 return caption1(isScalable, context); 155 case TYPOGRAPHY_CAPTION2: 156 return caption2(isScalable, context); 157 case TYPOGRAPHY_CAPTION3: 158 return caption3(isScalable, context); 159 case TYPOGRAPHY_DISPLAY1: 160 return display1(isScalable, context); 161 case TYPOGRAPHY_DISPLAY2: 162 return display2(isScalable, context); 163 case TYPOGRAPHY_DISPLAY3: 164 return display3(isScalable, context); 165 case TYPOGRAPHY_TITLE1: 166 return title1(isScalable, context); 167 case TYPOGRAPHY_TITLE2: 168 return title2(isScalable, context); 169 case TYPOGRAPHY_TITLE3: 170 return title3(isScalable, context); 171 default: 172 // Shouldn't happen. 173 throw new IllegalArgumentException( 174 "Typography " + typographyCode + " doesn't exist."); 175 } 176 } 177 178 /** 179 * Returns the recommended line height for the given Typography to be added to the Text 180 * component. 181 */ getLineHeightForTypography(@ypographyName int typography)182 static @NonNull SpProp getLineHeightForTypography(@TypographyName int typography) { 183 if (!TYPOGRAPHY_TO_LINE_HEIGHT_SP.containsKey(typography)) { 184 throw new IllegalArgumentException("Typography " + typography + " doesn't exist."); 185 } 186 return sp(checkNotNull(TYPOGRAPHY_TO_LINE_HEIGHT_SP.get(typography)).intValue()); 187 } 188 189 /** 190 * This is a helper function to make the font not scalable. It should interpret in value as DP 191 * and convert it to SP which is needed to be passed in as a font size. However, we will pass an 192 * SP object to it, because the default style is defined in it, but for the case when the font 193 * size on device is 1, so the DP is equal to SP. 194 */ 195 @Dimension(unit = SP) dpToSp(float fontScale, @Dimension(unit = DP) float valueDp)196 private static float dpToSp(float fontScale, @Dimension(unit = DP) float valueDp) { 197 FontScaleConverter converter = 198 (SDK_INT >= UPSIDE_DOWN_CAKE) 199 ? FontScaleConverterFactory.forScale(fontScale) 200 : null; 201 202 if (converter == null) { 203 return dpToSpLinear(fontScale, valueDp); 204 } 205 206 return converter.convertDpToSp(valueDp); 207 } 208 209 @Dimension(unit = SP) dpToSpLinear(float fontScale, @Dimension(unit = DP) float valueDp)210 private static float dpToSpLinear(float fontScale, @Dimension(unit = DP) float valueDp) { 211 return valueDp / fontScale; 212 } 213 214 // The @Dimension(unit = SP) on sp() is seemingly being ignored, so lint complains that we're 215 // passing SP to something expecting PX. Just suppress the warning for now. 216 @SuppressLint("ResourceType") createFontStyleBuilder( @imensionunit = SP) int size, @FontWeight int weight, @FontVariant int variant, float letterSpacing, boolean isScalable, @NonNull Context context)217 private static FontStyle.Builder createFontStyleBuilder( 218 @Dimension(unit = SP) int size, 219 @FontWeight int weight, 220 @FontVariant int variant, 221 float letterSpacing, 222 boolean isScalable, 223 @NonNull Context context) { 224 float fontScale = context.getResources().getConfiguration().fontScale; 225 return new FontStyle.Builder() 226 .setSize(DimensionBuilders.sp(isScalable ? size : dpToSp(fontScale, size))) 227 .setLetterSpacing(DimensionBuilders.em(letterSpacing)) 228 .setVariant(variant) 229 .setWeight(weight); 230 } 231 232 /** Font style for large display text. */ display1(boolean isScalable, @NonNull Context context)233 private static FontStyle.@NonNull Builder display1(boolean isScalable, 234 @NonNull Context context) { 235 return createFontStyleBuilder( 236 40, FONT_WEIGHT_MEDIUM, FONT_VARIANT_TITLE, 0.01f, isScalable, context); 237 } 238 239 /** Font style for medium display text. */ display2(boolean isScalable, @NonNull Context context)240 private static FontStyle.@NonNull Builder display2(boolean isScalable, 241 @NonNull Context context) { 242 return createFontStyleBuilder( 243 34, FONT_WEIGHT_MEDIUM, FONT_VARIANT_TITLE, 0.03f, isScalable, context); 244 } 245 246 /** Font style for small display text. */ display3(boolean isScalable, @NonNull Context context)247 private static FontStyle.@NonNull Builder display3(boolean isScalable, 248 @NonNull Context context) { 249 return createFontStyleBuilder( 250 30, FONT_WEIGHT_MEDIUM, FONT_VARIANT_TITLE, 0.03f, isScalable, context); 251 } 252 253 /** Font style for large title text. */ title1(boolean isScalable, @NonNull Context context)254 private static FontStyle.@NonNull Builder title1(boolean isScalable, @NonNull Context context) { 255 return createFontStyleBuilder( 256 24, FONT_WEIGHT_MEDIUM, FONT_VARIANT_TITLE, 0.008f, isScalable, context); 257 } 258 259 /** Font style for medium title text. */ title2(boolean isScalable, @NonNull Context context)260 private static FontStyle.@NonNull Builder title2(boolean isScalable, @NonNull Context context) { 261 return createFontStyleBuilder( 262 20, FONT_WEIGHT_MEDIUM, FONT_VARIANT_TITLE, 0.01f, isScalable, context); 263 } 264 265 /** Font style for small title text. */ title3(boolean isScalable, @NonNull Context context)266 private static FontStyle.@NonNull Builder title3(boolean isScalable, @NonNull Context context) { 267 return createFontStyleBuilder( 268 16, FONT_WEIGHT_MEDIUM, FONT_VARIANT_TITLE, 0.01f, isScalable, context); 269 } 270 271 /** Font style for normal body text. */ body1(boolean isScalable, @NonNull Context context)272 private static FontStyle.@NonNull Builder body1(boolean isScalable, @NonNull Context context) { 273 return createFontStyleBuilder( 274 16, FONT_WEIGHT_NORMAL, FONT_VARIANT_BODY, 0.01f, isScalable, context); 275 } 276 277 /** Font style for small body text. */ body2(boolean isScalable, @NonNull Context context)278 private static FontStyle.@NonNull Builder body2(boolean isScalable, @NonNull Context context) { 279 return createFontStyleBuilder( 280 14, FONT_WEIGHT_NORMAL, FONT_VARIANT_BODY, 0.014f, isScalable, context); 281 } 282 283 /** Font style for bold button text. */ button(boolean isScalable, @NonNull Context context)284 private static FontStyle.@NonNull Builder button(boolean isScalable, @NonNull Context context) { 285 return createFontStyleBuilder( 286 15, FONT_WEIGHT_BOLD, FONT_VARIANT_BODY, 0.03f, isScalable, context); 287 } 288 289 /** Font style for large caption text. */ caption1(boolean isScalable, @NonNull Context context)290 private static FontStyle.@NonNull Builder caption1(boolean isScalable, 291 @NonNull Context context) { 292 return createFontStyleBuilder( 293 14, FONT_WEIGHT_MEDIUM, FONT_VARIANT_BODY, 0.01f, isScalable, context); 294 } 295 296 /** Font style for medium caption text. */ caption2(boolean isScalable, @NonNull Context context)297 private static FontStyle.@NonNull Builder caption2(boolean isScalable, 298 @NonNull Context context) { 299 return createFontStyleBuilder( 300 12, FONT_WEIGHT_MEDIUM, FONT_VARIANT_BODY, 0.01f, isScalable, context); 301 } 302 303 /** Font style for small caption text. */ caption3(boolean isScalable, @NonNull Context context)304 private static FontStyle.@NonNull Builder caption3(boolean isScalable, 305 @NonNull Context context) { 306 return createFontStyleBuilder( 307 10, FONT_WEIGHT_MEDIUM, FONT_VARIANT_BODY, 0.01f, isScalable, context); 308 } 309 } 310