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