• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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 package com.android.car.scalableui.loader.xml;
17 
18 import static android.view.Display.DEFAULT_DISPLAY;
19 
20 import static com.android.car.scalableui.model.Alpha.DEFAULT_ALPHA;
21 import static com.android.car.scalableui.model.Layer.DEFAULT_LAYER;
22 import static com.android.car.scalableui.model.Transition.DEFAULT_DURATION;
23 import static com.android.car.scalableui.model.Visibility.DEFAULT_VISIBILITY;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorInflater;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.graphics.Insets;
30 import android.util.AttributeSet;
31 import android.util.DisplayMetrics;
32 import android.util.Xml;
33 import android.view.animation.AnimationUtils;
34 import android.view.animation.Interpolator;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 
39 import com.android.car.scalableui.model.Alpha;
40 import com.android.car.scalableui.model.Bounds;
41 import com.android.car.scalableui.model.Corner;
42 import com.android.car.scalableui.model.KeyFrameVariant;
43 import com.android.car.scalableui.model.Layer;
44 import com.android.car.scalableui.model.PanelState;
45 import com.android.car.scalableui.model.Role;
46 import com.android.car.scalableui.model.Transition;
47 import com.android.car.scalableui.model.Variant;
48 import com.android.car.scalableui.model.Visibility;
49 
50 import org.xmlpull.v1.XmlPullParser;
51 import org.xmlpull.v1.XmlPullParserException;
52 
53 import java.io.IOException;
54 import java.util.ArrayList;
55 import java.util.List;
56 import java.util.Locale;
57 
58 /**
59  * A utility class that uses a {@link XmlPullParser} to create a {@link PanelState} object.
60  */
61 public class PanelStateXmlParser {
62     private static final String TAG = PanelStateXmlParser.class.getSimpleName();
63 
64     // --- Panel Tags ---
65     public static final String PANEL_TAG = "Panel";
66     public static final String ID_ATTRIBUTE = "id";
67     public static final String DEFAULT_VARIANT_ATTRIBUTE = "defaultVariant";
68     public static final String ROLE_ATTRIBUTE = "role";
69     public static final String DISPLAY_ID = "displayId";
70     public static final String DEFAULT_LAYER_ATTRIBUTE = "defaultLayer";
71 
72     // --- Transitions Tags ---
73     public static final String TRANSITIONS_TAG = "Transitions";
74     public static final String DEFAULT_DURATION_ATTRIBUTE = "defaultDuration";
75     public static final String DEFAULT_INTERPOLATOR_ATTRIBUTE = "defaultInterpolator";
76 
77     // --- Transition Tags ---
78     public static final String TRANSITION_TAG = "Transition";
79     public static final String FROM_VARIANT_ATTRIBUTE = "fromVariant";
80     public static final String TO_VARIANT_ATTRIBUTE = "toVariant";
81     public static final String ON_EVENT_ATTRIBUTE = "onEvent";
82     public static final String ON_EVENT_TOKENS_ATTRIBUTE = "onEventTokens";
83     public static final String ANIMATOR_ATTRIBUTE = "animator";
84     public static final String DURATION_ATTRIBUTE = "duration";
85     public static final String INTERPOLATOR_ATTRIBUTE = "interpolator";
86 
87     // --- Variant Tags ---
88     public static final String VARIANT_TAG = "Variant";
89     public static final String PARENT_ATTRIBUTE = "parent";
90 
91     // --- KeyFrameVariant Tags ---
92     static final String KEY_FRAME_VARIANT_TAG = "KeyFrameVariant";
93     private static final String KEY_FRAME_TAG = "KeyFrame";
94     private static final String FRAME_ATTRIBUTE = "frame";
95     private static final String VARIANT_ATTRIBUTE = "variant";
96 
97     // --- Visibility Tags ---
98     public static final String VISIBILITY_TAG = "Visibility";
99     public static final String IS_VISIBLE_ATTRIBUTE = "isVisible";
100 
101     // --- Alpha Tags ---
102     public static final String ALPHA_TAG = "Alpha";
103     public static final String ALPHA_VALUE_ATTRIBUTE = "alpha";
104 
105     // --- Layer Tags ---
106     public static final String LAYER_TAG = "Layer";
107     public static final String LAYER_VALUE_ATTRIBUTE = "layer";
108 
109     // --- Bounds Tags ---
110     public static final String BOUNDS_TAG = "Bounds";
111     public static final String LEFT_ATTRIBUTE = "left";
112     public static final String RIGHT_ATTRIBUTE = "right";
113     public static final String TOP_ATTRIBUTE = "top";
114     public static final String BOTTOM_ATTRIBUTE = "bottom";
115     public static final String WIDTH_ATTRIBUTE = "width";
116     public static final String HEIGHT_ATTRIBUTE = "height";
117 
118     // --- Corner Tags ---
119     public static final String CORNER_TAG = "Corner";
120     public static final String RADIUS_ATTRIBUTE = "radius";
121 
122     // --- Insets Tags ---
123     public static final String INSETS_TAG = "Insets";
124 
125     public static final String DIP = "dip";
126     public static final String DP = "dp";
127     public static final String PERCENT = "%";
128 
129     @NonNull
parse(@onNull Context context, @NonNull XmlPullParser parser)130     static PanelState parse(@NonNull Context context, @NonNull XmlPullParser parser)
131             throws XmlPullParserException, IOException {
132 
133         // Consume any START_DOCUMENT or whitespace events
134         int eventType = parser.getEventType();
135         while (eventType == XmlPullParser.START_DOCUMENT
136                 || (eventType == XmlPullParser.TEXT && parser.isWhitespace())) {
137             eventType = parser.next();
138         }
139         if (eventType != XmlPullParser.START_TAG || !parser.getName().equals("Panel")) {
140             throw new XmlPullParserException("Expected <Panel> tag at the beginning but "
141                     + parser.getName());
142         }
143 
144         parser.require(XmlPullParser.START_TAG, null, PANEL_TAG);
145         AttributeSet attrs = Xml.asAttributeSet(parser);
146         String id = attrs.getAttributeValue(null, ID_ATTRIBUTE);
147         String displayIdStr = attrs.getAttributeValue(null, DISPLAY_ID);
148         int displayId = (displayIdStr == null) ? DEFAULT_DISPLAY : Integer.parseInt(displayIdStr);
149         String defaultVariant = attrs.getAttributeValue(null, DEFAULT_VARIANT_ATTRIBUTE);
150         int roleValue = attrs.getAttributeResourceValue(null, ROLE_ATTRIBUTE, 0);
151 
152         Integer defaultLayer = null;
153         if (attrs.getAttributeValue(null, DEFAULT_LAYER_ATTRIBUTE) != null) {
154             int resId = attrs.getAttributeResourceValue(null, DEFAULT_LAYER_ATTRIBUTE, 0);
155             if (resId != 0) {
156                 defaultLayer = context.getResources().getInteger(resId);
157             } else {
158                 defaultLayer =
159                         attrs.getAttributeIntValue(null, DEFAULT_LAYER_ATTRIBUTE, DEFAULT_LAYER);
160             }
161         }
162 
163         PanelState.Builder builder = new PanelState.Builder(id, new Role(roleValue));
164         builder.setDisplayId(displayId);
165         builder.setDefaultVariant(defaultVariant);
166         PanelState panelState = builder.build();
167 
168         while (parser.next() != XmlPullParser.END_TAG) {
169             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
170             String name = parser.getName();
171             switch (name) {
172                 case VARIANT_TAG:
173                     panelState.addVariant(
174                             parseVariant(context, panelState, defaultLayer, parser));
175                     break;
176                 case KEY_FRAME_VARIANT_TAG:
177                     panelState.addVariant(parseKeyFrameVariant(panelState, parser));
178                     break;
179                 case TRANSITIONS_TAG:
180                     List<Transition> transitions = parseTransitions(context, panelState, parser);
181                     for (Transition transition : transitions) {
182                         panelState.addTransition(transition);
183                     }
184                     break;
185                 default:
186                     XmlPullParserHelper.skip(parser);
187             }
188         }
189         panelState.setVariant(defaultVariant); // Set the initial variant
190         return panelState;
191     }
192 
193     @NonNull
parseKeyFrameVariant( @onNull PanelState panelState, @NonNull XmlPullParser parser)194     private static Variant parseKeyFrameVariant(
195             @NonNull PanelState panelState,
196             @NonNull XmlPullParser parser) throws IOException, XmlPullParserException {
197         parser.require(XmlPullParser.START_TAG, null, KEY_FRAME_VARIANT_TAG);
198         AttributeSet attrs = Xml.asAttributeSet(parser);
199         String id = attrs.getAttributeValue(null, ID_ATTRIBUTE);
200         String parentStr = attrs.getAttributeValue(null, PARENT_ATTRIBUTE);
201         Variant parent = panelState.getVariant(parentStr);
202         KeyFrameVariant.Builder builder = new KeyFrameVariant.Builder(id);
203         if (parent != null) {
204             builder.setParent(parent);
205         }
206         while (parser.next() != XmlPullParser.END_TAG) {
207             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
208             String name = parser.getName();
209             if (name.equals(KEY_FRAME_TAG)) {
210                 builder.addKeyFrame(parseKeyFrame(panelState, parser));
211             } else {
212                 XmlPullParserHelper.skip(parser);
213             }
214         }
215         return builder.build();
216     }
217 
parseKeyFrame( @onNull PanelState panelState, @NonNull XmlPullParser parser)218     private static KeyFrameVariant.KeyFrame parseKeyFrame(
219             @NonNull PanelState panelState,
220             @NonNull XmlPullParser parser) throws XmlPullParserException, IOException {
221         parser.require(XmlPullParser.START_TAG, null, KEY_FRAME_TAG);
222         AttributeSet attrs = Xml.asAttributeSet(parser);
223         int frame = attrs.getAttributeIntValue(null, FRAME_ATTRIBUTE, 0);
224         String variant = attrs.getAttributeValue(null, VARIANT_ATTRIBUTE);
225         parser.nextTag();
226         parser.require(XmlPullParser.END_TAG, null, KEY_FRAME_TAG);
227         Variant panelVariant = panelState.getVariant(variant);
228         if (panelVariant == null) {
229             throw new XmlPullParserException("Variant not found: " + variant);
230         }
231         return new KeyFrameVariant.KeyFrame.Builder(frame, panelVariant).build();
232     }
233 
234     @NonNull
parseVariant( @onNull Context context, @NonNull PanelState panelState, @Nullable Integer defaultLayer, @NonNull XmlPullParser parser)235     private static Variant parseVariant(
236             @NonNull Context context,
237             @NonNull PanelState panelState,
238             @Nullable Integer defaultLayer,
239             @NonNull XmlPullParser parser)
240             throws IOException, XmlPullParserException {
241         parser.require(XmlPullParser.START_TAG, null, VARIANT_TAG);
242         AttributeSet attrs = Xml.asAttributeSet(parser);
243 
244         String id = attrs.getAttributeValue(null, ID_ATTRIBUTE);
245         String parentVariantId = attrs.getAttributeValue(null, PARENT_ATTRIBUTE);
246         Variant parentVariant = panelState.getVariant(parentVariantId);
247 
248         Variant.Builder variantBuilder = new Variant.Builder(id);
249         variantBuilder.setLayer(defaultLayer);
250         variantBuilder.setParent(parentVariant);
251         while (parser.next() != XmlPullParser.END_TAG) {
252             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
253             String name = parser.getName();
254             switch (name) {
255                 case VISIBILITY_TAG:
256                     variantBuilder.setVisibility(parseVisibility(parser).isVisible());
257                     break;
258                 case ALPHA_TAG:
259                     variantBuilder.setAlpha(parseAlpha(context, parser).getAlpha());
260                     break;
261                 case LAYER_TAG:
262                     variantBuilder.setLayer(parseLayer(context, parser).getLayer());
263                     break;
264                 case BOUNDS_TAG:
265                     variantBuilder.setBounds(parseBounds(context, parser).getRect());
266                     break;
267                 case CORNER_TAG:
268                     variantBuilder.setCornerRadius(parseCorner(context, parser).getRadius());
269                     break;
270                 case INSETS_TAG:
271                     variantBuilder.setInsets(parseInsets(context, parser));
272                     break;
273                 default:
274                     XmlPullParserHelper.skip(parser); // Skip other nested tags
275             }
276         }
277         return variantBuilder.build();
278     }
279 
280     @NonNull
parseVisibility(@onNull XmlPullParser parser)281     private static Visibility parseVisibility(@NonNull XmlPullParser parser)
282             throws IOException, XmlPullParserException {
283         parser.require(XmlPullParser.START_TAG, null, VISIBILITY_TAG);
284         AttributeSet attrs = Xml.asAttributeSet(parser);
285         boolean isVisible =
286                 attrs.getAttributeBooleanValue(null, IS_VISIBLE_ATTRIBUTE, DEFAULT_VISIBILITY);
287 
288         while (parser.next() != XmlPullParser.END_TAG) {
289             XmlPullParserHelper.skip(parser); // Skip any nested tags
290         }
291 
292         return new Visibility.Builder().setIsVisible(isVisible).build();
293     }
294 
295     @NonNull
parseAlpha(@onNull Context context, @NonNull XmlPullParser parser)296     private static Alpha parseAlpha(@NonNull Context context, @NonNull XmlPullParser parser)
297             throws IOException, XmlPullParserException {
298         parser.require(XmlPullParser.START_TAG, null, ALPHA_TAG);
299         AttributeSet attrs = Xml.asAttributeSet(parser);
300         float alpha = DEFAULT_ALPHA;
301         int resId = attrs.getAttributeResourceValue(null, ALPHA_VALUE_ATTRIBUTE, 0);
302         if (resId != 0) {
303             alpha = context.getResources().getFloat(resId);
304         } else {
305             alpha = attrs.getAttributeFloatValue(null, ALPHA_VALUE_ATTRIBUTE, DEFAULT_ALPHA);
306         }
307 
308         while (parser.next() != XmlPullParser.END_TAG) {
309             XmlPullParserHelper.skip(parser); // Skip any nested tags
310         }
311 
312         return new Alpha.Builder().setAlpha(alpha).build();
313     }
314 
315     @NonNull
parseLayer(@onNull Context context, @NonNull XmlPullParser parser)316     private static Layer parseLayer(@NonNull Context context, @NonNull XmlPullParser parser)
317             throws IOException, XmlPullParserException {
318         parser.require(XmlPullParser.START_TAG, null, LAYER_TAG);
319         AttributeSet attrs = Xml.asAttributeSet(parser);
320 
321         int layer = DEFAULT_LAYER;
322         int resId = attrs.getAttributeResourceValue(null, LAYER_VALUE_ATTRIBUTE, 0);
323         if (resId != 0) {
324             layer = context.getResources().getInteger(resId);
325         } else {
326             layer = attrs.getAttributeIntValue(null, LAYER_VALUE_ATTRIBUTE, DEFAULT_LAYER);
327         }
328 
329         while (parser.next() != XmlPullParser.END_TAG) {
330             XmlPullParserHelper.skip(parser); // Skip any nested tags
331         }
332 
333         return new Layer.Builder().setLayer(layer).build();
334     }
335 
336     @NonNull
parseBounds(@onNull Context context, @NonNull XmlPullParser parser)337     private static Bounds parseBounds(@NonNull Context context, @NonNull XmlPullParser parser)
338             throws IOException, XmlPullParserException {
339 
340         parser.require(XmlPullParser.START_TAG, null, BOUNDS_TAG);
341         AttributeSet attrs = Xml.asAttributeSet(parser);
342 
343         Integer left = getDimensionPixelSize(context, attrs, LEFT_ATTRIBUTE, true);
344         Integer top = getDimensionPixelSize(context, attrs, TOP_ATTRIBUTE, false);
345         Integer right = getDimensionPixelSize(context, attrs, RIGHT_ATTRIBUTE, true);
346         Integer bottom = getDimensionPixelSize(context, attrs, BOTTOM_ATTRIBUTE, false);
347 
348         Integer width = getDimensionPixelSize(context, attrs, WIDTH_ATTRIBUTE, true);
349         Integer height = getDimensionPixelSize(context, attrs, HEIGHT_ATTRIBUTE, false);
350 
351         while (parser.next() != XmlPullParser.END_TAG) {
352             XmlPullParserHelper.skip(parser); // Skip any nested tags
353         }
354 
355         return new Bounds.Builder()
356                 .setLeft(left)
357                 .setTop(top)
358                 .setRight(right)
359                 .setBottom(bottom)
360                 .setWidth(width)
361                 .setHeight(height)
362                 .build();
363     }
364 
365     @NonNull
parseCorner(Context context, XmlPullParser parser)366     private static Corner parseCorner(Context context, XmlPullParser parser)
367             throws IOException, XmlPullParserException {
368         parser.require(XmlPullParser.START_TAG, null, CORNER_TAG);
369         AttributeSet attrs = Xml.asAttributeSet(parser);
370         Integer radius = getDimensionPixelSize(context, attrs, RADIUS_ATTRIBUTE, false);
371 
372         while (parser.next() != XmlPullParser.END_TAG) {
373             XmlPullParserHelper.skip(parser); // Skip any nested tags
374         }
375 
376         return new Corner.Builder()
377                 .setRadius(radius)
378                 .build();
379     }
380 
parseInsets(@onNull Context context, @NonNull XmlPullParser parser)381     private static Insets parseInsets(@NonNull Context context, @NonNull XmlPullParser parser)
382             throws IOException, XmlPullParserException {
383 
384         parser.require(XmlPullParser.START_TAG, null, INSETS_TAG);
385         AttributeSet attrs = Xml.asAttributeSet(parser);
386 
387         Integer left = getDimensionPixelSize(context, attrs, LEFT_ATTRIBUTE, true);
388         Integer top = getDimensionPixelSize(context, attrs, TOP_ATTRIBUTE, false);
389         Integer right = getDimensionPixelSize(context, attrs, RIGHT_ATTRIBUTE, true);
390         Integer bottom = getDimensionPixelSize(context, attrs, BOTTOM_ATTRIBUTE, false);
391 
392         while (parser.next() != XmlPullParser.END_TAG) {
393             XmlPullParserHelper.skip(parser); // Skip any nested tags
394         }
395 
396         return Insets.of(left, top, right, bottom);
397     }
398 
399     @NonNull
parseTransitions( @onNull Context context, @NonNull PanelState panelState, @NonNull XmlPullParser parser)400     private static List<Transition> parseTransitions(
401             @NonNull Context context, @NonNull PanelState panelState, @NonNull XmlPullParser parser)
402             throws XmlPullParserException, IOException {
403         parser.require(XmlPullParser.START_TAG, null, TRANSITIONS_TAG);
404         AttributeSet attrs = Xml.asAttributeSet(parser);
405         // possible lossy conversion from long to int. we're assuming the default duration can be
406         // convereted to int safely.
407         int duration = attrs.getAttributeIntValue(null, DEFAULT_DURATION_ATTRIBUTE,
408                 (int) DEFAULT_DURATION);
409         int interpolatorRef =
410                 attrs.getAttributeResourceValue(null, DEFAULT_INTERPOLATOR_ATTRIBUTE, 0);
411         Interpolator interpolator =
412                 interpolatorRef == 0
413                         ? null
414                         : AnimationUtils.loadInterpolator(context, interpolatorRef);
415 
416         List<Transition> result = new ArrayList<>();
417         while (parser.next() != XmlPullParser.END_TAG) {
418             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
419 
420             if (parser.getName().equals(TRANSITION_TAG)) {
421                 result.add(
422                         parseTransition(context, panelState, duration, interpolator, parser));
423             } else {
424                 XmlPullParserHelper.skip(parser);
425             }
426         }
427         return result;
428     }
429 
430     @NonNull
parseTransition( @onNull Context context, @NonNull PanelState panelState, long defaultDuration, @Nullable Interpolator defaultInterpolator, @NonNull XmlPullParser parser)431     private static Transition parseTransition(
432             @NonNull Context context,
433             @NonNull PanelState panelState,
434             long defaultDuration,
435             @Nullable Interpolator defaultInterpolator,
436             @NonNull XmlPullParser parser)
437             throws IOException, XmlPullParserException {
438         parser.require(XmlPullParser.START_TAG, null, TRANSITION_TAG);
439         AttributeSet attrs = Xml.asAttributeSet(parser);
440 
441         String from = attrs.getAttributeValue(null, FROM_VARIANT_ATTRIBUTE);
442         String to = attrs.getAttributeValue(null, TO_VARIANT_ATTRIBUTE);
443         String onEvent = attrs.getAttributeValue(null, ON_EVENT_ATTRIBUTE);
444         String onEventTokens = attrs.getAttributeValue(null, ON_EVENT_TOKENS_ATTRIBUTE);
445         int animatorId = attrs.getAttributeResourceValue(null, ANIMATOR_ATTRIBUTE, 0);
446         Animator animator =
447                 animatorId == 0 ? null : AnimatorInflater.loadAnimator(context, animatorId);
448         int duration = attrs.getAttributeIntValue(null, DURATION_ATTRIBUTE, (int) defaultDuration);
449         int interpolatorRef = attrs.getAttributeResourceValue(null, INTERPOLATOR_ATTRIBUTE, 0);
450         Interpolator interpolator =
451                 interpolatorRef == 0
452                         ? defaultInterpolator
453                         : AnimationUtils.loadInterpolator(context, interpolatorRef);
454         Variant fromVariant = panelState.getVariant(from);
455         Variant toVariant = panelState.getVariant(to);
456 
457         while (parser.next() != XmlPullParser.END_TAG) {
458             XmlPullParserHelper.skip(parser); // Should be no nested tags.
459         }
460 
461         return new Transition.Builder(fromVariant, toVariant)
462                 .setOnEvent(onEvent, onEventTokens)
463                 .setAnimator(animator)
464                 .setDefaultDuration(duration)
465                 .setDefaultInterpolator(interpolator)
466                 .build();
467     }
468 
469     /**
470      * Helper method to get a dimension pixel size from an attribute set.
471      *
472      * @param context      The application context.
473      * @param attrs        The attribute set.
474      * @param name         The name of the attribute.
475      * @param isHorizontal Whether the dimension is horizontal (width) or vertical (height).
476      * @return The dimension pixel size.
477      */
478     @Nullable
getDimensionPixelSize(@onNull Context context, @NonNull AttributeSet attrs, @NonNull String name, boolean isHorizontal)479     private static Integer getDimensionPixelSize(@NonNull Context context,
480             @NonNull AttributeSet attrs, @NonNull String name, boolean isHorizontal) {
481         int resId = attrs.getAttributeResourceValue(null, name, 0);
482         if (resId != 0) {
483             return context.getResources().getDimensionPixelSize(resId);
484         }
485         String dimenStr = attrs.getAttributeValue(null, name);
486         if (dimenStr == null) {
487             return null;
488         }
489         if (dimenStr.toLowerCase(Locale.ROOT).endsWith(DP)) {
490             String valueStr = dimenStr.substring(0, dimenStr.length() - DP.length());
491             float value = Float.parseFloat(valueStr);
492             return (int) (value * Resources.getSystem().getDisplayMetrics().density);
493         } else if (dimenStr.toLowerCase(Locale.ROOT).endsWith(DIP)) {
494             String valueStr = dimenStr.substring(0, dimenStr.length() - DIP.length());
495             float value = Float.parseFloat(valueStr);
496             return (int) (value * Resources.getSystem().getDisplayMetrics().density);
497         } else if (dimenStr.toLowerCase(Locale.ROOT).endsWith(PERCENT)) {
498             String valueStr = dimenStr.substring(0, dimenStr.length() - PERCENT.length());
499             float value = Float.parseFloat(valueStr);
500             DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
501             if (isHorizontal) {
502                 return (int) (value * displayMetrics.widthPixels / 100);
503             } else {
504                 return (int) (value * displayMetrics.heightPixels / 100);
505             }
506         } else {
507             // The default value is never returned because `attrs.getAttributeValue` is not null.
508             return attrs.getAttributeIntValue(null, name, 0);
509         }
510     }
511 }
512