blob: c6a3d9c475c58ed80c6a6b7e363811d8fb06c3fc [file] [log] [blame]
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001/*
2 * Copyright 2023 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
17package androidx.wear.protolayout.renderer.inflater;
18
19import static android.os.Looper.getMainLooper;
20
21import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
22import static androidx.wear.protolayout.proto.ModifiersProto.SlideParentSnapOption.SLIDE_PARENT_SNAP_TO_INSIDE;
23import static androidx.wear.protolayout.proto.ModifiersProto.SlideParentSnapOption.SLIDE_PARENT_SNAP_TO_OUTSIDE;
Neda Topoljanac3a83ae72023-04-05 20:48:11 +000024import static androidx.wear.protolayout.renderer.R.id.clickable_id_tag;
Neda Topoljanac4d26f822023-02-22 16:57:03 +000025import static androidx.wear.protolayout.renderer.helper.TestDsl.arc;
26import static androidx.wear.protolayout.renderer.helper.TestDsl.arcText;
27import static androidx.wear.protolayout.renderer.helper.TestDsl.box;
28import static androidx.wear.protolayout.renderer.helper.TestDsl.column;
29import static androidx.wear.protolayout.renderer.helper.TestDsl.dynamicFixedText;
30import static androidx.wear.protolayout.renderer.helper.TestDsl.image;
31import static androidx.wear.protolayout.renderer.helper.TestDsl.layout;
32import static androidx.wear.protolayout.renderer.helper.TestDsl.row;
33import static androidx.wear.protolayout.renderer.helper.TestDsl.text;
Neda Topoljanac742e9e72023-10-06 22:18:35 +000034import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.TEXT_AUTOSIZES_LIMIT;
Neda Topoljanac4d26f822023-02-22 16:57:03 +000035import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.getFrameLayoutGravity;
36import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.getRenderedMetadata;
37import static androidx.wear.protolayout.renderer.test.R.drawable.android_animated_24dp;
38
39import static com.google.common.truth.Truth.assertThat;
40
41import static org.junit.Assert.assertThrows;
42import static org.robolectric.Shadows.shadowOf;
43
44import static java.lang.Integer.MAX_VALUE;
45
46import android.app.Activity;
47import android.app.Application;
48import android.content.ComponentName;
49import android.content.Intent;
50import android.content.pm.ActivityInfo;
51import android.graphics.Paint.Cap;
52import android.graphics.Rect;
53import android.graphics.drawable.AnimatedVectorDrawable;
54import android.graphics.drawable.Drawable;
55import android.os.Build.VERSION;
56import android.os.Build.VERSION_CODES;
57import android.os.Looper;
58import android.os.SystemClock;
59import android.text.TextUtils.TruncateAt;
Mohammad Saboorian8d7f74f2023-04-17 16:55:34 +010060import android.util.Pair;
Neda Topoljanac4d26f822023-02-22 16:57:03 +000061import android.view.MotionEvent;
62import android.view.View;
63import android.view.View.MeasureSpec;
64import android.view.ViewGroup;
65import android.view.animation.Animation;
66import android.view.animation.Transformation;
67import android.widget.FrameLayout;
68import android.widget.FrameLayout.LayoutParams;
69import android.widget.ImageView;
70import android.widget.LinearLayout;
Mohammad Saboorian8d7f74f2023-04-17 16:55:34 +010071import android.widget.Space;
Neda Topoljanac4d26f822023-02-22 16:57:03 +000072import android.widget.TextView;
73
74import androidx.annotation.NonNull;
75import androidx.core.content.ContextCompat;
76import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
77import androidx.test.ext.junit.runners.AndroidJUnit4;
Xiangyin Mab33ea062023-07-28 16:14:25 +000078import androidx.vectordrawable.graphics.drawable.SeekableAnimatedVectorDrawable;
Xiangyin Ma3d99b132023-04-06 22:03:11 +000079import androidx.wear.protolayout.expression.AppDataKey;
80import androidx.wear.protolayout.expression.DynamicBuilders;
Neda Topoljanac4d26f822023-02-22 16:57:03 +000081import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
Neda Topoljanac9076abd2023-03-27 11:34:48 +000082import androidx.wear.protolayout.expression.pipeline.StateStore;
Xiangyin Ma4780e272023-03-07 16:42:32 +000083import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationParameters;
Neda Topoljanac4d26f822023-02-22 16:57:03 +000084import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
85import androidx.wear.protolayout.expression.proto.AnimationParameterProto.Repeatable;
Neda Topoljanacdcea2462023-06-12 14:02:03 +000086import androidx.wear.protolayout.expression.proto.DynamicDataProto.DynamicDataValue;
Neda Topoljanac4d26f822023-02-22 16:57:03 +000087import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicFloat;
88import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicColor;
89import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicFloat;
90import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicInt32;
91import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicString;
92import androidx.wear.protolayout.expression.proto.DynamicProto.Int32ToFloatOp;
93import androidx.wear.protolayout.expression.proto.DynamicProto.StateColorSource;
94import androidx.wear.protolayout.expression.proto.DynamicProto.StateFloatSource;
95import androidx.wear.protolayout.expression.proto.DynamicProto.StateInt32Source;
96import androidx.wear.protolayout.expression.proto.DynamicProto.StateStringSource;
97import androidx.wear.protolayout.expression.proto.FixedProto.FixedColor;
98import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
99import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
100import androidx.wear.protolayout.expression.proto.FixedProto.FixedString;
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000101import androidx.wear.protolayout.proto.ActionProto.Action;
102import androidx.wear.protolayout.proto.ActionProto.AndroidActivity;
103import androidx.wear.protolayout.proto.ActionProto.AndroidBooleanExtra;
104import androidx.wear.protolayout.proto.ActionProto.AndroidDoubleExtra;
105import androidx.wear.protolayout.proto.ActionProto.AndroidExtra;
106import androidx.wear.protolayout.proto.ActionProto.AndroidIntExtra;
107import androidx.wear.protolayout.proto.ActionProto.AndroidLongExtra;
108import androidx.wear.protolayout.proto.ActionProto.AndroidStringExtra;
109import androidx.wear.protolayout.proto.ActionProto.LaunchAction;
110import androidx.wear.protolayout.proto.ActionProto.LoadAction;
111import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignment;
112import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignmentProp;
113import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignment;
114import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignmentProp;
115import androidx.wear.protolayout.proto.ColorProto.ColorProp;
Neda Topoljanac742e9e72023-10-06 22:18:35 +0000116import androidx.wear.protolayout.proto.DimensionProto;
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000117import androidx.wear.protolayout.proto.DimensionProto.ArcLineLength;
118import androidx.wear.protolayout.proto.DimensionProto.ArcSpacerLength;
119import androidx.wear.protolayout.proto.DimensionProto.ContainerDimension;
120import androidx.wear.protolayout.proto.DimensionProto.DegreesProp;
121import androidx.wear.protolayout.proto.DimensionProto.DpProp;
122import androidx.wear.protolayout.proto.DimensionProto.ExpandedAngularDimensionProp;
123import androidx.wear.protolayout.proto.DimensionProto.ExpandedDimensionProp;
Mohammad Saboorian8d7f74f2023-04-17 16:55:34 +0100124import androidx.wear.protolayout.proto.DimensionProto.ExtensionDimension;
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000125import androidx.wear.protolayout.proto.DimensionProto.ImageDimension;
126import androidx.wear.protolayout.proto.DimensionProto.ProportionalDimensionProp;
127import androidx.wear.protolayout.proto.DimensionProto.SpacerDimension;
128import androidx.wear.protolayout.proto.DimensionProto.WrappedDimensionProp;
129import androidx.wear.protolayout.proto.LayoutElementProto;
130import androidx.wear.protolayout.proto.LayoutElementProto.Arc;
131import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
132import androidx.wear.protolayout.proto.LayoutElementProto.ArcLine;
133import androidx.wear.protolayout.proto.LayoutElementProto.ArcSpacer;
134import androidx.wear.protolayout.proto.LayoutElementProto.ArcText;
135import androidx.wear.protolayout.proto.LayoutElementProto.Box;
136import androidx.wear.protolayout.proto.LayoutElementProto.ColorFilter;
137import androidx.wear.protolayout.proto.LayoutElementProto.Column;
Mohammad Saboorian8d7f74f2023-04-17 16:55:34 +0100138import androidx.wear.protolayout.proto.LayoutElementProto.ExtensionLayoutElement;
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000139import androidx.wear.protolayout.proto.LayoutElementProto.FontStyle;
140import androidx.wear.protolayout.proto.LayoutElementProto.Image;
141import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
142import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
Lucas Muller Oliveira3a02f402023-04-12 09:25:42 +0000143import androidx.wear.protolayout.proto.LayoutElementProto.MarqueeParameters;
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000144import androidx.wear.protolayout.proto.LayoutElementProto.Row;
145import androidx.wear.protolayout.proto.LayoutElementProto.Spacer;
146import androidx.wear.protolayout.proto.LayoutElementProto.Span;
147import androidx.wear.protolayout.proto.LayoutElementProto.SpanImage;
148import androidx.wear.protolayout.proto.LayoutElementProto.SpanText;
149import androidx.wear.protolayout.proto.LayoutElementProto.Spannable;
150import androidx.wear.protolayout.proto.LayoutElementProto.StrokeCapProp;
151import androidx.wear.protolayout.proto.LayoutElementProto.Text;
152import androidx.wear.protolayout.proto.LayoutElementProto.TextOverflow;
153import androidx.wear.protolayout.proto.LayoutElementProto.TextOverflowProp;
154import androidx.wear.protolayout.proto.ModifiersProto.AnimatedVisibility;
155import androidx.wear.protolayout.proto.ModifiersProto.Border;
156import androidx.wear.protolayout.proto.ModifiersProto.Clickable;
157import androidx.wear.protolayout.proto.ModifiersProto.EnterTransition;
158import androidx.wear.protolayout.proto.ModifiersProto.ExitTransition;
159import androidx.wear.protolayout.proto.ModifiersProto.FadeInTransition;
160import androidx.wear.protolayout.proto.ModifiersProto.FadeOutTransition;
161import androidx.wear.protolayout.proto.ModifiersProto.Modifiers;
162import androidx.wear.protolayout.proto.ModifiersProto.Padding;
163import androidx.wear.protolayout.proto.ModifiersProto.Semantics;
164import androidx.wear.protolayout.proto.ModifiersProto.SemanticsRole;
165import androidx.wear.protolayout.proto.ModifiersProto.SlideBound;
166import androidx.wear.protolayout.proto.ModifiersProto.SlideDirection;
167import androidx.wear.protolayout.proto.ModifiersProto.SlideInTransition;
168import androidx.wear.protolayout.proto.ModifiersProto.SlideParentBound;
169import androidx.wear.protolayout.proto.ModifiersProto.SlideParentSnapOption;
170import androidx.wear.protolayout.proto.ModifiersProto.SpanModifiers;
171import androidx.wear.protolayout.proto.ResourceProto.AndroidAnimatedImageResourceByResId;
172import androidx.wear.protolayout.proto.ResourceProto.AndroidImageResourceByResId;
173import androidx.wear.protolayout.proto.ResourceProto.AndroidSeekableAnimatedImageResourceByResId;
174import androidx.wear.protolayout.proto.ResourceProto.AnimatedImageFormat;
175import androidx.wear.protolayout.proto.ResourceProto.ImageFormat;
176import androidx.wear.protolayout.proto.ResourceProto.ImageResource;
177import androidx.wear.protolayout.proto.ResourceProto.InlineImageResource;
178import androidx.wear.protolayout.proto.ResourceProto.Resources;
179import androidx.wear.protolayout.proto.StateProto.State;
180import androidx.wear.protolayout.proto.TriggerProto.OnVisibleTrigger;
181import androidx.wear.protolayout.proto.TriggerProto.Trigger;
182import androidx.wear.protolayout.proto.TypesProto.FloatProp;
183import androidx.wear.protolayout.proto.TypesProto.Int32Prop;
184import androidx.wear.protolayout.proto.TypesProto.StringProp;
185import androidx.wear.protolayout.protobuf.ByteString;
186import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
187import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
188import androidx.wear.protolayout.renderer.helper.TestFingerprinter;
189import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
190import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.ViewGroupMutation;
191import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.ViewMutationException;
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +0000192import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.ViewProperties;
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000193import androidx.wear.protolayout.renderer.test.R;
194import androidx.wear.widget.ArcLayout;
195import androidx.wear.widget.CurvedTextView;
196
197import com.google.common.collect.ImmutableMap;
198import com.google.common.truth.Expect;
199import com.google.common.util.concurrent.ListenableFuture;
200
201import org.junit.Ignore;
202import org.junit.Rule;
203import org.junit.Test;
204import org.junit.runner.RunWith;
205import org.robolectric.Robolectric;
206import org.robolectric.shadows.ShadowChoreographer;
207import org.robolectric.shadows.ShadowLooper;
208import org.robolectric.shadows.ShadowPackageManager;
209import org.robolectric.shadows.ShadowSystemClock;
210
211import java.io.IOException;
Mohammad Saboorian8d7f74f2023-04-17 16:55:34 +0100212import java.nio.charset.StandardCharsets;
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000213import java.time.Duration;
214import java.util.ArrayList;
215import java.util.Arrays;
216import java.util.List;
217import java.util.Map;
218import java.util.concurrent.ExecutionException;
219
220@RunWith(AndroidJUnit4.class)
221public class ProtoLayoutInflaterTest {
222 private static final String TEST_CLICKABLE_CLASS_NAME = "Hello";
223 private static final String TEST_CLICKABLE_PACKAGE_NAME = "World";
224 private static final String EXTRA_CLICKABLE_ID = "extra.clickable.id";
225
226 private static final int SCREEN_WIDTH = 400;
227 private static final int SCREEN_HEIGHT = 400;
228
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000229 @Rule public final Expect expect = Expect.create();
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000230
Neda Topoljanac9076abd2023-03-27 11:34:48 +0000231 private final StateStore mStateStore = new StateStore(ImmutableMap.of());
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000232 private ProtoLayoutDynamicDataPipeline mDataPipeline;
233
234 @Test
235 public void inflate_textView() {
236 String textContents = "Hello World";
237 LayoutElement root =
238 LayoutElement.newBuilder()
239 .setText(Text.newBuilder().setText(string(textContents)))
240 .build();
241
242 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
243
244 // Check that there's a text element in the layout...
245 assertThat(rootLayout.getChildCount()).isEqualTo(1);
246 assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class);
247
248 TextView tv = (TextView) rootLayout.getChildAt(0);
249
250 // Text pulled from the proto.
251 expect.that(tv.getText().toString()).isEqualTo(textContents);
252 }
253
254 @Test
255 public void inflate_textView_withColor() {
256 int color = 0xFF112233;
257 String textContents = "Hello World";
258 LayoutElement root =
259 LayoutElement.newBuilder()
260 .setText(
261 Text.newBuilder()
262 .setText(string(textContents))
263 .setFontStyle(
264 FontStyle.newBuilder()
265 .setColor(
266 ColorProp.newBuilder()
267 .setArgb(color))))
268 .build();
269
270 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
271
272 TextView tv = (TextView) rootLayout.getChildAt(0);
273 expect.that(tv.getTextColors().getDefaultColor()).isEqualTo(color);
274 }
275
276 @Test
277 public void inflate_textView_withoutText() {
278 LayoutElement root = LayoutElement.newBuilder().setText(Text.getDefaultInstance()).build();
279
280 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
281
282 TextView tv = (TextView) rootLayout.getChildAt(0);
283 expect.that(tv.getText().toString()).isEmpty();
284 }
285
Lucas Muller Oliveiraaf6d8712023-09-21 15:58:05 +0000286 @Test
287 public void inflate_textView_withEmptyValueForLayout() {
288 LayoutElement root =
289 LayoutElement.newBuilder()
290 .setText(
291 Text.newBuilder()
292 .setText(
293 StringProp.newBuilder()
294 .setValue("abcde")
295 .setDynamicValue(
296 DynamicString.newBuilder()
297 .setFixed(
298 FixedString.newBuilder()
299 .setValue("Dynamic Fixed Text")))
300 .setValueForLayout("")))
301 .build();
302
303 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
304
305 FrameLayout sizedContainer = (FrameLayout) rootLayout.getChildAt(0);
306 expect.that(sizedContainer.getWidth()).isEqualTo(0);
307 }
308
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000309 // obsoleteContentDescription is tested for backward compatibility
310 @SuppressWarnings("deprecation")
311 @Test
312 public void inflate_textView_withObsoleteSemanticsContentDescription() {
313 String textContents = "Hello World";
314 String textDescription = "Hello World Text Element";
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000315 Semantics semantics =
316 Semantics.newBuilder().setObsoleteContentDescription(textDescription).build();
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000317 LayoutElement root =
318 LayoutElement.newBuilder()
319 .setText(
320 Text.newBuilder()
321 .setText(string(textContents))
322 .setModifiers(
323 Modifiers.newBuilder().setSemantics(semantics)))
324 .build();
325
326 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
327
328 // Check that there's a text element in the layout...
329 assertThat(rootLayout.getChildCount()).isEqualTo(1);
330 assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class);
331
332 TextView tv = (TextView) rootLayout.getChildAt(0);
333
334 // Check the text contents.
335 assertThat(tv.getText().toString()).isEqualTo(textContents);
336
337 // Check the accessibility label.
338 AccessibilityNodeInfoCompat info =
339 AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo());
340 assertThat(info.getContentDescription().toString()).isEqualTo(textDescription);
341 assertThat(info.isImportantForAccessibility()).isTrue();
342 assertThat(tv.isImportantForAccessibility()).isTrue();
343 assertThat(info.isFocusable()).isTrue();
344 }
345
346 // obsoleteContentDescription is tested for backward compatibility
347 @SuppressWarnings("deprecation")
348 @Test
349 public void inflate_textView_withSemanticsContentDescription() {
350 String textContents = "Hello World";
351 String staticDescription = "StaticDescription";
352
353 StringProp descriptionProp = string(staticDescription).build();
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000354 Semantics semantics =
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000355 Semantics.newBuilder()
356 .setObsoleteContentDescription("ObsoleteContentDescription")
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000357 .setContentDescription(descriptionProp)
358 .build();
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000359 LayoutElement root =
360 LayoutElement.newBuilder()
361 .setText(
362 Text.newBuilder()
363 .setText(string(textContents))
364 .setModifiers(
365 Modifiers.newBuilder().setSemantics(semantics)))
366 .build();
367
368 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
369
370 // Check that there's a text element in the layout...
371 assertThat(rootLayout.getChildCount()).isEqualTo(1);
372 assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class);
373
374 TextView tv = (TextView) rootLayout.getChildAt(0);
375
376 // Check the text contents.
377 assertThat(tv.getText().toString()).isEqualTo(textContents);
378
379 // Check the accessibility label.
380 AccessibilityNodeInfoCompat info =
381 AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo());
382 assertThat(info.getContentDescription().toString()).isEqualTo(staticDescription);
383 assertThat(info.isImportantForAccessibility()).isTrue();
384 assertThat(tv.isImportantForAccessibility()).isTrue();
385 assertThat(info.isFocusable()).isTrue();
386 }
387
388 @Test
389 public void inflate_textView_withDynamicSemanticsDescription() {
390 String textContents = "Hello World";
391 String staticDescription = "StaticDescription";
392 String initialDynamicContentDescription = "content 1";
393 String targetDynamicContentDescription = "content 2";
394 String initialDynamicStateDescription = "state 1";
395 String targetDynamicStateDescription = "state 2";
396
397 StringProp contentDescriptionProp =
398 string(staticDescription)
399 .setDynamicValue(
400 DynamicString.newBuilder()
401 .setStateSource(
402 StateStringSource.newBuilder()
403 .setSourceKey("content")
404 .build())
405 .build())
406 .build();
407 StringProp stateDescriptionProp =
408 string(staticDescription)
409 .setDynamicValue(
410 DynamicString.newBuilder()
411 .setStateSource(
412 StateStringSource.newBuilder()
413 .setSourceKey("state")
414 .build())
415 .build())
416 .build();
417
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000418 Semantics semantics =
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000419 Semantics.newBuilder()
420 .setStateDescription(stateDescriptionProp)
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000421 .setContentDescription(contentDescriptionProp)
422 .build();
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000423 LayoutElement root =
424 LayoutElement.newBuilder()
425 .setText(
426 Text.newBuilder()
427 .setText(string(textContents))
428 .setModifiers(
429 Modifiers.newBuilder().setSemantics(semantics)))
430 .build();
431
432 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
433
434 // Check that there's a text element in the layout...
435 assertThat(rootLayout.getChildCount()).isEqualTo(1);
436 assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class);
437
438 TextView tv = (TextView) rootLayout.getChildAt(0);
439
440 AccessibilityNodeInfoCompat info =
441 AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo());
442 assertThat(info.isImportantForAccessibility()).isTrue();
443 assertThat(tv.isImportantForAccessibility()).isTrue();
444 assertThat(info.isFocusable()).isTrue();
445
Xiangyin Ma3d99b132023-04-06 22:03:11 +0000446 AppDataKey<DynamicBuilders.DynamicString> keyContent = new AppDataKey<>("content");
447 AppDataKey<DynamicBuilders.DynamicString> keyState = new AppDataKey<>("state");
Xiangyin Ma8313fb12023-04-12 16:44:11 +0000448 mStateStore.setAppStateEntryValuesProto(
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000449 ImmutableMap.of(
Xiangyin Ma3d99b132023-04-06 22:03:11 +0000450 keyContent,
Xiangyin Ma8313fb12023-04-12 16:44:11 +0000451 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000452 .setStringVal(
453 FixedString.newBuilder()
454 .setValue(initialDynamicContentDescription))
455 .build(),
Xiangyin Ma3d99b132023-04-06 22:03:11 +0000456 keyState,
Xiangyin Ma8313fb12023-04-12 16:44:11 +0000457 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000458 .setStringVal(
459 FixedString.newBuilder()
460 .setValue(initialDynamicStateDescription))
461 .build()));
462
463 info = AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo());
Xiangyin Ma8313fb12023-04-12 16:44:11 +0000464 assertThat(mStateStore.getDynamicDataValuesProto(keyContent).getStringVal().getValue())
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000465 .isEqualTo(initialDynamicContentDescription);
466 assertThat(info.getContentDescription().toString())
467 .isEqualTo(initialDynamicContentDescription);
468 assertThat(info.getStateDescription().toString()).isEqualTo(initialDynamicStateDescription);
469
Xiangyin Ma8313fb12023-04-12 16:44:11 +0000470 mStateStore.setAppStateEntryValuesProto(
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000471 ImmutableMap.of(
Xiangyin Ma3d99b132023-04-06 22:03:11 +0000472 keyContent,
Xiangyin Ma8313fb12023-04-12 16:44:11 +0000473 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000474 .setStringVal(
475 FixedString.newBuilder()
476 .setValue(targetDynamicContentDescription))
477 .build(),
Xiangyin Ma3d99b132023-04-06 22:03:11 +0000478 keyState,
Xiangyin Ma8313fb12023-04-12 16:44:11 +0000479 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000480 .setStringVal(
481 FixedString.newBuilder()
482 .setValue(targetDynamicStateDescription))
483 .build()));
484
485 info = AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo());
486 assertThat(info.getContentDescription().toString())
487 .isEqualTo(targetDynamicContentDescription);
488 assertThat(info.getStateDescription().toString()).isEqualTo(targetDynamicStateDescription);
489 }
490
491 @Test
492 public void inflate_box_withIllegalSize() {
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000493 LayoutElement textElement =
494 LayoutElement.newBuilder()
495 .setText(Text.newBuilder().setText(string("foo")))
496 .build();
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000497 LayoutElement root =
498 LayoutElement.newBuilder()
499 .setBox(
500 Box.newBuilder()
501 // Outer box's width and height left at default value of
502 // "wrap"
503 .addContents(
504 LayoutElement.newBuilder()
505 .setBox(
506 // Inner box's width set to
507 // "expand". Having a single
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000508 // "expand"
509 // element in a "wrap" element is an
510 // undefined state, so the outer box
511 // should not be displayed.
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000512 Box.newBuilder()
513 .setWidth(expand())
514 .addContents(textElement))))
515 .build();
516
517 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
518
519 // Check that the outer box is not displayed.
520 assertThat(rootLayout.getChildCount()).isEqualTo(0);
521 }
522
523 @Test
524 public void inflate_box_withSemanticsModifier() {
525 String textDescription = "this is a button";
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000526 Semantics semantics =
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000527 Semantics.newBuilder()
528 .setContentDescription(string(textDescription))
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000529 .setRole(SemanticsRole.SEMANTICS_ROLE_BUTTON)
530 .build();
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000531 String text = "some button";
532 LayoutElement root =
533 LayoutElement.newBuilder()
534 .setBox(
535 Box.newBuilder()
536 .addContents(
537 LayoutElement.newBuilder()
538 .setText(
539 Text.newBuilder()
540 .setText(string(text))))
541 .setModifiers(
542 Modifiers.newBuilder().setSemantics(semantics)))
543 .build();
544
545 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
546
547 assertThat(rootLayout.getChildCount()).isEqualTo(1);
548 View button = rootLayout.getChildAt(0);
549 AccessibilityNodeInfoCompat info =
550 AccessibilityNodeInfoCompat.wrap(button.createAccessibilityNodeInfo());
551 expect.that(info.getContentDescription().toString()).contains(textDescription);
552 expect.that(info.getClassName().toString()).contains("android.widget.Button");
553 expect.that(info.isImportantForAccessibility()).isTrue();
554 assertThat(button.isImportantForAccessibility()).isTrue();
555 }
556
557 @Test
558 public void inflate_box_withSemanticsStateDescription() {
559 String textDescription = "this is a switch";
560 String offState = "off";
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000561 Semantics semantics =
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000562 Semantics.newBuilder()
563 .setContentDescription(string(textDescription))
564 .setStateDescription(string(offState))
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000565 .setRole(SemanticsRole.SEMANTICS_ROLE_SWITCH)
566 .build();
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000567 String text = "a switch";
568 LayoutElement root =
569 LayoutElement.newBuilder()
570 .setBox(
571 Box.newBuilder()
572 .addContents(
573 LayoutElement.newBuilder()
574 .setText(
575 Text.newBuilder()
576 .setText(string(text))))
577 .setModifiers(
578 Modifiers.newBuilder().setSemantics(semantics)))
579 .build();
580
581 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
582
583 assertThat(rootLayout.getChildCount()).isEqualTo(1);
584 View switchView = rootLayout.getChildAt(0);
585 AccessibilityNodeInfoCompat info =
586 AccessibilityNodeInfoCompat.wrap(switchView.createAccessibilityNodeInfo());
587 expect.that(info.getContentDescription().toString()).contains(textDescription);
588 expect.that(info.getStateDescription().toString()).contains(offState);
589 expect.that(info.getClassName().toString()).contains("android.widget.Switch");
590 expect.that(info.isImportantForAccessibility()).isTrue();
591 assertThat(switchView.isImportantForAccessibility()).isTrue();
592 }
593
594 @Test
595 public void inflate_spacer() {
596 int width = 10;
597 int height = 20;
598 LayoutElement root =
599 LayoutElement.newBuilder()
600 .setSpacer(
601 Spacer.newBuilder()
602 .setHeight(
603 SpacerDimension.newBuilder()
604 .setLinearDimension(dp(height)))
605 .setWidth(
606 SpacerDimension.newBuilder()
607 .setLinearDimension(dp(width))))
608 .build();
609
610 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
611
612 // Check that there's a single element in the layout...
613 assertThat(rootLayout.getChildCount()).isEqualTo(1);
614 View tv = rootLayout.getChildAt(0);
615
616 // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine.
617 expect.that(tv.getMeasuredWidth()).isEqualTo(width);
618 expect.that(tv.getMeasuredHeight()).isEqualTo(height);
619 }
620
621 @Test
622 public void inflate_spacerWithModifiers() {
623 int width = 10;
624 int height = 20;
625 LayoutElement root =
626 LayoutElement.newBuilder()
627 .setSpacer(
628 Spacer.newBuilder()
629 .setModifiers(
630 Modifiers.newBuilder()
631 .setBorder(
632 Border.newBuilder()
633 .setWidth(dp(2))
634 .build()))
635 .setHeight(
636 SpacerDimension.newBuilder()
637 .setLinearDimension(dp(height)))
638 .setWidth(
639 SpacerDimension.newBuilder()
640 .setLinearDimension(dp(width))))
641 .build();
642
643 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
644
645 // Check that there's a single element in the layout...
646 assertThat(rootLayout.getChildCount()).isEqualTo(1);
647 View tv = rootLayout.getChildAt(0);
648
649 // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine.
650 expect.that(tv.getMeasuredWidth()).isEqualTo(width);
651 expect.that(tv.getMeasuredHeight()).isEqualTo(height);
652 }
653
654 @Test
655 public void inflate_image_withoutDimensions() {
656 // Must match a resource ID in buildResources
657 String protoResId = "android";
658
659 LayoutElement root =
660 LayoutElement.newBuilder()
661 .setImage(Image.newBuilder().setResourceId(string(protoResId)))
662 .build();
663
664 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
665
666 // An image without dimensions will not be displayed.
667 assertThat(rootLayout.getChildCount()).isEqualTo(0);
668 }
669
670 @Test
671 public void inflate_image_withDimensions() {
672 // Must match a resource ID in buildResources
673 String protoResId = "android";
674
675 LayoutElement root = buildImage(protoResId, 30, 20);
676
677 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
678
679 RatioViewWrapper iv = (RatioViewWrapper) rootLayout.getChildAt(0);
680
681 expect.that(iv.getMeasuredWidth()).isEqualTo(30);
682 expect.that(iv.getMeasuredHeight()).isEqualTo(20);
683 }
684
685 @Test
686 public void inflate_image_withInvalidRatio() {
687 LayoutElement root =
688 LayoutElement.newBuilder()
689 .setImage(
690 Image.newBuilder()
691 .setHeight(
692 ImageDimension.newBuilder()
693 .setProportionalDimension(
694 ProportionalDimensionProp
695 .getDefaultInstance()))
696 .setWidth(expandImage()))
697 .build();
698
699 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
700
701 // An image with invalid ratio will not be displayed.
702 assertThat(rootLayout.getChildCount()).isEqualTo(0);
703 }
704
705 @Test
706 public void inflate_image_byName() {
707 // Must match a resource ID in buildResources
708 String protoResId = "android_image_by_name";
709
710 LayoutElement root = buildImage(protoResId, 30, 20);
711
712 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
713 RatioViewWrapper iv = (RatioViewWrapper) rootLayout.getChildAt(0);
714
715 expect.that(iv.getMeasuredWidth()).isEqualTo(30);
716 expect.that(iv.getMeasuredHeight()).isEqualTo(20);
717 }
718
719 @Test
720 public void inflate_clickableModifier_withLaunchAction() throws IOException {
721 final String packageName = "com.foo.protolayout.test";
722 final String className = "com.foo.protolayout.test.TestActivity";
723 final String textContents = "I am a clickable";
724
725 // Register the activity so the intent can be resolved.
726 ComponentName cn = new ComponentName(packageName, className);
727 ShadowPackageManager pkgManager = shadowOf(getApplicationContext().getPackageManager());
728 ActivityInfo ai = pkgManager.addActivityIfNotPresent(cn);
729 ai.exported = true;
730
731 String stringVal = "foobar";
732 int int32Val = 123;
733 long int64Val = 1234567890123456789L;
734 double doubleVal = 0.1234;
735
736 LaunchAction launchAction =
737 LaunchAction.newBuilder()
738 .setAndroidActivity(
739 AndroidActivity.newBuilder()
740 .setPackageName(packageName)
741 .setClassName(className)
742 .putKeyToExtra(
743 "stringValue",
744 AndroidExtra.newBuilder()
745 .setStringVal(
746 AndroidStringExtra.newBuilder()
747 .setValue(stringVal))
748 .build())
749 .putKeyToExtra(
750 "int32Value",
751 AndroidExtra.newBuilder()
752 .setIntVal(
753 AndroidIntExtra.newBuilder()
754 .setValue(int32Val))
755 .build())
756 .putKeyToExtra(
757 "int64Value",
758 AndroidExtra.newBuilder()
759 .setLongVal(
760 AndroidLongExtra.newBuilder()
761 .setValue(int64Val))
762 .build())
763 .putKeyToExtra(
764 "doubleValue",
765 AndroidExtra.newBuilder()
766 .setDoubleVal(
767 AndroidDoubleExtra.newBuilder()
768 .setValue(doubleVal))
769 .build())
770 .putKeyToExtra(
771 "boolValue",
772 AndroidExtra.newBuilder()
773 .setBooleanVal(
774 AndroidBooleanExtra.newBuilder()
775 .setValue(true))
776 .build()))
777 .build();
778
779 Action action = Action.newBuilder().setLaunchAction(launchAction).build();
780
781 LayoutElement textElement =
782 LayoutElement.newBuilder()
783 .setText(
784 Text.newBuilder()
785 .setText(string(textContents))
786 .setModifiers(
787 Modifiers.newBuilder()
788 .setClickable(
789 Clickable.newBuilder()
790 .setId("foo")
791 .setOnClick(action))))
792 .build();
793
794 FrameLayout rootLayout = renderer(fingerprintedLayout(textElement)).inflate();
795
796 // Should be just a text view as the root.
797 assertThat(rootLayout.getChildCount()).isEqualTo(1);
798 assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class);
799
800 TextView tv = (TextView) rootLayout.getChildAt(0);
801
802 // The clickable view must have the same tag as the corresponding layout clickable.
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000803 expect.that(tv.getTag(clickable_id_tag)).isEqualTo("foo");
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000804
805 // Ensure that the text still went through properly.
806 expect.that(tv.getText().toString()).isEqualTo(textContents);
807
808 // Try and fire the intent.
809 tv.performClick();
810
811 Intent firedIntent =
812 shadowOf((Application) getApplicationContext()).getNextStartedActivity();
813 expect.that(firedIntent.getComponent()).isEqualTo(cn);
814 expect.that(firedIntent.getStringExtra("stringValue")).isEqualTo(stringVal);
815 expect.that(firedIntent.getIntExtra("int32Value", 0)).isEqualTo(int32Val);
816 expect.that(firedIntent.getLongExtra("int64Value", 0)).isEqualTo(int64Val);
817 expect.that(firedIntent.getDoubleExtra("doubleValue", 0)).isEqualTo(doubleVal);
818 expect.that(firedIntent.getBooleanExtra("boolValue", false)).isEqualTo(true);
819 }
820
821 @Test
822 public void inflate_clickableModifier_withLaunchAction_notExportedIsNotOp() {
823 final String packageName = "com.foo.protolayout.test";
824 final String className = "com.foo.protolayout.test.TestActivity";
825 final String textContents = "I am a clickable";
826
827 // Register the activity so the intent can be resolved.
828 ComponentName cn = new ComponentName(packageName, className);
829 ShadowPackageManager pkgManager = shadowOf(getApplicationContext().getPackageManager());
830 ActivityInfo ai = pkgManager.addActivityIfNotPresent(cn);
831
832 // Activity is not exported. Renderer shouldn't even try and call it.
833 ai.exported = false;
834
835 LaunchAction launchAction =
836 LaunchAction.newBuilder()
837 .setAndroidActivity(
838 AndroidActivity.newBuilder()
839 .setPackageName(packageName)
840 .setClassName(className))
841 .build();
842
843 Action action = Action.newBuilder().setLaunchAction(launchAction).build();
844
845 LayoutElement root =
846 LayoutElement.newBuilder()
847 .setText(
848 Text.newBuilder()
849 .setText(string(textContents))
850 .setModifiers(
851 Modifiers.newBuilder()
852 .setClickable(
853 Clickable.newBuilder()
854 .setId("foo")
855 .setOnClick(action))))
856 .build();
857
858 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
859
860 // Should be just a text view as the root.
861 assertThat(rootLayout.getChildCount()).isEqualTo(1);
862 assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class);
863
864 TextView tv = (TextView) rootLayout.getChildAt(0);
865
866 shadowOf((Application) getApplicationContext()).clearNextStartedActivities();
867
868 // Try and fire the intent.
869 tv.performClick();
870
871 expect.that(shadowOf((Application) getApplicationContext()).getNextStartedActivity())
872 .isNull();
873 }
874
875 @Test
876 public void inflate_clickableModifier_withLaunchAction_requiresPermissionIsNoOp() {
877 final String packageName = "com.foo.protolayout.test";
878 final String className = "com.foo.protolayout.test.TestActivity";
879 final String textContents = "I am a clickable";
880
881 // Register the activity so the intent can be resolved.
882 ComponentName cn = new ComponentName(packageName, className);
883 ShadowPackageManager pkgManager = shadowOf(getApplicationContext().getPackageManager());
884 ActivityInfo ai = pkgManager.addActivityIfNotPresent(cn);
885
886 // Activity has a permission associated with it; shouldn't be called.
887 ai.exported = true;
888 ai.permission = "android.MY_SENSITIVE_PERMISSION";
889
890 LaunchAction launchAction =
891 LaunchAction.newBuilder()
892 .setAndroidActivity(
893 AndroidActivity.newBuilder()
894 .setPackageName(packageName)
895 .setClassName(className))
896 .build();
897
898 Action action = Action.newBuilder().setLaunchAction(launchAction).build();
899
900 LayoutElement root =
901 LayoutElement.newBuilder()
902 .setText(
903 Text.newBuilder()
904 .setText(string(textContents))
905 .setModifiers(
906 Modifiers.newBuilder()
907 .setClickable(
908 Clickable.newBuilder()
909 .setId("foo")
910 .setOnClick(action))))
911 .build();
912
913 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
914
915 // Should be just a text view as the root.
916 assertThat(rootLayout.getChildCount()).isEqualTo(1);
917 assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class);
918
919 TextView tv = (TextView) rootLayout.getChildAt(0);
920
921 shadowOf((Application) getApplicationContext()).clearNextStartedActivities();
922
923 // Try and fire the intent.
924 tv.performClick();
925
926 expect.that(shadowOf((Application) getApplicationContext()).getNextStartedActivity())
927 .isNull();
928 }
929
930 @Test
931 public void inflate_clickableModifier_withLoadAction() {
932 final String textContents = "I am a clickable";
933
934 Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build();
935
936 LayoutElement root =
937 LayoutElement.newBuilder()
938 .setText(
939 Text.newBuilder()
940 .setText(string(textContents))
941 .setModifiers(
942 Modifiers.newBuilder()
943 .setClickable(
944 Clickable.newBuilder()
945 .setId("foo")
946 .setOnClick(action))))
947 .build();
948
949 State.Builder receivedState = State.newBuilder();
950 FrameLayout rootLayout =
951 renderer(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000952 newRendererConfigBuilder(
953 fingerprintedLayout(root), resourceResolvers())
954 .setLoadActionListener(receivedState::mergeFrom))
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000955 .inflate();
956
957 // Should be just a text view as the root.
958 assertThat(rootLayout.getChildCount()).isEqualTo(1);
959 assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class);
960
961 TextView tv = (TextView) rootLayout.getChildAt(0);
962
963 // The clickable view must have the same tag as the corresponding layout clickable.
Neda Topoljanac3a83ae72023-04-05 20:48:11 +0000964 expect.that(tv.getTag(clickable_id_tag)).isEqualTo("foo");
Neda Topoljanac4d26f822023-02-22 16:57:03 +0000965
966 // Ensure that the text still went through properly.
967 expect.that(tv.getText().toString()).isEqualTo(textContents);
968
969 // Try and fire the intent.
970 tv.performClick();
971 shadowOf(Looper.getMainLooper()).idle();
972
973 expect.that(receivedState.getLastClickableId()).isEqualTo("foo");
974 }
975
976 @Test
977 public void inflate_clickableModifier_withAndroidActivity_hasSourceBounds() {
978 final String packageName = "com.foo.protolayout.test";
979 final String className = "com.foo.protolayout.test.TestActivity";
980 final String textContents = "I am a clickable";
981
982 // Register the activity so the intent can be resolved.
983 ComponentName cn = new ComponentName(packageName, className);
984 ShadowPackageManager pkgManager = shadowOf(getApplicationContext().getPackageManager());
985 ActivityInfo ai = pkgManager.addActivityIfNotPresent(cn);
986 ai.exported = true;
987
988 LaunchAction launchAction =
989 LaunchAction.newBuilder()
990 .setAndroidActivity(
991 AndroidActivity.newBuilder()
992 .setPackageName(packageName)
993 .setClassName(className))
994 .build();
995
996 Action action = Action.newBuilder().setLaunchAction(launchAction).build();
997
998 LayoutElement textElement =
999 LayoutElement.newBuilder()
1000 .setText(
1001 Text.newBuilder()
1002 .setText(string(textContents))
1003 .setModifiers(
1004 Modifiers.newBuilder()
1005 .setClickable(
1006 Clickable.newBuilder()
1007 .setId("foo")
1008 .setOnClick(action))))
1009 .build();
1010
1011 FrameLayout rootLayout = renderer(fingerprintedLayout(textElement)).inflate();
1012
1013 // Need to run a layout / measure pass so that the Text element has some bounds...
1014 rootLayout.measure(
1015 MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, MeasureSpec.EXACTLY),
1016 MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, MeasureSpec.EXACTLY));
1017 rootLayout.layout(0, 0, rootLayout.getMeasuredWidth(), rootLayout.getMeasuredHeight());
1018
1019 TextView tv = (TextView) rootLayout.getChildAt(0);
1020 tv.performClick();
1021
1022 Intent firedIntent =
1023 shadowOf((Application) getApplicationContext()).getNextStartedActivity();
1024
1025 int[] screenLocation = new int[2];
1026 tv.getLocationOnScreen(screenLocation);
1027 Rect screenLocationRect =
1028 new Rect(
1029 /* left= */ screenLocation[0],
1030 /* top= */ screenLocation[1],
1031 /* right= */ screenLocation[0] + tv.getWidth(),
1032 /* bottom= */ screenLocation[1] + tv.getHeight());
1033
1034 expect.that(firedIntent.getSourceBounds()).isEqualTo(screenLocationRect);
1035 }
1036
1037 @Test
1038 public void inflate_arc_withLineDrawnWithArcTo() {
1039 LayoutElement root =
1040 LayoutElement.newBuilder()
1041 .setArc(
1042 Arc.newBuilder()
1043 .setAnchorAngle(degrees(0).build())
1044 .addContents(
1045 ArcLayoutElement.newBuilder()
1046 .setLine(
1047 ArcLine.newBuilder()
1048 // Shorter than 360 degrees,
1049 // so should be drawn as an
1050 // arc:
1051 .setLength(degrees(30))
1052 .setStrokeCap(
1053 strokeCapButt())
1054 .setThickness(dp(12)))))
1055 .build();
1056
1057 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1058
1059 assertThat(rootLayout.getChildCount()).isEqualTo(1);
1060 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
1061 assertThat(arcLayout.getChildCount()).isEqualTo(1);
1062 WearCurvedLineView line = (WearCurvedLineView) arcLayout.getChildAt(0);
1063 assertThat(line.getSweepAngleDegrees()).isEqualTo(30);
1064 assertThat(line.getStrokeCap()).isEqualTo(Cap.BUTT);
1065 // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine:
1066 assertThat(line.getThickness()).isEqualTo(12);
1067 }
1068
1069 @Test
1070 public void inflate_arc_withLineDrawnWithAddOval() {
1071 LayoutElement root =
1072 LayoutElement.newBuilder()
1073 .setArc(
1074 Arc.newBuilder()
1075 .setAnchorAngle(degrees(0).build())
1076 .addContents(
1077 ArcLayoutElement.newBuilder()
1078 .setLine(
1079 ArcLine.newBuilder()
1080 // Longer than 360 degrees,
1081 // so should be drawn as an
1082 // oval:
1083 .setLength(degrees(500))
1084 .setThickness(dp(12)))))
1085 .build();
1086
1087 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1088
1089 assertThat(rootLayout.getChildCount()).isEqualTo(1);
1090 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
1091 assertThat(arcLayout.getChildCount()).isEqualTo(1);
1092 WearCurvedLineView line = (WearCurvedLineView) arcLayout.getChildAt(0);
1093 assertThat(line.getSweepAngleDegrees()).isEqualTo(500);
1094 // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine:
1095 assertThat(line.getThickness()).isEqualTo(12);
1096 }
1097
1098 @Test
1099 public void inflate_arc_withText() {
1100 LayoutElement root =
1101 LayoutElement.newBuilder()
1102 .setArc(
1103 Arc.newBuilder()
1104 .setAnchorAngle(degrees(0).build())
1105 .addContents(
1106 ArcLayoutElement.newBuilder()
1107 .setText(
1108 ArcText.newBuilder()
1109 .setText(string("text1"))))
1110 .addContents(
1111 ArcLayoutElement.newBuilder()
1112 .setText(
1113 ArcText.newBuilder()
1114 .setText(string("text2")))))
1115 .build();
1116
1117 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1118
1119 assertThat(rootLayout.getChildCount()).isEqualTo(1);
1120 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
1121 assertThat(arcLayout.getChildCount()).isEqualTo(2);
1122 CurvedTextView textView1 = (CurvedTextView) arcLayout.getChildAt(0);
1123 assertThat(textView1.getText()).isEqualTo("text1");
1124 CurvedTextView textView2 = (CurvedTextView) arcLayout.getChildAt(1);
1125 assertThat(textView2.getText()).isEqualTo("text2");
1126 }
1127
1128 @Test
Neda Topoljanac742e9e72023-10-06 22:18:35 +00001129 public void inflate_arc_withText_autoSize_notSet() {
1130 int lastSize = 12;
1131 FontStyle.Builder style = FontStyle.newBuilder()
1132 .addAllSize(buildSizesList(new int[]{10, 20, lastSize}));
1133 LayoutElement root =
1134 LayoutElement.newBuilder()
1135 .setArc(
1136 Arc.newBuilder()
1137 .setAnchorAngle(degrees(0).build())
1138 .addContents(
1139 ArcLayoutElement.newBuilder()
1140 .setText(
1141 ArcText.newBuilder()
1142 .setText(string("text1"))
1143 .setFontStyle(style))))
1144 .build();
1145
1146 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1147 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
1148 CurvedTextView tv = (CurvedTextView) arcLayout.getChildAt(0);
1149 assertThat(tv.getText()).isEqualTo("text1");
1150 expect.that(tv.getTextSize()).isEqualTo(lastSize);
1151 }
1152
1153 @Test
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001154 public void inflate_arc_withSpacer() {
1155 LayoutElement root =
1156 LayoutElement.newBuilder()
1157 .setArc(
1158 Arc.newBuilder()
1159 .setAnchorAngle(degrees(0).build())
1160 .addContents(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00001161 arcLayoutElement(
1162 ArcSpacer.newBuilder()
1163 .setLength(degrees(90)))))
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001164 .build();
1165
1166 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1167
1168 assertThat(rootLayout.getChildCount()).isEqualTo(1);
1169 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
1170 assertThat(arcLayout.getChildCount()).isEqualTo(1);
1171 WearCurvedSpacer spacer = (WearCurvedSpacer) arcLayout.getChildAt(0);
1172 assertThat(spacer.getSweepAngleDegrees()).isEqualTo(90);
1173 // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine:
1174 assertThat(spacer.getThickness()).isEqualTo(20);
1175 }
1176
1177 @Test
1178 public void inflate_arc_withMaxAngleAndWeights() {
1179 ArcSpacerLength spacerLength =
1180 ArcSpacerLength.newBuilder()
1181 .setExpandedAngularDimension(expandAngular(1.0f))
1182 .build();
1183 ArcLineLength lineLength =
1184 ArcLineLength.newBuilder().setExpandedAngularDimension(expandAngular(2.0f)).build();
1185 LayoutElement root =
1186 LayoutElement.newBuilder()
1187 .setArc(
1188 Arc.newBuilder()
1189 .setAnchorAngle(degrees(0).build())
1190 .setMaxAngle(DegreesProp.newBuilder().setValue(90f).build())
1191 .addContents(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00001192 arcLayoutElement(
1193 ArcSpacer.newBuilder()
1194 .setAngularLength(spacerLength)))
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001195 .addContents(
1196 ArcLayoutElement.newBuilder()
1197 .setLine(
1198 ArcLine.newBuilder()
1199 .setAngularLength(
1200 lineLength)
1201 .setThickness(dp(12)))))
1202 .build();
1203
1204 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1205
1206 assertThat(rootLayout.getChildCount()).isEqualTo(1);
1207 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
1208 assertThat(arcLayout.isClockwise()).isTrue();
1209 assertThat(arcLayout.getMaxAngleDegrees()).isEqualTo(90f);
1210 assertThat(arcLayout.getChildCount()).isEqualTo(2);
1211
1212 WearCurvedSpacer spacer = (WearCurvedSpacer) arcLayout.getChildAt(0);
1213 assertThat(spacer.getSweepAngleDegrees()).isEqualTo(30f);
1214
1215 WearCurvedLineView line = (WearCurvedLineView) arcLayout.getChildAt(1);
1216 assertThat(line.getSweepAngleDegrees()).isEqualTo(60f);
1217 }
1218
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00001219 @NonNull
1220 private static ArcLayoutElement.Builder arcLayoutElement(ArcSpacer.Builder setAngularLength) {
1221 return ArcLayoutElement.newBuilder().setSpacer(setAngularLength.setThickness(dp(20)));
1222 }
1223
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001224 @Test
1225 public void inflate_row() {
1226 final String protoResId = "android";
1227
1228 LayoutElement image = buildImage(protoResId, 30, 20);
1229
1230 LayoutElement root =
1231 LayoutElement.newBuilder()
1232 .setRow(Row.newBuilder().addContents(image).addContents(image))
1233 .build();
1234
1235 FrameLayout layout = renderer(fingerprintedLayout(root)).inflate();
1236
1237 // There should be a child ViewGroup. Technically, we know it's a LinearLayout, but that
1238 // could change in the future. For now, just ensure that the two images are laid out
1239 // horizontally.
1240 assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class);
1241 ViewGroup firstChild = (ViewGroup) layout.getChildAt(0);
1242
1243 assertThat(firstChild.getChildCount()).isEqualTo(2);
1244 assertThat(firstChild.getChildAt(0)).isInstanceOf(RatioViewWrapper.class);
1245 assertThat(firstChild.getChildAt(1)).isInstanceOf(RatioViewWrapper.class);
1246
1247 RatioViewWrapper child1 = (RatioViewWrapper) firstChild.getChildAt(0);
1248 RatioViewWrapper child2 = (RatioViewWrapper) firstChild.getChildAt(1);
1249
1250 // There's no padding here...
1251 expect.that(child2.getX()).isEqualTo(child1.getX() + child1.getMeasuredWidth());
1252
1253 // In this case, because both children are the same size, they should definitely share the
1254 // same Y coordinate.
1255 expect.that(child1.getY()).isEqualTo(child2.getY());
1256 }
1257
1258 @Test
1259 public void inflate_column() {
1260 final String protoResId = "android";
1261
1262 LayoutElement image = buildImage(protoResId, 30, 20);
1263
1264 LayoutElement root =
1265 LayoutElement.newBuilder()
1266 .setColumn(Column.newBuilder().addContents(image).addContents(image))
1267 .build();
1268
1269 FrameLayout layout = renderer(fingerprintedLayout(root)).inflate();
1270
1271 assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class);
1272 ViewGroup firstChild = (ViewGroup) layout.getChildAt(0);
1273
1274 assertThat(firstChild.getChildCount()).isEqualTo(2);
1275 assertThat(firstChild.getChildAt(0)).isInstanceOf(RatioViewWrapper.class);
1276 assertThat(firstChild.getChildAt(1)).isInstanceOf(RatioViewWrapper.class);
1277
1278 RatioViewWrapper child1 = (RatioViewWrapper) firstChild.getChildAt(0);
1279 RatioViewWrapper child2 = (RatioViewWrapper) firstChild.getChildAt(1);
1280
1281 // There's no padding here...
1282 expect.that(child2.getY()).isEqualTo(child1.getY() + child1.getMeasuredHeight());
1283
1284 // In this case, because both children are the same size, they should definitely share the
1285 // same X coordinate.
1286 expect.that(child1.getX()).isEqualTo(child2.getX());
1287 }
1288
1289 private static LayoutElement buildImage(String protoResId, float widthDp, float heightDp) {
1290 return LayoutElement.newBuilder()
1291 .setImage(
1292 Image.newBuilder()
1293 .setResourceId(string(protoResId))
1294 .setWidth(linImageDim(dp(widthDp)))
1295 .setHeight(linImageDim(dp(heightDp))))
1296 .build();
1297 }
1298
1299 private static LayoutElement buildExampleRowLayoutWithAlignment(VerticalAlignment alignment) {
1300 final String protoResId = "android";
1301
1302 LayoutElement image1 = buildImage(protoResId, 30, 30);
1303 LayoutElement image2 = buildImage(protoResId, 30, 50);
1304
1305 Row row =
1306 Row.newBuilder()
1307 .addContents(image1)
1308 .addContents(image2)
1309 .setVerticalAlignment(
1310 VerticalAlignmentProp.newBuilder().setValue(alignment))
1311 .build();
1312
1313 // Gravity = top.
1314 return LayoutElement.newBuilder().setRow(row).build();
1315 }
1316
1317 @Test
1318 public void inflate_row_withGravity() {
1319 Map<VerticalAlignment, Integer> expectedY =
1320 ImmutableMap.of(
1321 VerticalAlignment.VERTICAL_ALIGN_TOP, 0,
1322 VerticalAlignment.VERTICAL_ALIGN_CENTER, 10,
1323 VerticalAlignment.VERTICAL_ALIGN_BOTTOM, 20);
1324
1325 for (Map.Entry<VerticalAlignment, Integer> entry : expectedY.entrySet()) {
1326 LayoutElement root = buildExampleRowLayoutWithAlignment(entry.getKey());
1327 FrameLayout topFrameLayout = renderer(fingerprintedLayout(root)).inflate();
1328 ViewGroup topViewGroup = (ViewGroup) topFrameLayout.getChildAt(0);
1329 RatioViewWrapper image1 = (RatioViewWrapper) topViewGroup.getChildAt(0);
1330 RatioViewWrapper image2 = (RatioViewWrapper) topViewGroup.getChildAt(1);
1331
1332 // Image 1 is the smaller of the two, so its Y coordinate should move accordingly.
1333 expect.that(image1.getY()).isEqualTo(image2.getY() + entry.getValue());
1334 }
1335 }
1336
1337 private static LayoutElement buildExampleColumnLayoutWithAlignment(
1338 HorizontalAlignment alignment) {
1339 final String resName = "android";
1340
1341 LayoutElement image1 =
1342 LayoutElement.newBuilder()
1343 .setImage(
1344 Image.newBuilder()
1345 .setResourceId(string(resName))
1346 .setWidth(linImageDim(dp(30)))
1347 .setHeight(linImageDim(dp(30))))
1348 .build();
1349
1350 LayoutElement image2 =
1351 LayoutElement.newBuilder()
1352 .setImage(
1353 Image.newBuilder()
1354 .setResourceId(string(resName))
1355 .setWidth(linImageDim(dp(50)))
1356 .setHeight(linImageDim(dp(30))))
1357 .build();
1358
1359 Column column =
1360 Column.newBuilder()
1361 .addContents(image1)
1362 .addContents(image2)
1363 .setHorizontalAlignment(
1364 HorizontalAlignmentProp.newBuilder().setValue(alignment))
1365 .build();
1366
1367 // Gravity = top.
1368 return LayoutElement.newBuilder().setColumn(column).build();
1369 }
1370
1371 @Test
1372 public void inflate_column_withGravity() {
1373 Map<HorizontalAlignment, Integer> expectedX =
1374 ImmutableMap.of(
1375 HorizontalAlignment.HORIZONTAL_ALIGN_START, 0,
1376 HorizontalAlignment.HORIZONTAL_ALIGN_CENTER, 10,
1377 HorizontalAlignment.HORIZONTAL_ALIGN_END, 20);
1378
1379 for (Map.Entry<HorizontalAlignment, Integer> entry : expectedX.entrySet()) {
1380 LayoutElement root = buildExampleColumnLayoutWithAlignment(entry.getKey());
1381 FrameLayout topFrameLayout = renderer(fingerprintedLayout(root)).inflate();
1382 ViewGroup topViewGroup = (ViewGroup) topFrameLayout.getChildAt(0);
1383 RatioViewWrapper image1 = (RatioViewWrapper) topViewGroup.getChildAt(0);
1384 RatioViewWrapper image2 = (RatioViewWrapper) topViewGroup.getChildAt(1);
1385
1386 // Image 1 is the smaller of the two, so its X coordinate should move accordingly.
1387 expect.that(image1.getX()).isEqualTo(image2.getX() + entry.getValue());
1388 }
1389 }
1390
1391 private static LayoutElement buildExampleContainerLayoutWithAlignment(
1392 HorizontalAlignment hAlign, VerticalAlignment vAlign) {
1393 final String resName = "android";
1394
1395 LayoutElement image1 =
1396 LayoutElement.newBuilder()
1397 .setImage(
1398 Image.newBuilder()
1399 .setResourceId(string(resName))
1400 .setWidth(linImageDim(dp(30)))
1401 .setHeight(linImageDim(dp(30))))
1402 .build();
1403
1404 LayoutElement image2 =
1405 LayoutElement.newBuilder()
1406 .setImage(
1407 Image.newBuilder()
1408 .setResourceId(string(resName))
1409 .setWidth(linImageDim(dp(50)))
1410 .setHeight(linImageDim(dp(50))))
1411 .build();
1412
1413 Box box =
1414 Box.newBuilder()
1415 .addContents(image1)
1416 .addContents(image2)
1417 .setVerticalAlignment(VerticalAlignmentProp.newBuilder().setValue(vAlign))
1418 .setHorizontalAlignment(
1419 HorizontalAlignmentProp.newBuilder().setValue(hAlign))
1420 .build();
1421
1422 // Gravity = top.
1423 return LayoutElement.newBuilder().setBox(box).build();
1424 }
1425
1426 @Test
1427 public void inflate_stack_withAlignment() {
1428 Map<HorizontalAlignment, Integer> expectedX =
1429 ImmutableMap.of(
1430 HorizontalAlignment.HORIZONTAL_ALIGN_START, 0,
1431 HorizontalAlignment.HORIZONTAL_ALIGN_CENTER, 10,
1432 HorizontalAlignment.HORIZONTAL_ALIGN_END, 20);
1433
1434 Map<VerticalAlignment, Integer> expectedY =
1435 ImmutableMap.of(
1436 VerticalAlignment.VERTICAL_ALIGN_TOP, 0,
1437 VerticalAlignment.VERTICAL_ALIGN_CENTER, 10,
1438 VerticalAlignment.VERTICAL_ALIGN_BOTTOM, 20);
1439
1440 for (Map.Entry<HorizontalAlignment, Integer> hEntry : expectedX.entrySet()) {
1441 for (Map.Entry<VerticalAlignment, Integer> vEntry : expectedY.entrySet()) {
1442 LayoutElement root =
1443 buildExampleContainerLayoutWithAlignment(hEntry.getKey(), vEntry.getKey());
1444 FrameLayout topFrameLayout = renderer(fingerprintedLayout(root)).inflate();
1445
1446 ViewGroup topViewGroup = (ViewGroup) topFrameLayout.getChildAt(0);
1447 RatioViewWrapper image1 = (RatioViewWrapper) topViewGroup.getChildAt(0);
1448 RatioViewWrapper image2 = (RatioViewWrapper) topViewGroup.getChildAt(1);
1449
1450 // Image 1 is the smaller of the two, so its coordinates should move accordingly.
1451 expect.that(image1.getX()).isEqualTo(image2.getX() + hEntry.getValue());
1452 expect.that(image1.getY()).isEqualTo(image2.getY() + vEntry.getValue());
1453 }
1454 }
1455 }
1456
1457 @Test
1458 public void inflate_layoutElement_noChild() {
1459 // Just an empty layout. This is just to ensure that the renderer doesn't crash with a
1460 // "barely valid" proto.
1461 LayoutElement root = LayoutElement.getDefaultInstance();
1462
1463 renderer(fingerprintedLayout(root)).inflate();
1464 }
1465
1466 @Test
1467 public void buildClickableIntent_setsPackageName() {
1468 LaunchAction launchAction =
1469 LaunchAction.newBuilder()
1470 .setAndroidActivity(
1471 AndroidActivity.newBuilder()
1472 .setClassName(TEST_CLICKABLE_CLASS_NAME)
1473 .setPackageName(TEST_CLICKABLE_PACKAGE_NAME))
1474 .build();
1475
1476 Intent i =
1477 ProtoLayoutInflater.buildLaunchActionIntent(launchAction, "", EXTRA_CLICKABLE_ID);
1478
1479 expect.that(i.getComponent().getClassName()).isEqualTo(TEST_CLICKABLE_CLASS_NAME);
1480 expect.that(i.getComponent().getPackageName()).isEqualTo(TEST_CLICKABLE_PACKAGE_NAME);
1481 }
1482
1483 @Test
1484 public void buildClickableIntent_launchAction_containsClickableId() {
1485 String testId = "HELLOWORLD";
1486
1487 LaunchAction launchAction =
1488 LaunchAction.newBuilder()
1489 .setAndroidActivity(
1490 AndroidActivity.newBuilder()
1491 .setClassName(TEST_CLICKABLE_CLASS_NAME)
1492 .setPackageName(TEST_CLICKABLE_PACKAGE_NAME))
1493 .build();
1494
1495 Intent i =
1496 ProtoLayoutInflater.buildLaunchActionIntent(
1497 launchAction, testId, EXTRA_CLICKABLE_ID);
1498
1499 expect.that(i.getStringExtra(EXTRA_CLICKABLE_ID)).isEqualTo(testId);
1500 }
1501
1502 @Test
1503 public void buildClickableIntent_noClickableExtraIfNotSet() {
1504 LaunchAction launchAction =
1505 LaunchAction.newBuilder()
1506 .setAndroidActivity(
1507 AndroidActivity.newBuilder()
1508 .setClassName(TEST_CLICKABLE_CLASS_NAME)
1509 .setPackageName(TEST_CLICKABLE_PACKAGE_NAME))
1510 .build();
1511
1512 Intent i =
1513 ProtoLayoutInflater.buildLaunchActionIntent(launchAction, "", EXTRA_CLICKABLE_ID);
1514
1515 expect.that(i.hasExtra(EXTRA_CLICKABLE_ID)).isFalse();
1516 }
1517
1518 @Test
1519 public void inflate_imageView_noResourceId() {
1520 LayoutElement root =
1521 LayoutElement.newBuilder().setImage(Image.getDefaultInstance()).build();
1522
1523 renderer(fingerprintedLayout(root)).inflate();
1524 }
1525
1526 @Test
1527 public void inflate_imageView_resourceHasNoAndroidResource() {
1528 LayoutElement root =
1529 LayoutElement.newBuilder()
1530 .setImage(
1531 Image.newBuilder().setResourceId(string("no_android_resource_set")))
1532 .build();
1533
1534 renderer(fingerprintedLayout(root)).inflate();
1535 }
1536
1537 @Test
1538 public void inflate_imageView_androidResourceDoesNotExist() {
1539 LayoutElement root =
1540 LayoutElement.newBuilder()
1541 .setImage(Image.newBuilder().setResourceId(string("does_not_exist")))
1542 .build();
1543
1544 renderer(fingerprintedLayout(root)).inflate();
1545 }
1546
1547 @Test
1548 public void inflate_imageView_resourceReferenceDoesNotExist() {
1549 LayoutElement root =
1550 LayoutElement.newBuilder()
1551 .setImage(Image.newBuilder().setResourceId(string("aaaaaaaaaaaaaa")))
1552 .build();
1553
1554 renderer(fingerprintedLayout(root)).inflate();
1555 }
1556
1557 @Test
1558 public void inflate_imageView_expandsToParentEvenWhenImageBitmapIsNotSet() {
1559 LayoutElement root =
1560 LayoutElement.newBuilder()
1561 .setImage(
1562 Image.newBuilder()
1563 .setResourceId(string("invalid"))
1564 .setHeight(expandImage())
1565 .setWidth(expandImage()))
1566 .build();
1567
1568 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1569
1570 RatioViewWrapper iv = (RatioViewWrapper) rootLayout.getChildAt(0);
1571
1572 expect.that(iv.getMeasuredWidth()).isEqualTo(SCREEN_WIDTH);
1573 expect.that(iv.getMeasuredHeight()).isEqualTo(SCREEN_HEIGHT);
1574 }
1575
1576 @Test
1577 public void inflate_imageView_expandsToParentContainerEvenWhenImageBitmapIsNotSet() {
1578 LayoutElement root =
1579 LayoutElement.newBuilder()
1580 .setBox(
1581 Box.newBuilder()
1582 .setHeight(expand())
1583 .setWidth(expand())
1584 .setModifiers(
1585 Modifiers.newBuilder()
1586 .setPadding(
1587 Padding.newBuilder()
1588 .setTop(dp(50))))
1589 .addContents(
1590 LayoutElement.newBuilder()
1591 .setImage(
1592 Image.newBuilder()
1593 .setResourceId(
1594 string("invalid"))
1595 .setHeight(expandImage())
1596 .setWidth(expandImage()))))
1597 .build();
1598
1599 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1600 FrameLayout boxLayout = (FrameLayout) rootLayout.getChildAt(0);
1601 RatioViewWrapper iv = (RatioViewWrapper) boxLayout.getChildAt(0);
1602
1603 expect.that(iv.getMeasuredWidth()).isEqualTo(SCREEN_WIDTH);
1604 expect.that(iv.getMeasuredHeight()).isEqualTo(SCREEN_HEIGHT - 50);
1605 }
1606
1607 @Test
1608 public void inflate_imageView_usesDimensionsEvenWhenImageBitmapIsNotSet() {
1609 LayoutElement root =
1610 LayoutElement.newBuilder()
1611 .setImage(
1612 Image.newBuilder()
1613 .setResourceId(string("invalid"))
1614 .setHeight(linImageDim(dp(100)))
1615 .setWidth(linImageDim(dp(100))))
1616 .build();
1617
1618 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1619
1620 RatioViewWrapper iv = (RatioViewWrapper) rootLayout.getChildAt(0);
1621
1622 expect.that(iv.getMeasuredWidth()).isEqualTo(100);
1623 expect.that(iv.getMeasuredHeight()).isEqualTo(100);
1624 }
1625
1626 @Test
1627 public void inflate_imageView_largeImage_isIgnored() {
1628 LayoutElement root =
1629 LayoutElement.newBuilder()
1630 .setImage(
1631 Image.newBuilder()
1632 .setResourceId(string("large_image"))
1633 .setHeight(linImageDim(dp(50)))
1634 .setWidth(linImageDim(dp(50))))
1635 .build();
1636 Resources resources =
1637 Resources.newBuilder()
1638 .putIdToImage(
1639 "large_image",
1640 ImageResource.newBuilder()
1641 .setInlineResource(
1642 InlineImageResource.newBuilder()
1643 .setFormat(ImageFormat.IMAGE_FORMAT_RGB_565)
1644 .setWidthPx(10000)
1645 .setHeightPx(10000)
1646 .setData(
1647 ByteString.copyFrom(
1648 new byte[10000 * 10000])))
1649 .build())
1650 .build();
1651 ResourceResolvers.Builder resourceResolver =
1652 StandardResourceResolvers.forLocalApp(
1653 resources,
1654 getApplicationContext(),
1655 ContextCompat.getMainExecutor(getApplicationContext()),
1656 true);
1657
1658 FrameLayout rootLayout =
1659 renderer(newRendererConfigBuilder(fingerprintedLayout(root), resourceResolver))
1660 .inflate();
1661 shadowOf(getMainLooper()).idle();
1662
1663 RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0);
1664 ImageView iv = (ImageView) rvw.getChildAt(0);
1665 expect.that(rvw.getMeasuredWidth()).isEqualTo(50);
1666 expect.that(rvw.getMeasuredHeight()).isEqualTo(50);
1667 expect.that(iv.getDrawable()).isNull();
1668 expect.that(iv.getMeasuredWidth()).isEqualTo(50);
1669 expect.that(iv.getMeasuredHeight()).isEqualTo(50);
1670 }
1671
1672 @Test
1673 public void inflate_imageView_nonAnimatingAvdResource_noAnimation() {
1674 LayoutElement root =
1675 LayoutElement.newBuilder()
1676 .setImage(
1677 Image.newBuilder()
1678 .setResourceId(
1679 string("android_AVD_pretending_to_be_static"))
1680 .setHeight(linImageDim(dp(50)))
1681 .setWidth(linImageDim(dp(50)))
1682 .build())
1683 .build();
1684
1685 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1686
1687 RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0);
1688 ImageView imageAVD = (ImageView) rvw.getChildAt(0);
1689 Drawable drawableAVD = imageAVD.getDrawable();
1690 assertThat(drawableAVD).isInstanceOf(AnimatedVectorDrawable.class);
1691 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
1692 }
1693
1694 @Test
1695 public void inflate_imageView_withAVDResource() {
1696 LayoutElement root =
1697 LayoutElement.newBuilder()
1698 .setImage(
1699 Image.newBuilder()
1700 .setResourceId(string("android_AVD"))
1701 .setHeight(linImageDim(dp(50)))
1702 .setWidth(linImageDim(dp(50)))
1703 .build())
1704 .build();
1705
1706 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1707
1708 RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0);
1709 ImageView imageAVD = (ImageView) rvw.getChildAt(0);
1710 Drawable drawableAVD = imageAVD.getDrawable();
1711 assertThat(drawableAVD).isInstanceOf(AnimatedVectorDrawable.class);
1712 }
1713
1714 @Test
1715 public void inflate_imageView_withSeekableAVDResource() {
1716 LayoutElement root =
1717 LayoutElement.newBuilder()
1718 .setImage(
1719 Image.newBuilder()
1720 .setResourceId(string("android_seekable_AVD"))
1721 .setHeight(linImageDim(dp(50)))
1722 .setWidth(linImageDim(dp(50)))
1723 .build())
1724 .build();
1725
1726 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1727
1728 RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0);
1729 ImageView imageAVDSeekable = (ImageView) rvw.getChildAt(0);
1730 Drawable drawableAVDSeekable = imageAVDSeekable.getDrawable();
1731 assertThat(drawableAVDSeekable).isInstanceOf(SeekableAnimatedVectorDrawable.class);
1732
Xiangyin Ma8313fb12023-04-12 16:44:11 +00001733 mStateStore.setAppStateEntryValuesProto(
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001734 ImmutableMap.of(
Xiangyin Ma3d99b132023-04-06 22:03:11 +00001735 new AppDataKey<DynamicBuilders.DynamicFloat>("anim_val"),
Xiangyin Ma8313fb12023-04-12 16:44:11 +00001736 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001737 .setFloatVal(FixedFloat.newBuilder().setValue(0.44f))
1738 .build()));
1739 shadowOf(getMainLooper()).idle();
1740 assertThat(((SeekableAnimatedVectorDrawable) drawableAVDSeekable).getCurrentPlayTime())
1741 .isEqualTo(440L);
1742 }
1743
1744 @Test
1745 public void inflate_spannable_imageOccupiesSpace() {
1746 LayoutElement rootWithoutImage =
1747 LayoutElement.newBuilder()
1748 .setSpannable(
1749 Spannable.newBuilder()
1750 .addSpans(textSpan("Foo"))
1751 .addSpans(textSpan("Bar")))
1752 .build();
1753
1754 LayoutElement rootWithImage =
1755 LayoutElement.newBuilder()
1756 .setSpannable(
1757 Spannable.newBuilder()
1758 .addSpans(textSpan("Foo"))
1759 .addSpans(
1760 Span.newBuilder()
1761 .setImage(
1762 SpanImage.newBuilder()
1763 .setResourceId(
1764 string("android"))
1765 .setHeight(dp(50))
1766 .setWidth(dp(50))))
1767 .addSpans(textSpan("Bar")))
1768 .build();
1769
1770 FrameLayout rootLayoutWithoutImage =
1771 renderer(fingerprintedLayout(rootWithoutImage)).inflate();
1772 TextView tvInRootLayoutWithoutImage = (TextView) rootLayoutWithoutImage.getChildAt(0);
1773 FrameLayout rootLayoutWithImage = renderer(fingerprintedLayout(rootWithImage)).inflate();
1774 TextView tvInRootLayoutWithImage = (TextView) rootLayoutWithImage.getChildAt(0);
1775
1776 int widthDiff =
1777 tvInRootLayoutWithImage.getMeasuredWidth()
1778 - tvInRootLayoutWithoutImage.getMeasuredWidth();
1779
1780 // Check that the layout with the image is larger by exactly the image's width.
1781 expect.that(widthDiff).isEqualTo(50);
1782
1783 assertThat(tvInRootLayoutWithoutImage.getText().toString()).isEqualTo("FooBar");
1784 assertThat(tvInRootLayoutWithImage.getText().toString()).isEqualTo("Foo\u200D \u200DBar");
1785 }
1786
1787 @Test
1788 public void inflate_spannable_onClickCanFire() {
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00001789 StringProp.Builder text = string("Hello World");
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001790 LayoutElement root =
1791 LayoutElement.newBuilder()
1792 .setSpannable(
1793 Spannable.newBuilder()
1794 .addSpans(
1795 Span.newBuilder()
1796 .setText(
1797 SpanText.newBuilder()
1798 .setText(text)
1799 .setModifiers(
1800 spanClickMod()))))
1801 .build();
1802
1803 List<Boolean> hasFiredList = new ArrayList<>();
1804 FrameLayout rootLayout =
1805 renderer(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00001806 newRendererConfigBuilder(
1807 fingerprintedLayout(root), resourceResolvers())
1808 .setLoadActionListener(p -> hasFiredList.add(true))
1809 .setProtoLayoutTheme(
1810 loadTheme(R.style.MyProtoLayoutSansSerifTheme)))
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001811 .inflate();
1812
1813 TextView tv = (TextView) rootLayout.getChildAt(0);
1814
1815 // Dispatch a click event to the first View; it should trigger the LoadAction...
1816 long startTime = SystemClock.uptimeMillis();
1817 MotionEvent evt =
1818 MotionEvent.obtain(
1819 /* downTime= */ startTime,
1820 /* eventTime= */ startTime,
1821 MotionEvent.ACTION_DOWN,
1822 /* x= */ 5f,
1823 /* y= */ 5f,
1824 /* metaState= */ 0);
1825 tv.dispatchTouchEvent(evt);
1826 evt.recycle();
1827
1828 evt =
1829 MotionEvent.obtain(
1830 /* downTime= */ startTime,
1831 /* eventTime= */ startTime + 100,
1832 MotionEvent.ACTION_UP,
1833 /* x= */ 5f,
1834 /* y= */ 5f,
1835 /* metaState= */ 0);
1836 tv.dispatchTouchEvent(evt);
1837 evt.recycle();
1838
1839 shadowOf(Looper.getMainLooper()).idle();
1840
1841 assertThat(hasFiredList).hasSize(1);
1842 }
1843
1844 @NonNull
1845 private static SpanModifiers.Builder spanClickMod() {
1846 return SpanModifiers.newBuilder()
1847 .setClickable(
1848 Clickable.newBuilder()
1849 .setOnClick(
1850 Action.newBuilder()
1851 .setLoadAction(LoadAction.getDefaultInstance())));
1852 }
1853
1854 @Test
1855 public void inflate_textView_marqueeAnimation() {
1856 String textContents = "Marquee Animation";
1857 LayoutElement root =
1858 LayoutElement.newBuilder()
1859 .setText(
1860 Text.newBuilder()
1861 .setText(string(textContents))
1862 .setMaxLines(Int32Prop.newBuilder().setValue(1))
1863 .setOverflow(
1864 TextOverflowProp.newBuilder()
1865 .setValue(
1866 TextOverflow.TEXT_OVERFLOW_MARQUEE)
1867 .build()))
1868 .build();
1869
1870 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1871 TextView tv = (TextView) rootLayout.getChildAt(0);
1872 expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE);
1873 expect.that(tv.isSelected()).isTrue();
1874 expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue();
Lucas Muller Oliveira3a02f402023-04-12 09:25:42 +00001875 expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(-1); // Default value.
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001876 if (VERSION.SDK_INT >= VERSION_CODES.Q) {
1877 expect.that(tv.isSingleLine()).isTrue();
1878 }
1879 }
1880
1881 @Test
Lucas Muller Oliveira776fb1a2023-05-19 18:02:10 +00001882 public void inflate_textView_marquee_animationsDisabled() {
1883 String textContents = "Marquee Animation";
1884 LayoutElement root =
1885 LayoutElement.newBuilder()
1886 .setText(
1887 Text.newBuilder()
1888 .setText(string(textContents))
1889 .setMaxLines(Int32Prop.newBuilder().setValue(1))
1890 .setOverflow(
1891 TextOverflowProp.newBuilder()
1892 .setValue(
1893 TextOverflow.TEXT_OVERFLOW_MARQUEE)
1894 .build()))
1895 .build();
1896
1897 FrameLayout rootLayout =
1898 renderer(
1899 newRendererConfigBuilder(fingerprintedLayout(root))
1900 .setAnimationEnabled(false))
1901 .inflate();
1902 TextView tv = (TextView) rootLayout.getChildAt(0);
1903 expect.that(tv.getEllipsize()).isNull();
1904 }
1905
1906 @Test
Neda Topoljanac4d26f822023-02-22 16:57:03 +00001907 public void inflate_textView_marqueeAnimationInMultiLine() {
1908 String textContents = "Marquee Animation";
1909 LayoutElement root =
1910 LayoutElement.newBuilder()
1911 .setText(
1912 Text.newBuilder()
1913 .setText(string(textContents))
1914 .setMaxLines(Int32Prop.newBuilder().setValue(2))
1915 .setOverflow(
1916 TextOverflowProp.newBuilder()
1917 .setValue(
1918 TextOverflow.TEXT_OVERFLOW_MARQUEE)
1919 .build()))
1920 .build();
1921
1922 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1923 TextView tv = (TextView) rootLayout.getChildAt(0);
1924 expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE);
1925 expect.that(tv.isSelected()).isFalse();
1926 expect.that(tv.isHorizontalFadingEdgeEnabled()).isFalse();
1927 if (VERSION.SDK_INT >= VERSION_CODES.Q) {
1928 expect.that(tv.isSingleLine()).isFalse();
1929 }
1930 }
1931
1932 @Test
Lucas Muller Oliveira3a02f402023-04-12 09:25:42 +00001933 public void inflate_textView_marqueeAnimation_repeatLimit() {
1934 String textContents = "Marquee Animation";
1935 int marqueeIterations = 5;
1936 LayoutElement root =
1937 LayoutElement.newBuilder()
1938 .setText(
1939 Text.newBuilder()
1940 .setText(string(textContents))
1941 .setMaxLines(Int32Prop.newBuilder().setValue(1))
1942 .setOverflow(
1943 TextOverflowProp.newBuilder()
1944 .setValue(
1945 TextOverflow.TEXT_OVERFLOW_MARQUEE)
1946 .build())
1947 .setMarqueeParameters(
1948 MarqueeParameters.newBuilder()
1949 .setIterations(marqueeIterations)))
1950 .build();
1951
1952 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1953 TextView tv = (TextView) rootLayout.getChildAt(0);
1954 expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE);
1955 expect.that(tv.isSelected()).isTrue();
1956 expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue();
1957 expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(marqueeIterations);
1958 if (VERSION.SDK_INT >= VERSION_CODES.Q) {
1959 expect.that(tv.isSingleLine()).isTrue();
1960 }
1961 }
1962
1963 @Test
Neda Topoljanac742e9e72023-10-06 22:18:35 +00001964 public void inflate_textView_autosize_set() {
1965 String text = "Test text";
1966 int[] presetSizes = new int[]{12, 20, 10};
1967 List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes);
1968
1969 LayoutElement textElement =
1970 LayoutElement.newBuilder()
1971 .setText(
1972 Text.newBuilder()
1973 .setText(string(text))
1974 .setFontStyle(
1975 FontStyle.newBuilder()
1976 .addAllSize(sizes)))
1977 .build();
1978 LayoutElement root =
1979 LayoutElement.newBuilder().setBox(
1980 Box.newBuilder()
1981 .setWidth(expand())
1982 .setHeight(expand())
1983 .addContents(textElement)).build();
1984
1985 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
1986 ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
1987 TextView tv = (TextView) firstChild.getChildAt(0);
1988
1989 // TextView sorts preset sizes.
1990 Arrays.sort(presetSizes);
1991 expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM);
1992 expect.that(tv.getAutoSizeTextAvailableSizes()).isEqualTo(presetSizes);
1993 expect.that(tv.getTextSize()).isEqualTo(20);
1994 }
1995
1996 @Test
1997 public void inflate_textView_autosize_setLimit_usesSingleSize() {
1998 String text = "Test text";
1999 int sizesLength = TEXT_AUTOSIZES_LIMIT + 5;
2000 int[] presetSizes = new int[sizesLength];
2001 int expectedLastSize = 120;
2002 for (int i = 0; i < sizesLength - 1; i++) {
2003 presetSizes[i] = i + 1;
2004 }
2005 presetSizes[sizesLength - 1] = expectedLastSize;
2006 List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes);
2007
2008 LayoutElement textElement =
2009 LayoutElement.newBuilder()
2010 .setText(
2011 Text.newBuilder()
2012 .setText(string(text))
2013 .setMaxLines(Int32Prop.newBuilder().setValue(4))
2014 .setFontStyle(
2015 FontStyle.newBuilder()
2016 .addAllSize(sizes)))
2017 .build();
2018 LayoutElement root =
2019 LayoutElement.newBuilder().setBox(
2020 Box.newBuilder()
2021 .setWidth(expand())
2022 .setHeight(expand())
2023 .addContents(textElement)).build();
2024
2025 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2026 ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
2027 TextView tv = (TextView) firstChild.getChildAt(0);
2028 expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
2029 expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty();
2030 expect.that(tv.getTextSize()).isEqualTo(expectedLastSize);
2031 }
2032
2033 @Test
2034 public void inflate_textView_autosize_notSet() {
2035 String text = "Test text";
2036 int size = 24;
2037 List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{size});
2038
2039 LayoutElement textElement =
2040 LayoutElement.newBuilder()
2041 .setText(
2042 Text.newBuilder()
2043 .setText(string(text))
2044 .setFontStyle(
2045 FontStyle.newBuilder()
2046 .addAllSize(sizes)))
2047 .build();
2048 LayoutElement root =
2049 LayoutElement.newBuilder().setBox(
2050 Box.newBuilder()
2051 .setWidth(expand())
2052 .setHeight(expand())
2053 .addContents(textElement)).build();
2054
2055 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2056 ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
2057 TextView tv = (TextView) firstChild.getChildAt(0);
2058 expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
2059 expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty();
2060 expect.that(tv.getTextSize()).isEqualTo(size);
2061 }
2062
2063 @Test
2064 public void inflate_textView_autosize_setDynamic_noop() {
2065 String text = "Test text";
2066 int lastSize = 24;
2067 List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{10, 30, lastSize});
2068
2069 LayoutElement textElement =
2070 LayoutElement.newBuilder()
2071 .setText(
2072 Text.newBuilder()
2073 .setText(dynamicString(text))
2074 .setFontStyle(
2075 FontStyle.newBuilder()
2076 .addAllSize(sizes)))
2077 .build();
2078 LayoutElement root =
2079 LayoutElement.newBuilder().setBox(
2080 Box.newBuilder()
2081 .setWidth(expand())
2082 .setHeight(expand())
2083 .addContents(textElement)).build();
2084
2085 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2086 ArrayList<View> textChildren = new ArrayList<>();
2087 rootLayout.findViewsWithText(textChildren, text, View.FIND_VIEWS_WITH_TEXT);
2088 TextView tv = (TextView) textChildren.get(0);
2089 expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
2090 expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty();
2091 expect.that(tv.getTextSize()).isEqualTo(lastSize);
2092 }
2093
2094 @Test
Neda Topoljanacf1294462023-10-19 14:25:12 +00002095 public void inflate_textView_autosize_wrongSizes_noop() {
2096 String text = "Test text";
2097 List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{0, -2, 0});
2098
2099 LayoutElement textElement =
2100 LayoutElement.newBuilder()
2101 .setText(
2102 Text.newBuilder()
2103 .setText(string(text))
2104 .setFontStyle(
2105 FontStyle.newBuilder()
2106 .addAllSize(sizes)))
2107 .build();
2108 LayoutElement root =
2109 LayoutElement.newBuilder().setBox(
2110 Box.newBuilder()
2111 .setWidth(expand())
2112 .setHeight(expand())
2113 .addContents(textElement)).build();
2114
2115 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2116 ArrayList<View> textChildren = new ArrayList<>();
2117 rootLayout.findViewsWithText(textChildren, text, View.FIND_VIEWS_WITH_TEXT);
2118 TextView tv = (TextView) textChildren.get(0);
2119 expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
2120 expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty();
2121 }
2122
2123 @Test
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002124 public void inflate_spannable_marqueeAnimation() {
2125 String text = "Marquee Animation";
2126 LayoutElement root =
2127 LayoutElement.newBuilder()
2128 .setSpannable(
2129 Spannable.newBuilder()
2130 .addSpans(
2131 Span.newBuilder()
2132 .setText(
2133 SpanText.newBuilder()
2134 .setText(string(text))))
2135 .setOverflow(
2136 TextOverflowProp.newBuilder()
2137 .setValue(
2138 TextOverflow.TEXT_OVERFLOW_MARQUEE))
2139 .setMaxLines(Int32Prop.newBuilder().setValue(1)))
2140 .build();
2141
2142 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2143 TextView tv = (TextView) rootLayout.getChildAt(0);
2144 expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE);
2145 expect.that(tv.isSelected()).isTrue();
2146 expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue();
Lucas Muller Oliveira3a02f402023-04-12 09:25:42 +00002147 expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(-1); // Default value.
2148 if (VERSION.SDK_INT >= VERSION_CODES.Q) {
2149 expect.that(tv.isSingleLine()).isTrue();
2150 }
2151 }
2152
2153 @Test
Neda Topoljanac742e9e72023-10-06 22:18:35 +00002154 public void inflate_spantext_ignoresMultipleSizes() {
2155 String text = "Test text";
2156 int firstSize = 12;
2157 FontStyle.Builder style = FontStyle.newBuilder()
2158 .addAllSize(buildSizesList(new int[]{firstSize, 10, 20}));
2159 LayoutElement root =
2160 LayoutElement.newBuilder()
2161 .setSpannable(
2162 Spannable.newBuilder()
2163 .addSpans(
2164 Span.newBuilder()
2165 .setText(
2166 SpanText.newBuilder()
2167 .setText(string(text))
2168 .setFontStyle(style))))
2169 .build();
2170
2171 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2172 TextView tv = (TextView) rootLayout.getChildAt(0);
2173 expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
2174 }
2175
2176 @Test
Lucas Muller Oliveira3a02f402023-04-12 09:25:42 +00002177 public void inflate_spannable_marqueeAnimation_repeatLimit() {
2178 String text = "Marquee Animation";
2179 int marqueeIterations = 5;
2180 LayoutElement root =
2181 LayoutElement.newBuilder()
2182 .setSpannable(
2183 Spannable.newBuilder()
2184 .addSpans(
2185 Span.newBuilder()
2186 .setText(
2187 SpanText.newBuilder()
2188 .setText(string(text))))
2189 .setOverflow(
2190 TextOverflowProp.newBuilder()
2191 .setValue(
2192 TextOverflow.TEXT_OVERFLOW_MARQUEE))
2193 .setMarqueeParameters(
2194 MarqueeParameters.newBuilder()
2195 .setIterations(marqueeIterations))
2196 .setMaxLines(Int32Prop.newBuilder().setValue(1)))
2197 .build();
2198
2199 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2200 TextView tv = (TextView) rootLayout.getChildAt(0);
2201 expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE);
2202 expect.that(tv.isSelected()).isTrue();
2203 expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue();
2204 expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(marqueeIterations);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002205 if (VERSION.SDK_INT >= VERSION_CODES.Q) {
2206 expect.that(tv.isSingleLine()).isTrue();
2207 }
2208 }
2209
2210 @Test
2211 public void inflate_image_intrinsicSizeIsIgnored() {
2212 Image.Builder image =
2213 Image.newBuilder()
2214 .setWidth(expandImage())
2215 .setHeight(expandImage())
2216 .setResourceId(string("large_image_120dp"));
2217 LayoutElement root =
2218 LayoutElement.newBuilder()
2219 .setBox(
2220 Box.newBuilder()
2221 .setWidth(wrap())
2222 .setHeight(wrap())
2223 .addContents(
2224 LayoutElement.newBuilder()
2225 .setImage(
2226 Image.newBuilder()
2227 .setWidth(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002228 linImageDim(
2229 dp(24f)))
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002230 .setHeight(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002231 linImageDim(
2232 dp(24f)))
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002233 .setResourceId(
2234 string("android"))))
2235 .addContents(LayoutElement.newBuilder().setImage(image)))
2236 .build();
2237
2238 FrameLayout rootLayout =
2239 renderer(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002240 newRendererConfigBuilder(fingerprintedLayout(root))
2241 .setProtoLayoutTheme(
2242 loadTheme(R.style.MyProtoLayoutSansSerifTheme)))
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002243 .inflate();
2244
2245 // Outer box should be 24dp
2246 FrameLayout firstBox = (FrameLayout) rootLayout.getChildAt(0);
2247 expect.that(firstBox.getWidth()).isEqualTo(24);
2248 expect.that(firstBox.getHeight()).isEqualTo(24);
2249
2250 // Both children (images) should have the same dimensions as the FrameLayout.
2251 RatioViewWrapper rvw1 = (RatioViewWrapper) firstBox.getChildAt(0);
2252 RatioViewWrapper rvw2 = (RatioViewWrapper) firstBox.getChildAt(1);
2253
2254 expect.that(rvw1.getWidth()).isEqualTo(24);
2255 expect.that(rvw1.getHeight()).isEqualTo(24);
2256
2257 expect.that(rvw2.getWidth()).isEqualTo(24);
2258 expect.that(rvw2.getHeight()).isEqualTo(24);
2259
2260 ImageViewWithoutIntrinsicSizes image1 = (ImageViewWithoutIntrinsicSizes) rvw1.getChildAt(0);
2261 ImageViewWithoutIntrinsicSizes image2 = (ImageViewWithoutIntrinsicSizes) rvw2.getChildAt(0);
2262
2263 expect.that(image1.getWidth()).isEqualTo(24);
2264 expect.that(image1.getHeight()).isEqualTo(24);
2265
2266 expect.that(image2.getWidth()).isEqualTo(24);
2267 expect.that(image2.getHeight()).isEqualTo(24);
2268 }
2269
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002270 @NonNull
2271 private static ImageDimension.Builder linImageDim(DpProp.Builder builderForValue) {
2272 return ImageDimension.newBuilder().setLinearDimension(builderForValue);
2273 }
2274
2275 @NonNull
2276 private static ContainerDimension.Builder wrap() {
2277 return ContainerDimension.newBuilder()
2278 .setWrappedDimension(WrappedDimensionProp.getDefaultInstance());
2279 }
2280
2281 @Test
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002282 public void inflate_image_undefinedSizeIgnoresIntrinsicSize() {
2283 // This can happen in the case that a layout is ever inflated into a Scrolling layout. In
2284 // that case, the scrolling layout will measure all children with height = UNDEFINED, which
2285 // can lead to an Image still using its intrinsic size.
2286 String resId = "large_image_120dp";
2287 LayoutElement root =
2288 LayoutElement.newBuilder()
2289 .setBox(
2290 Box.newBuilder()
2291 .setWidth(wrap())
2292 .setHeight(wrap())
2293 .addContents(
2294 LayoutElement.newBuilder()
2295 .setImage(
2296 Image.newBuilder()
2297 .setWidth(
2298 linImageDim(
2299 dp(24f)))
2300 .setHeight(
2301 linImageDim(
2302 dp(24f)))
2303 .setResourceId(
2304 string("android"))))
2305 .addContents(
2306 LayoutElement.newBuilder()
2307 .setImage(
2308 Image.newBuilder()
2309 .setWidth(expandImage())
2310 .setHeight(expandImage())
2311 .setResourceId(
2312 string(resId)))))
2313 .build();
2314
2315 FrameLayout rootLayout =
2316 renderer(
2317 newRendererConfigBuilder(fingerprintedLayout(root))
2318 .setProtoLayoutTheme(
2319 loadTheme(R.style.MyProtoLayoutSansSerifTheme)))
2320 .inflate();
2321
2322 // Re-measure the root layout with an UNDEFINED constraint...
2323 int screenWidth = MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, MeasureSpec.EXACTLY);
2324 int screenHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
2325 rootLayout.measure(screenWidth, screenHeight);
2326 rootLayout.layout(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
2327
2328 // Outer box should be 24dp
2329 FrameLayout firstBox = (FrameLayout) rootLayout.getChildAt(0);
2330 expect.that(firstBox.getWidth()).isEqualTo(24);
2331 expect.that(firstBox.getHeight()).isEqualTo(24);
2332
2333 // Both children (images) should have the same dimensions as the FrameLayout.
2334 RatioViewWrapper rvw1 = (RatioViewWrapper) firstBox.getChildAt(0);
2335 RatioViewWrapper rvw2 = (RatioViewWrapper) firstBox.getChildAt(1);
2336
2337 expect.that(rvw1.getWidth()).isEqualTo(24);
2338 expect.that(rvw1.getHeight()).isEqualTo(24);
2339
2340 expect.that(rvw2.getWidth()).isEqualTo(24);
2341 expect.that(rvw2.getHeight()).isEqualTo(24);
2342
2343 ImageViewWithoutIntrinsicSizes image1 = (ImageViewWithoutIntrinsicSizes) rvw1.getChildAt(0);
2344 ImageViewWithoutIntrinsicSizes image2 = (ImageViewWithoutIntrinsicSizes) rvw2.getChildAt(0);
2345
2346 expect.that(image1.getWidth()).isEqualTo(24);
2347 expect.that(image1.getHeight()).isEqualTo(24);
2348
2349 expect.that(image2.getWidth()).isEqualTo(24);
2350 expect.that(image2.getHeight()).isEqualTo(24);
2351 }
2352
2353 @Test
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002354 public void inflate_arcLine_usesValueForLayout() {
2355 DynamicFloat arcLength =
2356 DynamicFloat.newBuilder().setFixed(FixedFloat.newBuilder().setValue(45f)).build();
2357
2358 LayoutElement root =
2359 LayoutElement.newBuilder()
2360 .setArc(
2361 Arc.newBuilder()
2362 .setAnchorAngle(degrees(0).build())
2363 .addContents(
2364 ArcLayoutElement.newBuilder()
2365 .setLine(
2366 ArcLine.newBuilder()
2367 // Shorter than 360 degrees,
2368 // so should be drawn as an
2369 // arc:
2370 .setLength(
2371 degreesDynamic(
2372 arcLength,
2373 180f))
2374 .setThickness(dp(12)))))
2375 .build();
2376
2377 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2378
2379 shadowOf(Looper.getMainLooper()).idle();
2380
2381 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
2382 SizedArcContainer sizedContainer = (SizedArcContainer) arcLayout.getChildAt(0);
2383 WearCurvedLineView line = (WearCurvedLineView) sizedContainer.getChildAt(0);
2384 assertThat(sizedContainer.getSweepAngleDegrees()).isEqualTo(180f);
2385 assertThat(line.getLineSweepAngleDegrees()).isEqualTo(45f);
2386 assertThat(line.getMaxSweepAngleDegrees()).isEqualTo(180f);
2387 }
2388
2389 @Test
Lucas Muller Oliveiraaf6d8712023-09-21 15:58:05 +00002390 public void inflate_arcLine_usesZeroValueForLayout() {
2391 DynamicFloat arcLength =
2392 DynamicFloat.newBuilder().setFixed(FixedFloat.newBuilder().setValue(45f)).build();
2393
2394 LayoutElement root =
2395 LayoutElement.newBuilder()
2396 .setArc(
2397 Arc.newBuilder()
2398 .setAnchorAngle(degrees(0).build())
2399 .addContents(
2400 ArcLayoutElement.newBuilder()
2401 .setLine(
2402 ArcLine.newBuilder()
2403 .setLength(
2404 degreesDynamic(
2405 arcLength,
2406 0f))
2407 .setThickness(dp(12)))))
2408 .build();
2409
2410 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2411
2412 shadowOf(Looper.getMainLooper()).idle();
2413
2414 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
2415 SizedArcContainer sizedContainer = (SizedArcContainer) arcLayout.getChildAt(0);
2416 expect.that(sizedContainer.getSweepAngleDegrees()).isEqualTo(0f);
2417 WearCurvedLineView line = (WearCurvedLineView) sizedContainer.getChildAt(0);
2418 expect.that(line.getMaxSweepAngleDegrees()).isEqualTo(0f);
2419 }
2420
2421 @Test
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002422 public void inflate_arcLine_dynamicData_updatesArcLength() {
Xiangyin Ma3d99b132023-04-06 22:03:11 +00002423 AppDataKey<DynamicBuilders.DynamicInt32> keyFoo = new AppDataKey<>("foo");
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002424 mStateStore.setAppStateEntryValuesProto(
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002425 ImmutableMap.of(
Xiangyin Ma3d99b132023-04-06 22:03:11 +00002426 keyFoo,
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002427 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002428 .setInt32Val(FixedInt32.newBuilder().setValue(10))
2429 .build()));
2430
2431 shadowOf(Looper.getMainLooper()).idle();
2432
2433 DynamicFloat arcLength =
2434 DynamicFloat.newBuilder()
2435 .setInt32ToFloatOperation(
2436 Int32ToFloatOp.newBuilder()
2437 .setInput(
2438 DynamicInt32.newBuilder()
2439 .setStateSource(
2440 StateInt32Source.newBuilder()
2441 .setSourceKey("foo"))))
2442 .build();
2443
2444 LayoutElement root =
2445 LayoutElement.newBuilder()
2446 .setArc(
2447 Arc.newBuilder()
2448 .setAnchorAngle(degrees(0).build())
2449 .addContents(
2450 ArcLayoutElement.newBuilder()
2451 .setLine(
2452 ArcLine.newBuilder()
2453 // Shorter than 360 degrees,
2454 // so should be drawn as an
2455 // arc:
2456 .setLength(
2457 degreesDynamic(
2458 arcLength,
2459 180f))
2460 .setThickness(dp(12)))))
2461 .build();
2462
2463 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2464
2465 shadowOf(Looper.getMainLooper()).idle();
2466
2467 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
2468 SizedArcContainer sizedContainer = (SizedArcContainer) arcLayout.getChildAt(0);
2469 WearCurvedLineView line = (WearCurvedLineView) sizedContainer.getChildAt(0);
2470 assertThat(line.getLineSweepAngleDegrees()).isEqualTo(10);
2471
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002472 mStateStore.setAppStateEntryValuesProto(
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002473 ImmutableMap.of(
Xiangyin Ma3d99b132023-04-06 22:03:11 +00002474 keyFoo,
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002475 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002476 .setInt32Val(FixedInt32.newBuilder().setValue(20))
2477 .build()));
2478
2479 assertThat(line.getLineSweepAngleDegrees()).isEqualTo(20);
2480 }
2481
2482 @Test
2483 public void inflate_arcLine_withoutValueForLayout_noLegacyMode_usesZero() {
2484 DynamicFloat arcLength =
2485 DynamicFloat.newBuilder().setFixed(FixedFloat.newBuilder().setValue(45f)).build();
2486
2487 LayoutElement root =
2488 LayoutElement.newBuilder()
2489 .setArc(
2490 Arc.newBuilder()
2491 .setAnchorAngle(degrees(0).build())
2492 .addContents(
2493 ArcLayoutElement.newBuilder()
2494 .setLine(
2495 ArcLine.newBuilder()
2496 // Shorter than 360 degrees,
2497 // so should be drawn as an
2498 // arc:
2499 .setLength(
2500 degreesDynamic(
2501 arcLength))
2502 .setThickness(dp(12)))))
2503 .build();
2504
2505 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2506
2507 shadowOf(Looper.getMainLooper()).idle();
2508
2509 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
2510 SizedArcContainer sizedContainer = (SizedArcContainer) arcLayout.getChildAt(0);
2511 WearCurvedLineView line = (WearCurvedLineView) sizedContainer.getChildAt(0);
2512 assertThat(sizedContainer.getSweepAngleDegrees()).isEqualTo(0f);
2513 assertThat(line.getLineSweepAngleDegrees()).isEqualTo(45f);
2514 assertThat(line.getMaxSweepAngleDegrees()).isEqualTo(0);
2515 }
2516
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002517 @NonNull
2518 private static DegreesProp.Builder degreesDynamic(DynamicFloat arcLength) {
2519 return DegreesProp.newBuilder().setDynamicValue(arcLength);
2520 }
2521
2522 @NonNull
2523 private static DegreesProp.Builder degreesDynamic(
2524 DynamicFloat arcLength, float valueForLayout) {
2525 return DegreesProp.newBuilder()
2526 .setValueForLayout(valueForLayout)
2527 .setDynamicValue(arcLength);
2528 }
2529
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002530 @Test
Lucas Muller Oliveiraaf6d8712023-09-21 15:58:05 +00002531 public void inflate_arcLine_withoutValueForLayout_legacyMode_usesArcLength() {
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002532 DynamicFloat arcLength =
2533 DynamicFloat.newBuilder().setFixed(FixedFloat.newBuilder().setValue(45f)).build();
2534
2535 LayoutElement root =
2536 LayoutElement.newBuilder()
2537 .setArc(
2538 Arc.newBuilder()
2539 .setAnchorAngle(degrees(0).build())
2540 .addContents(
2541 ArcLayoutElement.newBuilder()
2542 .setLine(
2543 ArcLine.newBuilder()
2544 // Shorter than 360 degrees,
2545 // so should be drawn as an
2546 // arc:
2547 .setLength(
2548 degreesDynamic(
2549 arcLength))
2550 .setThickness(dp(12)))))
2551 .build();
2552
2553 FrameLayout rootLayout =
2554 renderer(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002555 newRendererConfigBuilder(fingerprintedLayout(root))
2556 .setAllowLayoutChangingBindsWithoutDefault(true))
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002557 .inflate();
2558
2559 shadowOf(Looper.getMainLooper()).idle();
2560
2561 ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0);
2562 WearCurvedLineView line = (WearCurvedLineView) arcLayout.getChildAt(0);
2563 assertThat(line.getSweepAngleDegrees()).isEqualTo(45f);
2564 assertThat(line.getLineSweepAngleDegrees()).isEqualTo(45f);
2565 }
2566
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002567 @Test
2568 public void inflate_text_dynamicColor_updatesColor() {
Xiangyin Ma3d99b132023-04-06 22:03:11 +00002569 AppDataKey<DynamicBuilders.DynamicColor> keyFoo = new AppDataKey<>("foo");
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002570 mStateStore.setAppStateEntryValuesProto(
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002571 ImmutableMap.of(
Xiangyin Ma3d99b132023-04-06 22:03:11 +00002572 keyFoo,
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002573 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002574 .setColorVal(FixedColor.newBuilder().setArgb(0xFFFFFFFF))
2575 .build()));
2576 shadowOf(Looper.getMainLooper()).idle();
2577
2578 DynamicColor color =
2579 DynamicColor.newBuilder()
2580 .setStateSource(StateColorSource.newBuilder().setSourceKey("foo"))
2581 .build();
2582
2583 LayoutElement root =
2584 LayoutElement.newBuilder()
2585 .setText(
2586 Text.newBuilder()
2587 .setText(string("Hello World"))
2588 .setFontStyle(
2589 FontStyle.newBuilder()
2590 .setColor(
2591 ColorProp.newBuilder()
2592 .setDynamicValue(color))))
2593 .build();
2594
2595 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2596 shadowOf(Looper.getMainLooper()).idle();
2597
2598 TextView tv = (TextView) rootLayout.getChildAt(0);
2599 assertThat(tv.getCurrentTextColor()).isEqualTo(0xFFFFFFFF);
2600
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002601 mStateStore.setAppStateEntryValuesProto(
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002602 ImmutableMap.of(
Xiangyin Ma3d99b132023-04-06 22:03:11 +00002603 keyFoo,
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002604 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002605 .setColorVal(FixedColor.newBuilder().setArgb(0x11111111))
2606 .build()));
2607
2608 assertThat(tv.getCurrentTextColor()).isEqualTo(0x11111111);
2609 }
2610
2611 @Test
2612 public void inflate_image_dynamicTint_changesTintColor() {
2613 // Must match a resource ID in buildResources
2614 String protoResId = "android";
2615
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002616 mStateStore.setAppStateEntryValuesProto(
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002617 ImmutableMap.of(
Xiangyin Ma3d99b132023-04-06 22:03:11 +00002618 new AppDataKey<DynamicBuilders.DynamicColor>("tint"),
Xiangyin Ma8313fb12023-04-12 16:44:11 +00002619 DynamicDataValue.newBuilder()
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002620 .setColorVal(FixedColor.newBuilder().setArgb(0xFFFFFFFF))
2621 .build()));
2622 shadowOf(Looper.getMainLooper()).idle();
2623
2624 DynamicColor color =
2625 DynamicColor.newBuilder()
2626 .setStateSource(StateColorSource.newBuilder().setSourceKey("tint"))
2627 .build();
2628
2629 LayoutElement root =
2630 LayoutElement.newBuilder()
2631 .setImage(
2632 Image.newBuilder()
2633 .setResourceId(string(protoResId))
2634 .setColorFilter(
2635 ColorFilter.newBuilder()
2636 .setTint(
2637 ColorProp.newBuilder()
2638 .setDynamicValue(color)))
2639 .setHeight(expandImage())
2640 .setWidth(expandImage()))
2641 .build();
2642
2643 FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
2644
2645 shadowOf(Looper.getMainLooper()).idle();
2646
2647 RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0);
2648 ImageView iv = (ImageView) rvw.getChildAt(0);
2649 assertThat(iv.getImageTintList().getDefaultColor()).isEqualTo(0xFFFFFFFF);
2650 }
2651
2652 @Test
Mohammad Saboorian8d7f74f2023-04-17 16:55:34 +01002653 public void inflate_extension_onlySpaceIfNoExtension() {
2654 byte[] payload = "Hello World".getBytes(StandardCharsets.UTF_8);
2655 int size = 5;
2656
2657 ExtensionDimension dim =
2658 ExtensionDimension.newBuilder().setLinearDimension(dp(size)).build();
2659 LayoutElement rootElement =
2660 LayoutElement.newBuilder()
2661 .setExtension(
2662 ExtensionLayoutElement.newBuilder()
2663 .setExtensionId("foo")
2664 .setPayload(ByteString.copyFrom(payload))
2665 .setWidth(dim)
2666 .setHeight(dim))
2667 .build();
2668
2669 FrameLayout inflatedLayout = renderer(fingerprintedLayout(rootElement)).inflate();
2670
2671 assertThat(inflatedLayout.getChildCount()).isEqualTo(1);
2672 assertThat(inflatedLayout.getChildAt(0)).isInstanceOf(Space.class);
2673
2674 Space s = (Space) inflatedLayout.getChildAt(0);
2675 assertThat(s.getMeasuredWidth()).isEqualTo(size);
2676 assertThat(s.getMeasuredHeight()).isEqualTo(size);
2677 }
2678
2679 @Test
2680 public void inflate_rendererExtension_withExtension_callsExtension() {
2681 List<Pair<byte[], String>> invokedExtensions = new ArrayList<>();
2682
2683 final byte[] payload = "Hello World".getBytes(StandardCharsets.UTF_8);
2684 final int size = 5;
2685 final String extensionId = "foo";
2686
2687 ExtensionDimension dim =
2688 ExtensionDimension.newBuilder().setLinearDimension(dp(size)).build();
2689 LayoutElement rootElement =
2690 LayoutElement.newBuilder()
2691 .setExtension(
2692 ExtensionLayoutElement.newBuilder()
2693 .setExtensionId(extensionId)
2694 .setPayload(ByteString.copyFrom(payload))
2695 .setWidth(dim)
2696 .setHeight(dim))
2697 .build();
2698
2699 FrameLayout inflatedLayout =
2700 renderer(
2701 newRendererConfigBuilder(fingerprintedLayout(rootElement))
2702 .setExtensionViewProvider(
2703 (extensionPayload, id) -> {
2704 invokedExtensions.add(
2705 new Pair<>(extensionPayload, id));
2706 TextView returnedView =
2707 new TextView(getApplicationContext());
2708 returnedView.setText("testing");
2709
2710 return returnedView;
2711 }))
2712 .inflate();
2713
2714 assertThat(inflatedLayout.getChildCount()).isEqualTo(1);
2715 assertThat(inflatedLayout.getChildAt(0)).isInstanceOf(TextView.class);
2716
2717 TextView tv = (TextView) inflatedLayout.getChildAt(0);
2718 assertThat(tv.getText().toString()).isEqualTo("testing");
2719
2720 assertThat(invokedExtensions).hasSize(1);
2721 assertThat(invokedExtensions.get(0).first).isEqualTo(payload);
2722 assertThat(invokedExtensions.get(0).second).isEqualTo(extensionId);
2723 }
2724
2725 @Test
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002726 public void inflateThenMutate_withChangeToText_causesUpdate() {
2727 Layout layout1 =
2728 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002729 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002730 text("Hello"), // 1.1
2731 text("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002732 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002733
2734 // Check that we have the initial layout correctly rendered
2735 Renderer renderer = renderer(layout1);
2736 ViewGroup inflatedViewParent = renderer.inflate();
2737 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
2738 TextView tv1 = (TextView) column.getChildAt(0);
2739 TextView tv2 = (TextView) column.getChildAt(1);
2740 assertThat(column.getChildCount()).isEqualTo(2);
2741 assertThat(tv1.getText().toString()).isEqualTo("Hello");
2742 assertThat(tv2.getText().toString()).isEqualTo("World");
2743
2744 // Produce a new layout with only one Text element changed.
2745 Layout layout2 =
2746 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002747 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002748 text("Hello"), // 1.1
2749 text("Mars") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002750 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002751
2752 // Compute the mutation
2753 ViewGroupMutation mutation =
2754 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
2755 assertThat(mutation).isNotNull();
2756 assertThat(mutation.isNoOp()).isFalse();
2757
2758 // Apply the mutation
2759 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
2760 assertThat(mutationResult).isTrue();
2761 ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
2762 TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0);
2763 TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1);
2764
2765 // Unchanged views should be left exactly the same:
2766 assertThat(columnAfterMutation).isSameInstanceAs(column);
2767 assertThat(columnAfterMutation.getChildCount()).isEqualTo(2);
2768 assertThat(tv1AfterMutation).isSameInstanceAs(tv1);
2769 // Overall content should match layout2:
2770 assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello");
2771 assertThat(tv2AfterMutation.getText().toString()).isEqualTo("Mars");
2772 }
2773
2774 @Test
2775 public void inflateThenMutate_withChangeToImageAndText_causesUpdate() {
2776 Layout layout1 =
2777 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002778 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002779 text("Hello"), // 1.1
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002780 row( // 1.2
2781 image( // 1.2.1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002782 props -> {
2783 props.heightDp = 50;
2784 props.widthDp = 50;
2785 },
2786 "android"),
2787 text("World") // 1.2.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002788 )));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002789
2790 // Check that we have the initial layout correctly rendered
2791 Renderer renderer = renderer(layout1);
2792 ViewGroup inflatedViewParent = renderer.inflate();
2793 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
2794 TextView tv1 = (TextView) column.getChildAt(0);
2795 ViewGroup row = (ViewGroup) column.getChildAt(1);
2796 ImageView image = (ImageView) ((ViewGroup) row.getChildAt(0)).getChildAt(0);
2797 TextView tv2 = (TextView) row.getChildAt(1);
2798 assertThat(column.getChildCount()).isEqualTo(2);
2799 assertThat(row.getChildCount()).isEqualTo(2);
2800 assertThat(tv1.getText().toString()).isEqualTo("Hello");
2801 assertThat(tv2.getText().toString()).isEqualTo("World");
2802 // Can't get android resource ID from image, so use the size to infer that we start with the
2803 // correct one.
2804 assertThat(image.getDrawable().getIntrinsicHeight()).isEqualTo(24);
2805 assertThat(image.getDrawable().getIntrinsicWidth()).isEqualTo(24);
2806 assertThat(image.getMeasuredHeight()).isEqualTo(50);
2807 assertThat(image.getMeasuredWidth()).isEqualTo(50);
2808
2809 // Produce a new layout with one Text element and one Image changed.
2810 Layout layout2 =
2811 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002812 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002813 text("Hello"), // 1.1
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002814 row( // 1.2
2815 image( // 1.2.1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002816 props -> {
2817 props.heightDp = 50;
2818 props.widthDp = 50;
2819 },
2820 "large_image_120dp"),
2821 text("Mars") // 1.2.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002822 )));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002823
2824 // Compute the mutation
2825 ViewGroupMutation mutation =
2826 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
2827 assertThat(mutation).isNotNull();
2828 assertThat(mutation.isNoOp()).isFalse();
2829
2830 // Apply the mutation
2831 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
2832 assertThat(mutationResult).isTrue();
2833 ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
2834 TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0);
2835 ViewGroup rowAfterMutation = (ViewGroup) columnAfterMutation.getChildAt(1);
2836 ImageView imageAfterMutation =
2837 (ImageView) ((ViewGroup) rowAfterMutation.getChildAt(0)).getChildAt(0);
2838 TextView tv2AfterMutation = (TextView) row.getChildAt(1);
2839
2840 // Unchanged views should be left exactly the same:
2841 assertThat(columnAfterMutation).isSameInstanceAs(column);
2842 assertThat(columnAfterMutation.getChildCount()).isEqualTo(2);
2843 assertThat(tv1AfterMutation).isSameInstanceAs(tv1);
2844 assertThat(rowAfterMutation).isSameInstanceAs(row);
2845 assertThat(rowAfterMutation.getChildCount()).isEqualTo(2);
2846 // Overall content should match layout2:
2847 assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello");
2848 assertThat(tv2AfterMutation.getText().toString()).isEqualTo("Mars");
2849 // Can't get android resource ID from image, so use the size to infer that the image has
Achraf Feydide73e2d2023-04-05 16:28:01 +00002850 // been
2851 // correctly updated to a different one:
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002852 assertThat(imageAfterMutation.getDrawable().getIntrinsicHeight()).isEqualTo(120);
2853 assertThat(imageAfterMutation.getDrawable().getIntrinsicWidth()).isEqualTo(120);
2854 assertThat(imageAfterMutation.getMeasuredHeight()).isEqualTo(50);
2855 assertThat(imageAfterMutation.getMeasuredWidth()).isEqualTo(50);
2856 }
2857
2858 @Test
2859 public void inflateThenMutate_withChangeToProps_causesUpdate() {
2860 Layout layout1 =
2861 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002862 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002863 props -> props.widthDp = 55,
2864 text("Hello"), // 1.1
2865 text("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002866 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002867
2868 // Check that we have the initial layout correctly rendered
2869 Renderer renderer = renderer(layout1);
2870 ViewGroup inflatedViewParent = renderer.inflate();
2871 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
2872 TextView tv1 = (TextView) column.getChildAt(0);
2873 TextView tv2 = (TextView) column.getChildAt(1);
2874 assertThat(column.getMeasuredWidth()).isEqualTo(55);
2875 assertThat(column.getChildCount()).isEqualTo(2);
2876 assertThat(tv1.getText().toString()).isEqualTo("Hello");
2877 assertThat(tv2.getText().toString()).isEqualTo("World");
2878
2879 // Produce a new layout with only the props of the container changed.
2880 Layout layout2 =
2881 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002882 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002883 props -> props.widthDp = 123,
2884 text("Hello"), // 1.1
2885 text("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002886 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002887
2888 // Compute the mutation
2889 ViewGroupMutation mutation =
2890 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
2891 assertThat(mutation).isNotNull();
2892 assertThat(mutation.isNoOp()).isFalse();
2893
2894 // Apply the mutation
2895 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
2896 assertThat(mutationResult).isTrue();
2897
2898 // Check contents after mutation
2899 ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
2900 TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0);
2901 TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1);
2902 assertThat(columnAfterMutation.getMeasuredWidth()).isEqualTo(123);
2903 assertThat(columnAfterMutation.getChildCount()).isEqualTo(2);
2904 assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello");
2905 assertThat(tv2AfterMutation.getText().toString()).isEqualTo("World");
2906 assertThat(tv1AfterMutation).isSameInstanceAs(tv1);
2907 assertThat(tv2AfterMutation).isSameInstanceAs(tv2);
2908 }
2909
2910 @Test
2911 public void inflateThenMutate_withChangeToPropsAndOneChild_doesntUpdateAllChildren() {
2912 Layout layout1 =
2913 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002914 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002915 props -> props.widthDp = 55,
2916 text("Hello"), // 1.1
2917 text("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002918 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002919
2920 // Check that we have the initial layout correctly rendered
2921 Renderer renderer = renderer(layout1);
2922 ViewGroup inflatedViewParent = renderer.inflate();
2923 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
2924 TextView tv1 = (TextView) column.getChildAt(0);
2925 TextView tv2 = (TextView) column.getChildAt(1);
2926 assertThat(column.getMeasuredWidth()).isEqualTo(55);
2927 assertThat(column.getChildCount()).isEqualTo(2);
2928 assertThat(tv1.getText().toString()).isEqualTo("Hello");
2929 assertThat(tv2.getText().toString()).isEqualTo("World");
2930
2931 // Produce a new layout with the props of the container and one child changed.
2932 Layout layout2 =
2933 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002934 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002935 props -> props.widthDp = 123,
2936 text("Hello"), // 1.1
2937 text("MARS") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002938 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002939
2940 // Compute the mutation
2941 ViewGroupMutation mutation =
2942 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
2943 assertThat(mutation).isNotNull();
2944 assertThat(mutation.isNoOp()).isFalse();
2945
2946 // Apply the mutation
2947 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
2948 assertThat(mutationResult).isTrue();
2949
2950 // Check contents after mutation
2951 ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
2952 TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0);
2953 TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1);
2954 assertThat(columnAfterMutation.getMeasuredWidth()).isEqualTo(123);
2955 assertThat(columnAfterMutation.getChildCount()).isEqualTo(2);
2956 assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello");
2957 assertThat(tv2AfterMutation.getText().toString()).isEqualTo("MARS");
2958 assertThat(tv1AfterMutation).isSameInstanceAs(tv1);
2959 }
2960
2961 @Test
2962 public void inflateThenMutate_withNoChange_producesNoOpMutation() {
2963 Layout layout =
2964 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002965 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002966 text("Hello"), // 1.1
2967 text("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00002968 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00002969
2970 // Check that we have the initial layout correctly rendered
2971 Renderer renderer = renderer(layout);
2972 ViewGroup inflatedViewParent = renderer.inflate();
2973 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
2974 TextView tv1 = (TextView) column.getChildAt(0);
2975 TextView tv2 = (TextView) column.getChildAt(1);
2976 assertThat(column.getChildCount()).isEqualTo(2);
2977 assertThat(tv1.getText().toString()).isEqualTo("Hello");
2978 assertThat(tv2.getText().toString()).isEqualTo("World");
2979
2980 // Compute the mutation for the same layout
2981 ViewGroupMutation mutation =
2982 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout);
2983 assertThat(mutation).isNotNull();
2984 assertThat(mutation.isNoOp()).isTrue();
2985
2986 // Apply the mutation
2987 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
2988 assertThat(mutationResult).isTrue();
2989 ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
2990 TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0);
2991 TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1);
2992
2993 // Everything should be exactly the same:
2994 assertThat(columnAfterMutation).isSameInstanceAs(column);
2995 assertThat(columnAfterMutation.getChildCount()).isEqualTo(2);
2996 assertThat(tv1AfterMutation).isSameInstanceAs(tv1);
2997 assertThat(tv2AfterMutation).isSameInstanceAs(tv2);
2998 assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello");
2999 assertThat(tv2AfterMutation.getText().toString()).isEqualTo("World");
3000 }
3001
3002 @Test
3003 public void inflateThenMutate_withDifferentNumberOfChildren_causesUpdate() {
3004 Layout layout1 =
3005 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003006 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003007 text("Hello"), // 1.1
3008 text("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003009 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003010
3011 // Check that we have the initial layout correctly rendered
3012 Renderer renderer = renderer(layout1);
3013 ViewGroup inflatedViewParent = renderer.inflate();
3014
3015 // Check the pre-mutation layout
3016 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
3017 TextView tv1 = (TextView) column.getChildAt(0);
3018 TextView tv2 = (TextView) column.getChildAt(1);
3019 assertThat(tv1.getText().toString()).isEqualTo("Hello");
3020 assertThat(tv2.getText().toString()).isEqualTo("World");
3021
3022 Layout layout2 =
3023 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003024 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003025 text("Hello"), // 1.1
3026 text("World"), // 1.2
3027 text("and"), // 1.3
3028 text("Mars") // 1.4
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003029 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003030
3031 // Compute the mutation
3032 ViewGroupMutation mutation =
3033 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
3034 assertThat(mutation).isNotNull();
3035 assertThat(mutation.isNoOp()).isFalse();
3036
3037 // Apply the mutation
3038 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
3039 assertThat(mutationResult).isTrue();
3040 ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
3041 TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0);
3042 TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1);
3043 TextView tv3AfterMutation = (TextView) columnAfterMutation.getChildAt(2);
3044 TextView tv4AfterMutation = (TextView) columnAfterMutation.getChildAt(3);
3045
3046 // Check contents after mutation
3047 assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello");
3048 assertThat(tv2AfterMutation.getText().toString()).isEqualTo("World");
3049 assertThat(tv3AfterMutation.getText().toString()).isEqualTo("and");
3050 assertThat(tv4AfterMutation.getText().toString()).isEqualTo("Mars");
3051 }
3052
3053 @Test
3054 public void inflateThenMutate_withDynamicText_dataPipelineIsUpdated() {
3055 Layout layout1 =
3056 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003057 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003058 dynamicFixedText("Hello"), // 1.1
3059 dynamicFixedText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003060 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003061
3062 // Check that we have the initial layout correctly rendered
3063 Renderer renderer = renderer(layout1);
3064 ViewGroup inflatedViewParent = renderer.inflate();
3065 assertThat(renderer.getDataPipelineSize()).isEqualTo(2);
3066 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
3067 assertThat(column.getChildCount()).isEqualTo(2);
3068
3069 FrameLayout tv1Wrapper = (FrameLayout) column.getChildAt(0);
3070 FrameLayout tv2Wrapper = (FrameLayout) column.getChildAt(1);
3071 TextView tv1 = (TextView) tv1Wrapper.getChildAt(0);
3072 TextView tv2 = (TextView) tv2Wrapper.getChildAt(0);
3073 assertThat(tv1.getText().toString()).isEqualTo("Hello");
3074 assertThat(tv2.getText().toString()).isEqualTo("World");
3075
3076 // Produce a new layout with the second Text element changed.
3077
3078 Layout layout2 =
3079 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003080 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003081 dynamicFixedText("Hello"), // 1.1
3082 dynamicFixedText("Mars") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003083 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003084
3085 // Compute the mutation
3086 ViewGroupMutation mutation =
3087 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
3088 assertThat(mutation).isNotNull();
3089 assertThat(mutation.isNoOp()).isFalse();
3090
3091 // Apply the mutation
3092 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
3093 assertThat(mutationResult).isTrue();
3094 assertThat(renderer.getDataPipelineSize()).isEqualTo(2);
3095 ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
3096 assertThat(columnAfterMutation.getChildCount()).isEqualTo(2);
3097 FrameLayout tv1WrapperAfterMutation = (FrameLayout) columnAfterMutation.getChildAt(0);
3098 FrameLayout tv2WrapperAfterMutation = (FrameLayout) columnAfterMutation.getChildAt(1);
3099 TextView tv1AfterMutation = (TextView) tv1WrapperAfterMutation.getChildAt(0);
3100 TextView tv2AfterMutation = (TextView) tv2WrapperAfterMutation.getChildAt(0);
3101
3102 // Unchanged views should be left exactly the same:
3103 assertThat(columnAfterMutation).isSameInstanceAs(column);
3104 assertThat(tv1WrapperAfterMutation).isSameInstanceAs(tv1Wrapper);
3105 assertThat(tv1AfterMutation).isSameInstanceAs(tv1);
3106 // Overall content should match layout2:
3107 assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello");
3108 assertThat(tv2AfterMutation.getText().toString()).isEqualTo("Mars");
3109 }
3110
3111 @Test
3112 public void inflateThenMutate_withSelfMutation_dataPipelineIsPreserved() {
3113 Layout layout1 =
3114 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003115 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003116 props -> props.widthDp = 10,
3117 dynamicFixedText("Hello"), // 1.1
3118 dynamicFixedText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003119 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003120
3121 // Check that we have the initial layout correctly rendered
3122 Renderer renderer = renderer(layout1);
3123 ViewGroup inflatedViewParent = renderer.inflate();
3124 assertThat(renderer.getDataPipelineSize()).isEqualTo(2);
3125 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
3126 assertThat(column.getChildCount()).isEqualTo(2);
3127
3128 FrameLayout tv1Wrapper = (FrameLayout) column.getChildAt(0);
3129 TextView tv1 = (TextView) tv1Wrapper.getChildAt(0);
3130 FrameLayout tv2Wrapper = (FrameLayout) column.getChildAt(1);
3131 TextView tv2 = (TextView) tv2Wrapper.getChildAt(0);
3132 assertThat(tv1.getText().toString()).isEqualTo("Hello");
3133 assertThat(tv2.getText().toString()).isEqualTo("World");
3134
3135 // Produce a new layout with the column width changed.
3136 Layout layout2 =
3137 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003138 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003139 props -> props.widthDp = 20,
3140 dynamicFixedText("Hello"), // 1.1
3141 dynamicFixedText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003142 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003143
3144 // Compute the mutation
3145 ViewGroupMutation mutation =
3146 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
3147 assertThat(mutation).isNotNull();
3148 assertThat(mutation.isNoOp()).isFalse();
3149
3150 // Apply the mutation
3151 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
3152 assertThat(mutationResult).isTrue();
3153 assertThat(renderer.getDataPipelineSize()).isEqualTo(2);
3154 ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
3155 assertThat(columnAfterMutation.getChildCount()).isEqualTo(2);
3156 FrameLayout tv1WrapperAfterMutation = (FrameLayout) columnAfterMutation.getChildAt(0);
3157 TextView tv1AfterMutation = (TextView) tv1WrapperAfterMutation.getChildAt(0);
3158 FrameLayout tv2WrapperAfterMutation = (FrameLayout) columnAfterMutation.getChildAt(1);
3159 TextView tv2AfterMutation = (TextView) tv2WrapperAfterMutation.getChildAt(0);
3160
3161 // Unchanged views should be left exactly the same:
3162 expect.that(tv1AfterMutation).isSameInstanceAs(tv1);
3163 expect.that(tv2AfterMutation).isSameInstanceAs(tv2);
3164 expect.that(tv1WrapperAfterMutation).isSameInstanceAs(tv1Wrapper);
3165 expect.that(tv2WrapperAfterMutation).isSameInstanceAs(tv2Wrapper);
3166 // Overall content should match layout2:
3167 assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello");
3168 assertThat(tv2AfterMutation.getText().toString()).isEqualTo("World");
3169 }
3170
3171 @Test
3172 public void reInflate_dataPipelineIsReset() {
3173 Layout layout =
3174 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003175 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003176 dynamicFixedText("Hello"), // 1.1
3177 dynamicFixedText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003178 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003179
3180 // Check that we have the initial layout correctly rendered
3181 Renderer renderer = renderer(layout);
3182 ViewGroup inflatedViewParent = renderer.inflate();
3183 assertThat(renderer.getDataPipelineSize()).isEqualTo(2);
3184 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
3185 assertThat(column.getChildCount()).isEqualTo(2);
3186
3187 FrameLayout tv1Wrapper = (FrameLayout) column.getChildAt(0);
3188 FrameLayout tv2Wrapper = (FrameLayout) column.getChildAt(1);
3189 TextView tv1 = (TextView) tv1Wrapper.getChildAt(0);
3190 TextView tv2 = (TextView) tv2Wrapper.getChildAt(0);
3191 assertThat(tv1.getText().toString()).isEqualTo("Hello");
3192 assertThat(tv2.getText().toString()).isEqualTo("World");
3193
3194 // Re-inflate and check that the number of nodes is the same as the previous inflation.
3195 renderer.inflate();
3196 assertThat(renderer.getDataPipelineSize()).isEqualTo(2);
3197 }
3198
3199 @Test
3200 public void inflateWithNoFingerprint_producesNoRenderingMetadata() {
3201 Layout layout1WithNoFingerprints =
3202 layout(text("Hello")).toBuilder().clearFingerprint().build();
3203 // Check that we have the initial layout correctly rendered
3204 Renderer renderer = renderer(layout1WithNoFingerprints);
3205
3206 ViewGroup inflatedViewParent = renderer.inflate();
3207 TextView tv = (TextView) inflatedViewParent.getChildAt(0);
3208
3209 assertThat(tv.getText().toString()).isEqualTo("Hello");
3210 assertThat(getRenderedMetadata(inflatedViewParent)).isNull();
3211 }
3212
3213 @Test
3214 public void inflateArcThenMutate_withChangeToText_causesUpdate() {
3215 Layout layout1 =
3216 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003217 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003218 arcText("Hello"), // 1.1
3219 arcText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003220 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003221
3222 // Check that we have the initial layout correctly rendered
3223 Renderer renderer = renderer(layout1);
3224 ViewGroup inflatedViewParent = renderer.inflate();
3225 assertThat(inflatedViewParent.getChildCount()).isEqualTo(1);
3226 ArcLayout arcLayout = (ArcLayout) inflatedViewParent.getChildAt(0);
3227 assertThat(arcLayout.getChildCount()).isEqualTo(2);
3228 CurvedTextView tv1 = (CurvedTextView) arcLayout.getChildAt(0);
3229 CurvedTextView tv2 = (CurvedTextView) arcLayout.getChildAt(1);
3230 assertThat(tv1.getText()).isEqualTo("Hello");
3231 assertThat(tv2.getText()).isEqualTo("World");
3232
3233 // Produce a new layout with only one Text element changed.
3234 Layout layout2 =
3235 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003236 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003237 arcText("Hello"), // 1.1
3238 arcText("Mars") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003239 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003240
3241 // Compute the mutation
3242 ViewGroupMutation mutation =
3243 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
3244 assertThat(mutation).isNotNull();
3245 assertThat(mutation.isNoOp()).isFalse();
3246
3247 // Apply the mutation
3248 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
3249 assertThat(mutationResult).isTrue();
3250 assertThat(inflatedViewParent.getChildCount()).isEqualTo(1);
3251 ArcLayout arcLayoutAfterMutation = (ArcLayout) inflatedViewParent.getChildAt(0);
3252
3253 // Overall content should match layout2:
3254 CurvedTextView tv1AfterMutation = (CurvedTextView) arcLayout.getChildAt(0);
3255 assertThat(tv1AfterMutation.getText()).isEqualTo("Hello");
3256 CurvedTextView tv2AfterMutation = (CurvedTextView) arcLayout.getChildAt(1);
3257 assertThat(tv2AfterMutation.getText()).isEqualTo("Mars");
3258
3259 // Unchanged views should be left exactly the same:
3260 assertThat(arcLayoutAfterMutation).isSameInstanceAs(arcLayout);
3261 assertThat(arcLayoutAfterMutation.getChildCount()).isEqualTo(2);
3262 assertThat(tv1AfterMutation).isSameInstanceAs(tv1);
3263 }
3264
3265 @Test
3266 public void inflateArcThenMutate_withChangeToProps_causesUpdate() throws Exception {
3267 Layout layout1 =
3268 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003269 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003270 arcText("Hello"), // 1.1
3271 arcText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003272 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003273
3274 // Check the premutation layout
3275 Renderer renderer = renderer(layout1);
3276 ViewGroup inflatedViewParent = renderer.inflate();
3277 assertThat(inflatedViewParent.getChildCount()).isEqualTo(1);
3278 ArcLayout arcLayout = (ArcLayout) inflatedViewParent.getChildAt(0);
3279 assertThat(arcLayout.getAnchorAngleDegrees()).isEqualTo(0);
3280 assertThat(arcLayout.getChildCount()).isEqualTo(2);
3281 CurvedTextView tv1 = (CurvedTextView) arcLayout.getChildAt(0);
3282 CurvedTextView tv2 = (CurvedTextView) arcLayout.getChildAt(1);
3283 assertThat(tv1.getText()).isEqualTo("Hello");
3284 assertThat(tv2.getText()).isEqualTo("World");
3285
3286 Layout layout2 =
3287 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003288 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003289 props -> props.anchorAngleDegrees = 35,
3290 arcText("Hello"), // 1.1
3291 arcText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003292 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003293
3294 // Compute the mutation
3295 ViewGroupMutation mutation =
3296 renderer.mRenderer.computeMutation(
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +00003297 getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003298 assertThat(mutation).isNotNull();
3299 assertThat(mutation.isNoOp()).isFalse();
3300
3301 // Apply the mutation
3302 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
3303
3304 // Check the post-mutation layout
3305 ArcLayout arcLayoutAfterMutation = (ArcLayout) inflatedViewParent.getChildAt(0);
3306 assertThat(arcLayoutAfterMutation.getChildCount()).isEqualTo(2);
3307 assertThat(arcLayoutAfterMutation.getAnchorAngleDegrees()).isEqualTo(35);
3308 CurvedTextView tv1AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(0);
3309 CurvedTextView tv2AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(1);
3310 assertThat(tv1AfterMutation.getText()).isEqualTo("Hello");
3311 assertThat(tv2AfterMutation.getText()).isEqualTo("World");
3312 assertThat(tv1AfterMutation).isSameInstanceAs(tv1);
3313 assertThat(tv2AfterMutation).isSameInstanceAs(tv2);
3314 }
3315
3316 @Test
3317 @Ignore("b/262537912")
3318 public void viewChangesWhileComputingMutation_applyMutationFails() throws Exception {
3319 Layout layout1 =
3320 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003321 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003322 arcText("Hello"), // 1.1
3323 arcText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003324 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003325 Layout layout2 =
3326 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003327 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003328 props -> props.anchorAngleDegrees = 35,
3329 arcText("Hello"), // 1.1
3330 arcText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003331 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003332 Layout layout3 =
3333 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003334 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003335 arcText("Hello") // 1.1
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003336 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003337 // Check the premutation layout
3338 Renderer renderer = renderer(layout1);
3339 ViewGroup inflatedViewParent1 = renderer.inflate();
3340 // Compute the mutation
3341 ViewGroupMutation mutation2 =
3342 renderer.mRenderer.computeMutation(
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +00003343 getRenderedMetadata(inflatedViewParent1), layout2, ViewProperties.EMPTY);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003344 ViewGroupMutation mutation3 =
3345 renderer.mRenderer.computeMutation(
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +00003346 getRenderedMetadata(inflatedViewParent1), layout3, ViewProperties.EMPTY);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003347
3348 renderer.mRenderer.applyMutation(inflatedViewParent1, mutation3).get();
3349 assertThrows(
3350 ViewMutationException.class,
3351 () -> renderer.mRenderer.applyMutation(inflatedViewParent1, mutation2).get());
3352 }
3353
3354 @Test
3355 public void inflateArcThenMutate_withDifferentNumberOfChildren_causesUpdate() {
3356 Layout layout1 =
3357 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003358 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003359 arcText("Hello"), // 1.1
3360 arcText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003361 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003362
3363 // Check the premutation layout
3364 Renderer renderer = renderer(layout1);
3365 ViewGroup inflatedViewParent = renderer.inflate();
3366 assertThat(inflatedViewParent.getChildCount()).isEqualTo(1);
3367 ArcLayout arcLayout = (ArcLayout) inflatedViewParent.getChildAt(0);
3368 assertThat(arcLayout.getChildCount()).isEqualTo(2);
3369 CurvedTextView tv1 = (CurvedTextView) arcLayout.getChildAt(0);
3370 CurvedTextView tv2 = (CurvedTextView) arcLayout.getChildAt(1);
3371 assertThat(tv1.getText()).isEqualTo("Hello");
3372 assertThat(tv2.getText()).isEqualTo("World");
3373
3374 Layout layout2 =
3375 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003376 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003377 arcText("Hello"), // 1.1
3378 arcText("World"), // 1.2
3379 arcText("and"), // 1.3
3380 arcText("Mars") // 1.4
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003381 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003382
3383 // Compute the mutation
3384 ViewGroupMutation mutation =
3385 renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2);
3386 assertThat(mutation).isNotNull();
3387 assertThat(mutation.isNoOp()).isFalse();
3388
3389 // Apply the mutation
3390 boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation);
3391 assertThat(mutationResult).isTrue();
3392
3393 // Check the post-mutation layout
3394 ArcLayout arcLayoutAfterMutation = (ArcLayout) inflatedViewParent.getChildAt(0);
3395 assertThat(arcLayoutAfterMutation.getChildCount()).isEqualTo(4);
3396 CurvedTextView tv1AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(0);
3397 CurvedTextView tv2AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(1);
3398 CurvedTextView tv3AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(2);
3399 CurvedTextView tv4AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(3);
3400 assertThat(tv1AfterMutation.getText()).isEqualTo("Hello");
3401 assertThat(tv2AfterMutation.getText()).isEqualTo("World");
3402 assertThat(tv3AfterMutation.getText()).isEqualTo("and");
3403 assertThat(tv4AfterMutation.getText()).isEqualTo("Mars");
3404 }
3405
3406 @Test
3407 public void inflateAndMutateTwice_causesTwoUpdates() throws Exception {
3408 Layout layout1 =
3409 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003410 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003411 text("Hello"), // 1.1
3412 text("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003413 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003414
3415 // Do the initial inflation.
3416 Renderer renderer = renderer(layout1);
3417 ViewGroup inflatedViewParent = renderer.inflate();
3418 ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0);
3419 TextView tv1 = (TextView) column.getChildAt(0);
3420 TextView tv2 = (TextView) column.getChildAt(1);
3421 assertThat(tv1.getText().toString()).isEqualTo("Hello");
3422 assertThat(tv2.getText().toString()).isEqualTo("World");
3423
3424 Layout layout2 =
3425 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003426 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003427 text("Goodbye"), // 1.1
3428 text("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003429 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003430
3431 // Apply first mutation
3432 ViewGroupMutation mutation1 =
3433 renderer.mRenderer.computeMutation(
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +00003434 getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003435 assertThat(mutation1).isNotNull();
3436 assertThat(mutation1.isNoOp()).isFalse();
3437 renderer.mRenderer.applyMutation(inflatedViewParent, mutation1).get();
3438
3439 ViewGroup columnAfterMutation1 = (ViewGroup) inflatedViewParent.getChildAt(0);
3440 TextView tv1AfterMutation1 = (TextView) columnAfterMutation1.getChildAt(0);
3441 TextView tv2AfterMutation1 = (TextView) columnAfterMutation1.getChildAt(1);
3442 assertThat(tv1AfterMutation1.getText().toString()).isEqualTo("Goodbye");
3443 assertThat(tv2AfterMutation1.getText().toString()).isEqualTo("World");
3444
3445 Layout layout3 =
3446 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003447 column( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003448 text("Hello"), // 1.1
3449 text("Mars") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003450 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003451
3452 // Apply second mutation
3453 ViewGroupMutation mutation2 =
3454 renderer.mRenderer.computeMutation(
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +00003455 getRenderedMetadata(inflatedViewParent), layout3, ViewProperties.EMPTY);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003456 assertThat(mutation2).isNotNull();
3457 assertThat(mutation2.isNoOp()).isFalse();
3458 renderer.mRenderer.applyMutation(inflatedViewParent, mutation2).get();
3459
3460 ViewGroup columnAfterMutation2 = (ViewGroup) inflatedViewParent.getChildAt(0);
3461 TextView tv1AfterMutation2 = (TextView) columnAfterMutation2.getChildAt(0);
3462 TextView tv2AfterMutation2 = (TextView) columnAfterMutation2.getChildAt(1);
3463 assertThat(tv1AfterMutation2.getText().toString()).isEqualTo("Hello");
3464 assertThat(tv2AfterMutation2.getText().toString()).isEqualTo("Mars");
3465 }
3466
3467 @Test
3468 public void inflateArcThenMutate_withNoChange_producesNoOpMutation() {
3469 Layout layout =
3470 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003471 arc( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003472 arcText("Hello"), // 1.1
3473 arcText("World") // 1.2
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003474 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003475
3476 // Check that we have the initial layout correctly rendered
3477 Renderer renderer = renderer(layout);
3478 ViewGroup inflatedViewParent = renderer.inflate();
3479 RenderedMetadata renderedMetadata = getRenderedMetadata(inflatedViewParent);
3480
3481 // Compute the mutation for the same layout
3482 ViewGroupMutation mutation = renderer.computeMutation(renderedMetadata, layout);
3483 assertThat(mutation).isNotNull();
3484 assertThat(mutation.isNoOp()).isTrue();
3485 }
3486
3487 @Test
3488 public void inflateArcWithNoFingerprint_producesNoRenderingMetadata() {
3489 Layout layout1WithNoFingerprints =
3490 layout(arc(arcText("Hello"))).toBuilder().clearFingerprint().build();
3491 // Check that we have the initial layout correctly rendered
3492 Renderer renderer = renderer(layout1WithNoFingerprints);
3493
3494 ViewGroup inflatedViewParent = renderer.inflate();
3495
3496 assertThat(getRenderedMetadata(inflatedViewParent)).isNull();
3497 }
3498
3499 @Test
3500 public void boxWithChild_childChanges_appliesGravityToUpdatedChild() throws Exception {
3501 Layout layout1 =
3502 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003503 box( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003504 boxProps -> {
3505 boxProps.horizontalAlignment =
3506 HorizontalAlignment.HORIZONTAL_ALIGN_CENTER;
3507 boxProps.verticalAlignment =
3508 VerticalAlignment.VERTICAL_ALIGN_CENTER;
3509 },
3510 text("Hello") // 1.1
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003511 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003512 Renderer renderer = renderer(layout1);
3513 ViewGroup inflatedViewParent = renderer.inflate();
3514 Layout layout2 =
3515 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003516 box( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003517 boxProps -> {
3518 boxProps.horizontalAlignment =
3519 HorizontalAlignment.HORIZONTAL_ALIGN_CENTER;
3520 boxProps.verticalAlignment =
3521 VerticalAlignment.VERTICAL_ALIGN_CENTER;
3522 },
3523 text("World") // 1.1
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003524 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003525
3526 ViewGroupMutation mutation =
3527 renderer.mRenderer.computeMutation(
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +00003528 getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003529 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
3530
3531 ViewGroup boxAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
3532 TextView textAfterMutation = (TextView) boxAfterMutation.getChildAt(0);
3533 LayoutParams layoutParamsAfterMutation = (LayoutParams) textAfterMutation.getLayoutParams();
3534 assertThat(layoutParamsAfterMutation.gravity)
3535 .isEqualTo(
3536 getFrameLayoutGravity(
3537 HorizontalAlignment.HORIZONTAL_ALIGN_CENTER,
3538 VerticalAlignment.VERTICAL_ALIGN_CENTER));
3539 }
3540
3541 @Test
3542 public void boxWithChild_boxChanges_appliesNewGravityToChild() throws Exception {
3543 Layout layout1 =
3544 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003545 box( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003546 boxProps -> {
3547 boxProps.horizontalAlignment =
3548 HorizontalAlignment.HORIZONTAL_ALIGN_CENTER;
3549 boxProps.verticalAlignment =
3550 VerticalAlignment.VERTICAL_ALIGN_CENTER;
3551 },
3552 text("Hello") // 1.1
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003553 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003554 Renderer renderer = renderer(layout1);
3555 ViewGroup inflatedViewParent = renderer.inflate();
3556 Layout layout2 =
3557 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003558 box( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003559 boxProps -> {
3560 // A different set of alignments.
3561 boxProps.horizontalAlignment =
3562 HorizontalAlignment.HORIZONTAL_ALIGN_LEFT;
3563 boxProps.verticalAlignment =
3564 VerticalAlignment.VERTICAL_ALIGN_BOTTOM;
3565 },
3566 text("Hello") // 1.1
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003567 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003568
3569 ViewGroupMutation mutation =
3570 renderer.mRenderer.computeMutation(
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +00003571 getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003572 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
3573
3574 ViewGroup boxAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
3575 TextView textAfterMutation = (TextView) boxAfterMutation.getChildAt(0);
3576 LayoutParams layoutParamsAfterMutation = (LayoutParams) textAfterMutation.getLayoutParams();
3577 assertThat(layoutParamsAfterMutation.gravity)
3578 .isEqualTo(
3579 getFrameLayoutGravity(
3580 HorizontalAlignment.HORIZONTAL_ALIGN_LEFT,
3581 VerticalAlignment.VERTICAL_ALIGN_BOTTOM));
3582 }
3583
3584 @Test
3585 public void boxWithChild_bothChange_appliesNewGravityToUpdatedChild() throws Exception {
3586 Layout layout1 =
3587 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003588 box( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003589 boxProps -> {
3590 boxProps.horizontalAlignment =
3591 HorizontalAlignment.HORIZONTAL_ALIGN_CENTER;
3592 boxProps.verticalAlignment =
3593 VerticalAlignment.VERTICAL_ALIGN_CENTER;
3594 },
3595 text("Hello") // 1.1
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003596 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003597 // Do the initial inflation.
3598 Renderer renderer = renderer(layout1);
3599 ViewGroup inflatedViewParent = renderer.inflate();
3600 Layout layout2 =
3601 layout(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003602 box( // 1
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003603 boxProps -> {
3604 // A different set of alignments.
3605 boxProps.horizontalAlignment =
3606 HorizontalAlignment.HORIZONTAL_ALIGN_LEFT;
3607 boxProps.verticalAlignment =
3608 VerticalAlignment.VERTICAL_ALIGN_BOTTOM;
3609 },
3610 text("World") // 1.1
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003611 ));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003612
3613 ViewGroupMutation mutation =
3614 renderer.mRenderer.computeMutation(
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +00003615 getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003616 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
3617
3618 ViewGroup boxAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0);
3619 TextView textAfterMutation = (TextView) boxAfterMutation.getChildAt(0);
3620 LayoutParams layoutParamsAfterMutation = (LayoutParams) textAfterMutation.getLayoutParams();
3621 assertThat(layoutParamsAfterMutation.gravity)
3622 .isEqualTo(
3623 getFrameLayoutGravity(
3624 HorizontalAlignment.HORIZONTAL_ALIGN_LEFT,
3625 VerticalAlignment.VERTICAL_ALIGN_BOTTOM));
3626 }
3627
3628 private static Span textSpan(String text) {
3629 return Span.newBuilder()
3630 .setText(SpanText.newBuilder().setText(string(text)).build())
3631 .build();
3632 }
3633
3634 private ResourceResolvers.Builder resourceResolvers() {
3635 return StandardResourceResolvers.forLocalApp(
3636 buildResources(),
3637 getApplicationContext(),
3638 ContextCompat.getMainExecutor(getApplicationContext()),
3639 true);
3640 }
3641
3642 private static Layout fingerprintedLayout(LayoutElement rootElement) {
3643 return TestFingerprinter.getDefault().buildLayoutWithFingerprints(rootElement);
3644 }
3645
3646 private static ProtoLayoutTheme loadTheme(int themeResId) {
3647 return new ProtoLayoutThemeImpl(getApplicationContext(), themeResId);
3648 }
3649
3650 ProtoLayoutInflater.Config.Builder newRendererConfigBuilder(Layout layout) {
3651 return newRendererConfigBuilder(layout, resourceResolvers());
3652 }
3653
3654 ProtoLayoutInflater.Config.Builder newRendererConfigBuilder(
3655 Layout layout, ResourceResolvers.Builder resourceResolvers) {
3656 return new ProtoLayoutInflater.Config.Builder(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003657 getApplicationContext(), layout, resourceResolvers.build())
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003658 .setClickableIdExtra(EXTRA_CLICKABLE_ID)
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003659 .setLoadActionListener(p -> {})
Lucas Muller Oliveira76fa5fb2023-09-23 16:18:11 +00003660 .setLoadActionExecutor(ContextCompat.getMainExecutor(getApplicationContext()))
3661 .setApplyFontVariantBodyAsDefault(true);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003662 }
3663
3664 private Renderer renderer(Layout layout) {
3665 return renderer(newRendererConfigBuilder(layout), new FixedQuotaManagerImpl(MAX_VALUE));
3666 }
3667
3668 // Renderer using a dataPipeline with default values.
3669 private Renderer renderer(ProtoLayoutInflater.Config.Builder rendererConfigBuilder) {
3670 return renderer(rendererConfigBuilder, new FixedQuotaManagerImpl(MAX_VALUE));
3671 }
3672
3673 @SuppressWarnings("RestrictTo")
3674 private Renderer renderer(
3675 ProtoLayoutInflater.Config.Builder rendererConfigBuilder,
3676 FixedQuotaManagerImpl quotaManager) {
3677 mDataPipeline =
3678 new ProtoLayoutDynamicDataPipeline(
Mohammad Saboorianb3f03ae2023-05-26 17:04:25 +01003679 /* platformDataProviders= */ ImmutableMap.of(),
Achraf Feydide73e2d2023-04-05 16:28:01 +00003680 mStateStore,
3681 quotaManager,
3682 new FixedQuotaManagerImpl(MAX_VALUE));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003683 rendererConfigBuilder.setDynamicDataPipeline(mDataPipeline);
3684 return new Renderer(rendererConfigBuilder.build(), mDataPipeline);
3685 }
3686
3687 @SuppressWarnings("RestrictTo")
3688 private static final class Renderer {
3689 final ProtoLayoutInflater mRenderer;
3690 final ProtoLayoutDynamicDataPipeline mDataPipeline;
3691
3692 Renderer(
3693 ProtoLayoutInflater.Config rendererConfig,
3694 ProtoLayoutDynamicDataPipeline dataPipeline) {
3695 this.mRenderer = new ProtoLayoutInflater(rendererConfig);
3696 this.mDataPipeline = dataPipeline;
3697 }
3698
3699 FrameLayout inflate() {
3700 FrameLayout rootLayout = new FrameLayout(getApplicationContext());
3701 // This needs to be an attached view to test animations in data pipeline.
3702 Robolectric.buildActivity(Activity.class).setup().get().setContentView(rootLayout);
3703 InflateResult inflateResult = mRenderer.inflate(rootLayout);
3704 if (inflateResult != null) {
3705 inflateResult.updateDynamicDataPipeline(/* isReattaching= */ false);
3706 }
3707 shadowOf(Looper.getMainLooper()).idle();
3708 doLayout(rootLayout);
3709
3710 return rootLayout;
3711 }
3712
3713 ViewGroupMutation computeMutation(RenderedMetadata renderedMetadata, Layout targetLayout) {
Lucas Muller Oliveira72d7f8d2023-09-22 08:55:02 +00003714 return mRenderer.computeMutation(renderedMetadata, targetLayout, ViewProperties.EMPTY);
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003715 }
3716
3717 boolean applyMutation(ViewGroup parent, ViewGroupMutation mutation) {
3718 try {
3719 ListenableFuture<Void> applyMutationFuture =
3720 mRenderer.applyMutation(parent, mutation);
3721 shadowOf(Looper.getMainLooper()).idle();
3722 applyMutationFuture.get();
3723 doLayout(parent);
3724 return true;
3725 } catch (ViewMutationException | ExecutionException | InterruptedException ex) {
3726 return false;
3727 }
3728 }
3729
3730 int getDataPipelineSize() {
3731 return mDataPipeline.size();
3732 }
3733
3734 private void doLayout(View rootLayout) {
3735 // Run a layout pass etc. This is required for basically everything that tries to make
3736 // assertions about width/height, or relative placement.
3737 int screenWidth = MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, MeasureSpec.EXACTLY);
3738 int screenHeight = MeasureSpec.makeMeasureSpec(SCREEN_HEIGHT, MeasureSpec.EXACTLY);
3739 rootLayout.measure(screenWidth, screenHeight);
3740 rootLayout.layout(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
3741 }
3742 }
3743
3744 private static Resources buildResources() {
3745 return Resources.newBuilder()
3746 .putIdToImage(
3747 "android",
3748 ImageResource.newBuilder()
3749 .setAndroidResourceByResId(
3750 AndroidImageResourceByResId.newBuilder()
3751 .setResourceId(R.drawable.android_24dp))
3752 .build())
3753 .putIdToImage(
3754 "android_AVD",
3755 ImageResource.newBuilder()
3756 .setAndroidAnimatedResourceByResId(
3757 AndroidAnimatedImageResourceByResId.newBuilder()
3758 .setAnimatedImageFormat(
3759 AnimatedImageFormat
3760 .ANIMATED_IMAGE_FORMAT_AVD)
3761 .setResourceId(android_animated_24dp)
3762 .setStartTrigger(onVisibleTrigger()))
3763 .build())
3764 .putIdToImage(
3765 "android_AVD_pretending_to_be_static",
3766 ImageResource.newBuilder()
3767 .setAndroidResourceByResId(
3768 AndroidImageResourceByResId.newBuilder()
3769 .setResourceId(android_animated_24dp))
3770 .build())
3771 .putIdToImage(
3772 "android_seekable_AVD",
3773 ImageResource.newBuilder()
3774 .setAndroidSeekableAnimatedResourceByResId(
3775 AndroidSeekableAnimatedImageResourceByResId.newBuilder()
3776 .setAnimatedImageFormat(
3777 AnimatedImageFormat
3778 .ANIMATED_IMAGE_FORMAT_AVD)
3779 .setResourceId(android_animated_24dp)
3780 .setProgress(
3781 DynamicFloat.newBuilder()
3782 .setAnimatableDynamic(
3783 stateDynamicFloat())
3784 .build())
3785 .build())
3786 .build())
3787 .putIdToImage(
3788 "does_not_exist",
3789 ImageResource.newBuilder()
3790 .setAndroidResourceByResId(
3791 AndroidImageResourceByResId.newBuilder().setResourceId(-1))
3792 .build())
3793 .putIdToImage(
3794 "large_image_120dp",
3795 ImageResource.newBuilder()
3796 .setAndroidResourceByResId(
3797 AndroidImageResourceByResId.newBuilder()
3798 .setResourceId(R.drawable.ic_channel_foreground))
3799 .build())
3800 .putIdToImage("no_android_resource_set", ImageResource.getDefaultInstance())
3801 .build();
3802 }
3803
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003804 @NonNull
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003805 private static Trigger onVisibleTrigger() {
3806 return Trigger.newBuilder()
3807 .setOnVisibleTrigger(OnVisibleTrigger.getDefaultInstance())
3808 .build();
3809 }
3810
3811 @NonNull
3812 private static AnimatableDynamicFloat.Builder stateDynamicFloat() {
3813 return AnimatableDynamicFloat.newBuilder()
3814 .setInput(
3815 DynamicFloat.newBuilder()
3816 .setStateSource(
3817 StateFloatSource.newBuilder().setSourceKey("anim_val")));
3818 }
3819
3820 @Test
3821 public void inflate_row_withLayoutWeight() {
3822 final String protoResId = "android";
3823
3824 LayoutElement image = buildImage(protoResId, 30, 30);
3825
3826 LayoutElement root =
3827 LayoutElement.newBuilder()
3828 .setRow(
3829 Row.newBuilder()
3830 .setWidth(expand())
3831 .setHeight(expand())
3832 .addContents(
3833 LayoutElement.newBuilder()
3834 .setRow(
3835 Row.newBuilder()
3836 .setWidth(expandWeight())
3837 .addContents(image)
3838 .build())))
3839 .build();
3840
3841 FrameLayout layout = renderer(fingerprintedLayout(root)).inflate();
3842
3843 // There should be a child ViewGroup which is a LinearLayout.
3844 assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class);
3845 ViewGroup firstChild = (ViewGroup) layout.getChildAt(0);
3846 ViewGroup rowWithWeight = (ViewGroup) firstChild.getChildAt(0);
3847
3848 LinearLayout.LayoutParams linearLayoutParams =
3849 (LinearLayout.LayoutParams) rowWithWeight.getLayoutParams();
3850
3851 expect.that(linearLayoutParams.weight).isEqualTo(10.0f);
3852 }
3853
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00003854 @NonNull
3855 private static ContainerDimension expandWeight() {
3856 return ContainerDimension.newBuilder()
3857 .setExpandedDimension(
3858 ExpandedDimensionProp.newBuilder()
3859 .setLayoutWeight(FloatProp.newBuilder().setValue(10.0f).build())
3860 .build())
3861 .build();
3862 }
3863
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003864 @Test
3865 public void inflate_column_withLayoutWeight() {
3866 final String protoResId = "android";
3867
3868 LayoutElement image = buildImage(protoResId, 30, 30);
3869
3870 LayoutElement root =
3871 LayoutElement.newBuilder()
3872 .setColumn(
3873 Column.newBuilder()
3874 .setWidth(expand())
3875 .setHeight(expand())
3876 .addContents(
3877 LayoutElement.newBuilder()
3878 .setColumn(
3879 Column.newBuilder()
3880 .setHeight(expandWeight())
3881 .addContents(image)
3882 .build())))
3883 .build();
3884
3885 FrameLayout layout = renderer(fingerprintedLayout(root)).inflate();
3886
3887 // There should be a child ViewGroup which is a LinearLayout.
3888 assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class);
3889 ViewGroup firstChild = (ViewGroup) layout.getChildAt(0);
3890 ViewGroup columnWithWeight = (ViewGroup) firstChild.getChildAt(0);
3891
3892 LinearLayout.LayoutParams linearLayoutParams =
3893 (LinearLayout.LayoutParams) columnWithWeight.getLayoutParams();
3894
3895 expect.that(linearLayoutParams.weight).isEqualTo(10.0f);
3896 }
3897
3898 @Test
3899 public void inflate_box_withLayoutWeight() {
3900 final String protoResId = "android";
3901
3902 LayoutElement image = buildImage(protoResId, 30, 30);
3903
3904 LayoutElement root =
3905 LayoutElement.newBuilder()
3906 .setRow(
3907 Row.newBuilder()
3908 .setWidth(expand())
3909 .setHeight(expand())
3910 .addContents(
3911 LayoutElement.newBuilder()
3912 .setBox(
3913 Box.newBuilder()
3914 .setWidth(expandWeight())
3915 .addContents(image)
3916 .build())))
3917 .build();
3918
3919 FrameLayout layout = renderer(fingerprintedLayout(root)).inflate();
3920
3921 // There should be a child ViewGroup which is a LinearLayout.
3922 assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class);
3923 ViewGroup firstChild = (ViewGroup) layout.getChildAt(0);
3924 ViewGroup boxWithWeight = (ViewGroup) firstChild.getChildAt(0);
3925
3926 LinearLayout.LayoutParams linearLayoutParams =
3927 (LinearLayout.LayoutParams) boxWithWeight.getLayoutParams();
3928
3929 expect.that(linearLayoutParams.weight).isEqualTo(10.0f);
3930 }
3931
Neda Topoljanac4d26f822023-02-22 16:57:03 +00003932 @Test
3933 public void enterTransition_noQuota_notPlayed() throws Exception {
3934 Renderer renderer =
3935 renderer(
3936 newRendererConfigBuilder(fingerprintedLayout(textFadeIn("Hello"))),
3937 new FixedQuotaManagerImpl(/* quotaCap= */ 0));
3938 mDataPipeline.setFullyVisible(true);
3939 FrameLayout inflatedViewParent = renderer.inflate();
3940 shadowOf(getMainLooper()).idle();
3941 ShadowChoreographer.setPaused(true);
3942 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
3943
3944 ViewGroupMutation mutation =
3945 renderer.computeMutation(
3946 getRenderedMetadata(inflatedViewParent),
3947 fingerprintedLayout(textFadeIn("World")));
3948 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
3949 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
3950
3951 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
3952 }
3953
3954 @Test
3955 public void enterTransition_animationEnabled_hasEnterAnimation() throws Exception {
3956 Renderer renderer = renderer(fingerprintedLayout(textFadeIn("Hello")));
3957 mDataPipeline.setFullyVisible(true);
3958 FrameLayout inflatedViewParent = renderer.inflate();
3959 shadowOf(getMainLooper()).idle();
3960 ShadowChoreographer.setPaused(true);
3961 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
3962
3963 ViewGroupMutation mutation =
3964 renderer.computeMutation(
3965 getRenderedMetadata(inflatedViewParent),
3966 fingerprintedLayout(textFadeIn("World")));
3967 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
3968
3969 // Idle for running code for starting animations.
3970 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
3971 // Idle for calling the onStart listener so that animation has started status.
3972 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
3973
3974 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
3975 }
3976
3977 @Test
3978 public void multipleEnterTransition_animationEnabled_correctlyReleaseQuota() throws Exception {
3979 Renderer renderer =
3980 renderer(
3981 fingerprintedLayout(
3982 LayoutElement.newBuilder()
3983 .setColumn(
3984 Column.newBuilder()
3985 .addContents(textFadeIn("Hello"))
3986 .addContents(textFadeInSlideIn("Hello2"))
3987 .build())
3988 .build()));
3989 mDataPipeline.setFullyVisible(true);
3990 FrameLayout inflatedViewParent = renderer.inflate();
3991 shadowOf(getMainLooper()).idle();
3992 ShadowChoreographer.setPaused(true);
3993 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
3994
3995 ViewGroupMutation mutation =
3996 renderer.computeMutation(
3997 getRenderedMetadata(inflatedViewParent),
3998 fingerprintedLayout(
3999 LayoutElement.newBuilder()
4000 .setColumn(
4001 Column.newBuilder()
4002 .addContents(textFadeIn("World"))
4003 .addContents(textFadeInSlideIn("World2"))
4004 .build())
4005 .build()));
4006 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
4007
4008 // Idle for running code for starting animations.
4009 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4010 // Idle for calling the onStart listener so that animation has started status.
4011 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4012
4013 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(/* fadeInx2 + slideIn */ 3);
4014
4015 // This is needed to let enter animations finish.
4016 ShadowChoreographer.setPaused(false);
4017 ShadowSystemClock.advanceBy(Duration.ofSeconds(1));
4018 shadowOf(getMainLooper()).idle();
4019 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4020 assertThat(mDataPipeline.isAllQuotaReleased()).isTrue();
4021 }
4022
4023 @Test
4024 public void multipleEnterTransition_withDelay_animationEnabled_notOverlapping_correctlyPlays()
4025 throws Exception {
4026 Renderer renderer =
4027 renderer(
4028 newRendererConfigBuilder(
4029 fingerprintedLayout(
4030 LayoutElement.newBuilder()
4031 .setColumn(
4032 Column.newBuilder()
4033 .addContents(
4034 textFadeIn(
4035 "Hello",
4036 /* delay= */ 0))
4037 .addContents(
4038 textFadeIn(
4039 "Hello2",
4040 /* delay= */ 600)))
4041 .build())),
4042 new FixedQuotaManagerImpl(/* quotaCap= */ 1));
4043 mDataPipeline.setFullyVisible(true);
4044 FrameLayout inflatedViewParent = renderer.inflate();
4045
4046 shadowOf(getMainLooper()).idle();
4047 ShadowChoreographer.setPaused(true);
4048 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4049
4050 ViewGroupMutation mutation =
4051 renderer.computeMutation(
4052 getRenderedMetadata(inflatedViewParent),
4053 fingerprintedLayout(
4054 LayoutElement.newBuilder()
4055 .setColumn(
4056 Column.newBuilder()
4057 .addContents(
4058 textFadeIn("World", /* delay= */ 0))
4059 .addContents(
4060 textFadeIn(
4061 "World2",
4062 /* delay= */ 600)))
4063 .build()));
4064 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
4065
4066 // First content transition animation Idle for running code for starting animations.
4067 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4068 // Idle for calling the onStart listener so that animation has started status.
4069 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4070
4071 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
4072
4073 // Second content transition animation Idle for running code for starting animations.
4074 shadowOf(getMainLooper()).idleFor(Duration.ofMillis(500));
4075 // Idle for calling the onStart listener so that animation has started status.
4076 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4077
4078 // Quota cap is 1, but since the first animating is finished, this should be played too.
4079 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
4080
4081 // This is needed to let enter animations finish.
4082 ShadowChoreographer.setPaused(false);
4083 ShadowSystemClock.advanceBy(Duration.ofSeconds(1));
4084 shadowOf(getMainLooper()).idle();
4085 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4086 assertThat(mDataPipeline.isAllQuotaReleased()).isTrue();
4087 }
4088
4089 @Test
4090 public void multipleEnterTransition_withDelay_animationEnabled_overlapping_playesOne()
4091 throws Exception {
4092 Renderer renderer =
4093 renderer(
4094 newRendererConfigBuilder(
4095 fingerprintedLayout(
4096 LayoutElement.newBuilder()
4097 .setColumn(
4098 Column.newBuilder()
4099 .addContents(
4100 textFadeIn(
4101 "Hello",
4102 /* delay= */ 400))
4103 .addContents(
4104 textFadeIn(
4105 "Hello2",
4106 /* delay= */ 600)))
4107 .build())),
4108 new FixedQuotaManagerImpl(/* quotaCap= */ 1));
4109 mDataPipeline.setFullyVisible(true);
4110 FrameLayout inflatedViewParent = renderer.inflate();
4111
4112 shadowOf(getMainLooper()).idle();
4113 ShadowChoreographer.setPaused(true);
4114 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4115
4116 ViewGroupMutation mutation =
4117 renderer.computeMutation(
4118 getRenderedMetadata(inflatedViewParent),
4119 fingerprintedLayout(
4120 LayoutElement.newBuilder()
4121 .setColumn(
4122 Column.newBuilder()
4123 .addContents(
4124 textFadeIn(
4125 "World", /* delay= */ 400))
4126 .addContents(
4127 textFadeIn(
4128 "World2",
4129 /* delay= */ 600)))
4130 .build()));
4131 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
4132
4133 // First content transition animation Idle for running code for starting animations.
4134 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(500));
4135 ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
4136 // Idle for calling the onStart listener so that animation has started status.
4137 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4138
4139 // Since we've run delayed tasks, second animation also got a chance to be run, but quota
4140 // prevented it.
4141 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
4142
4143 // This is needed to let enter animations finish.
4144 ShadowChoreographer.setPaused(false);
4145 ShadowSystemClock.advanceBy(Duration.ofSeconds(1));
4146 shadowOf(getMainLooper()).idle();
4147 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4148 assertThat(mDataPipeline.isAllQuotaReleased()).isTrue();
4149 }
4150
4151 @Test
4152 public void enterTransition_animationDisabled_noEnterAnimations() throws Exception {
4153 Renderer renderer =
4154 renderer(
4155 newRendererConfigBuilder(fingerprintedLayout(textFadeIn("Hello")))
4156 .setAnimationEnabled(false));
4157 mDataPipeline.setFullyVisible(true);
4158 FrameLayout inflatedViewParent = renderer.inflate();
4159 shadowOf(getMainLooper()).idle();
4160 ShadowChoreographer.setPaused(true);
4161 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4162
4163 ViewGroupMutation mutation =
4164 renderer.computeMutation(
4165 getRenderedMetadata(inflatedViewParent),
4166 fingerprintedLayout(textFadeIn("World")));
4167 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
4168 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4169
4170 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4171 }
4172
4173 @Test
4174 public void enterTransition_notFullyVisible_noEnterAnimation() throws Exception {
4175 Renderer renderer = renderer(fingerprintedLayout(textFadeIn("Hello")));
4176 mDataPipeline.setFullyVisible(false);
4177 FrameLayout inflatedViewParent = renderer.inflate();
4178 shadowOf(getMainLooper()).idle();
4179 ShadowChoreographer.setPaused(true);
4180 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4181
4182 ViewGroupMutation mutation =
4183 renderer.computeMutation(
4184 getRenderedMetadata(inflatedViewParent),
4185 fingerprintedLayout(textFadeIn("World")));
4186 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
4187 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4188
4189 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4190 }
4191
4192 @Test
4193 public void exitTransition_noQuota_notPlayed() throws Exception {
4194 Renderer renderer =
4195 renderer(
4196 newRendererConfigBuilder(
4197 fingerprintedLayout(
4198 getTextElementWithExitAnimation(
4199 "Hello", /* iterations= */ 1))),
4200 new FixedQuotaManagerImpl(/* quotaCap= */ 0));
4201 mDataPipeline.setFullyVisible(true);
4202 FrameLayout inflatedViewParent = renderer.inflate();
4203 shadowOf(getMainLooper()).idle();
4204
4205 ViewGroupMutation mutation =
4206 renderer.computeMutation(
4207 getRenderedMetadata(inflatedViewParent),
4208 fingerprintedLayout(textFadeIn("World")));
4209 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
4210
4211 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4212 }
4213
4214 @Test
4215 public void exitTransition_animationEnabled_hasExitAnimation() throws Exception {
4216 Renderer renderer =
4217 renderer(
4218 fingerprintedLayout(
4219 getTextElementWithExitAnimation("Hello", /* iterations= */ 1)));
4220 mDataPipeline.setFullyVisible(true);
4221 FrameLayout inflatedViewParent = renderer.inflate();
4222 shadowOf(getMainLooper()).idle();
4223 ShadowChoreographer.setPaused(true);
4224 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4225
4226 ViewGroupMutation mutation =
4227 renderer.computeMutation(
4228 getRenderedMetadata(inflatedViewParent),
4229 fingerprintedLayout(textFadeIn("World")));
4230 ListenableFuture<Void> applyMutationFuture =
4231 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
4232
4233 // Idle for running code for starting animations.
4234 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4235 // Idle for calling the onStart listener so that animation has started status.
4236 shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100));
4237
4238 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
4239
4240 // Waiting on onAnimationEnd listener so that future is resolved.
4241 ShadowChoreographer.setPaused(false);
4242 shadowOf(getMainLooper()).idleFor(Duration.ofSeconds(5));
4243
4244 applyMutationFuture.get();
4245 }
4246
4247 @Test
4248 public void exitTransition_indefiniteRepeatable_ignored() throws Exception {
4249 Renderer renderer =
4250 renderer(
4251 fingerprintedLayout(
4252 getTextElementWithExitAnimation("Hello", /* iterations= */ 0)));
4253 mDataPipeline.setFullyVisible(true);
4254 FrameLayout inflatedViewParent = renderer.inflate();
4255 shadowOf(getMainLooper()).idle();
4256
4257 ViewGroupMutation mutation =
4258 renderer.computeMutation(
4259 getRenderedMetadata(inflatedViewParent),
4260 fingerprintedLayout(
4261 getTextElementWithExitAnimation("World", /* iterations= */ 0)));
4262 ListenableFuture<Void> applyMutationFuture =
4263 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
4264
4265 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4266 shadowOf(getMainLooper()).idle();
4267 applyMutationFuture.get();
4268 }
4269
4270 @Test
4271 public void exitTransition_animationDisabled_noExitAnimations() throws Exception {
4272 Renderer renderer =
4273 renderer(
4274 newRendererConfigBuilder(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00004275 fingerprintedLayout(
4276 getTextElementWithExitAnimation(
4277 "Hello", /* iterations= */ 1)))
Neda Topoljanac4d26f822023-02-22 16:57:03 +00004278 .setAnimationEnabled(false));
4279 mDataPipeline.setFullyVisible(true);
4280 FrameLayout inflatedViewParent = renderer.inflate();
4281 shadowOf(getMainLooper()).idle();
4282
4283 ViewGroupMutation mutation =
4284 renderer.computeMutation(
4285 getRenderedMetadata(inflatedViewParent),
4286 fingerprintedLayout(
4287 getTextElementWithExitAnimation("World", /* iterations= */ 1)));
4288 ListenableFuture<Void> applyMutationFuture =
4289 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
4290
4291 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4292 shadowOf(getMainLooper()).idle();
4293 applyMutationFuture.get();
4294 }
4295
4296 @Test
4297 public void exitTransition_notFullyVisible_noExitAnimation() throws Exception {
4298 Renderer renderer =
4299 renderer(
4300 fingerprintedLayout(
4301 getTextElementWithExitAnimation("Hello", /* iterations= */ 1)));
4302 mDataPipeline.setFullyVisible(false);
4303 FrameLayout inflatedViewParent = renderer.inflate();
4304 shadowOf(getMainLooper()).idle();
4305
4306 ViewGroupMutation mutation =
4307 renderer.computeMutation(
4308 getRenderedMetadata(inflatedViewParent),
4309 fingerprintedLayout(
4310 getTextElementWithExitAnimation("World", /* iterations= */ 1)));
4311 ListenableFuture<Void> applyMutationFuture =
4312 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
4313
4314 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4315 shadowOf(getMainLooper()).idle();
4316 applyMutationFuture.get();
4317 }
4318
4319 @Test
4320 public void exitTransition_noChangeToLayoutContentWhileExitAnimationIsPlaying()
4321 throws Exception {
4322 Renderer renderer =
4323 renderer(
4324 fingerprintedLayout(
4325 getTextElementWithExitAnimation("Hello", /* iterations= */ 10)));
4326 mDataPipeline.setFullyVisible(true);
4327 FrameLayout inflatedViewParent = renderer.inflate();
4328 shadowOf(getMainLooper()).idle();
4329 ShadowChoreographer.setPaused(true);
4330 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4331
4332 ViewGroupMutation mutation =
4333 renderer.computeMutation(
4334 getRenderedMetadata(inflatedViewParent),
4335 fingerprintedLayout(
4336 getTextElementWithExitAnimation("World", /* iterations= */ 10)));
4337 ListenableFuture<Void> applyMutationFuture =
4338 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
4339
4340 shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
4341 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
4342 assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString())
4343 .isEqualTo("Hello");
4344
4345 ShadowChoreographer.setPaused(false);
4346 shadowOf(getMainLooper()).idleFor(Duration.ofSeconds(5));
4347 applyMutationFuture.get();
4348 }
4349
4350 @Test
4351 public void exitTransition_afterExitAnimationsEnd_newLayoutGetApplied() throws Exception {
4352 Renderer renderer =
4353 renderer(
4354 fingerprintedLayout(
4355 getTextElementWithExitAnimation("Hello", /* iterations= */ 10)));
4356 mDataPipeline.setFullyVisible(true);
4357 FrameLayout inflatedViewParent = renderer.inflate();
4358 shadowOf(getMainLooper()).idle();
4359
4360 ViewGroupMutation mutation =
4361 renderer.computeMutation(
4362 getRenderedMetadata(inflatedViewParent),
4363 fingerprintedLayout(
4364 getTextElementWithExitAnimation("World", /* iterations= */ 10)));
4365 ListenableFuture<Void> applyMutationFuture =
4366 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
4367 shadowOf(getMainLooper()).idle();
4368
4369 applyMutationFuture.get();
4370 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4371 assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString())
4372 .isEqualTo("World");
4373 }
4374
4375 @Test
4376 public void exitTransition_removedNodes_triggersExitAnimation() throws Exception {
4377 Renderer renderer =
4378 renderer(
4379 fingerprintedLayout(
4380 getMultipleTextElementWithExitAnimation(
4381 Arrays.asList("Hello", "World"), /* iterations= */ 10)));
4382 mDataPipeline.setFullyVisible(true);
4383 FrameLayout inflatedViewParent = renderer.inflate();
4384 shadowOf(getMainLooper()).idle();
4385 ShadowChoreographer.setPaused(true);
4386 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4387
4388 ViewGroupMutation mutation =
4389 renderer.computeMutation(
4390 getRenderedMetadata(inflatedViewParent),
4391 fingerprintedLayout(
4392 getMultipleTextElementWithExitAnimation(
4393 Arrays.asList("Hello"), /* iterations= */ 10)));
4394 ListenableFuture<Void> applyMutationFuture =
4395 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
4396
4397 shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
4398 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(2);
4399
4400 ShadowChoreographer.setPaused(false);
4401 shadowOf(getMainLooper()).idleFor(Duration.ofSeconds(5));
4402 applyMutationFuture.get();
4403 }
4404
4405 @Test
Paul Milian967c0d42023-08-24 16:29:16 +01004406 public void layoutGetsApplied_whenApplyingSecondMutation_beforeExitAnimationsAreFinished()
4407 throws Exception {
4408 Renderer renderer =
4409 renderer(
4410 fingerprintedLayout(
4411 getTextElementWithExitAnimation("Hello", /* iterations= */ 10)));
4412 mDataPipeline.setFullyVisible(true);
4413 FrameLayout inflatedViewParent = renderer.inflate();
4414 shadowOf(getMainLooper()).idle();
4415 ShadowChoreographer.setPaused(true);
4416 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4417
4418 ViewGroupMutation mutation =
4419 renderer.computeMutation(
4420 getRenderedMetadata(inflatedViewParent),
4421 fingerprintedLayout(
4422 getTextElementWithExitAnimation("World", /* iterations= */ 10)));
4423 ListenableFuture<Void> applyMutationFuture =
4424 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
4425
4426 shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
4427 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
4428
4429 ViewGroupMutation secondMutation =
4430 renderer.computeMutation(
4431 getRenderedMetadata(inflatedViewParent),
4432 fingerprintedLayout(
4433 getTextElementWithExitAnimation(
4434 "Second mutation",
4435 /* iterations= */ 10)));
4436
4437 ListenableFuture<Void> applySecondMutationFuture =
4438 renderer.mRenderer.applyMutation(inflatedViewParent, secondMutation);
4439
4440 // the previous mutation should be finished
4441 assertThat(applyMutationFuture.isDone()).isTrue();
4442 assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString())
4443 .isEqualTo("World");
4444
4445 shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
4446 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1);
4447
4448 ShadowChoreographer.setPaused(false);
4449 shadowOf(getMainLooper()).idleFor(Duration.ofSeconds(5));
4450 applySecondMutationFuture.get();
4451
4452 assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
4453 assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString())
4454 .isEqualTo("Second mutation");
4455 }
4456
4457 @Test
Neda Topoljanac4d26f822023-02-22 16:57:03 +00004458 public void slideInTransition_snapToOutside_startsFromOutsideParentBounds() throws Exception {
4459 Renderer renderer =
4460 renderer(
4461 fingerprintedLayout(
4462 getTextElementWithSlideInAnimation(
4463 "Hello",
4464 /* snapTo= */ SLIDE_PARENT_SNAP_TO_OUTSIDE.getNumber())));
4465 mDataPipeline.setFullyVisible(true);
4466 FrameLayout inflatedViewParent = renderer.inflate();
4467 shadowOf(getMainLooper()).idle();
4468 ShadowChoreographer.setPaused(true);
4469 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4470
4471 ViewGroupMutation mutation =
4472 renderer.computeMutation(
4473 getRenderedMetadata(inflatedViewParent),
4474 fingerprintedLayout(
4475 getTextElementWithSlideInAnimation(
4476 "World",
4477 /* snapTo= */ SLIDE_PARENT_SNAP_TO_OUTSIDE.getNumber())));
4478 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
4479 shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
4480
4481 ViewGroup box = (ViewGroup) inflatedViewParent.getChildAt(0);
4482 TextView textView = (TextView) box.getChildAt(0);
4483 assertSlideInInitialOffset(
4484 box,
4485 textView,
4486 inflatedViewParent.getLeft() - (textView.getLeft() + textView.getWidth()));
4487 }
4488
4489 @Test
4490 public void slideInTransition_snapToInside_startsFromInsideParentBounds() throws Exception {
4491 Renderer renderer =
4492 renderer(
4493 fingerprintedLayout(
4494 getTextElementWithSlideInAnimation(
4495 "Hello",
4496 /* snapTo= */ SLIDE_PARENT_SNAP_TO_INSIDE.getNumber())));
4497 mDataPipeline.setFullyVisible(true);
4498 FrameLayout inflatedViewParent = renderer.inflate();
4499 shadowOf(getMainLooper()).idle();
4500 ShadowChoreographer.setPaused(true);
4501 ShadowChoreographer.setFrameDelay(Duration.ofMillis(15));
4502
4503 ViewGroupMutation mutation =
4504 renderer.computeMutation(
4505 getRenderedMetadata(inflatedViewParent),
4506 fingerprintedLayout(
4507 getTextElementWithSlideInAnimation(
4508 "World",
4509 /* snapTo= */ SLIDE_PARENT_SNAP_TO_INSIDE.getNumber())));
4510 renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get();
4511 shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
4512
4513 ViewGroup box = (ViewGroup) inflatedViewParent.getChildAt(0);
4514 TextView textView = (TextView) box.getChildAt(0);
4515 assertSlideInInitialOffset(
4516 box, textView, inflatedViewParent.getLeft() - textView.getLeft());
4517 }
4518
4519 private void assertSlideInInitialOffset(
4520 ViewGroup box, TextView textView, float expectedInitialOffset) {
4521 Animation animation = textView.getAnimation();
4522 animation.initialize(
4523 textView.getWidth(), textView.getHeight(), box.getWidth(), box.getHeight());
4524 Transformation transformation = new Transformation();
4525 textView.getAnimation().getTransformation(0, transformation);
4526 float[] matrix = new float[9];
4527 transformation.getMatrix().getValues(matrix);
4528 assertThat(matrix[2]).isWithin(0.1f).of(expectedInitialOffset);
4529 }
4530
4531 private LayoutElement textFadeIn(String text) {
4532 return textFadeIn(text, 0);
4533 }
4534
4535 private LayoutElement textFadeIn(String text, int delay) {
4536 return LayoutElement.newBuilder()
4537 .setText(
4538 Text.newBuilder()
4539 .setModifiers(
4540 Modifiers.newBuilder()
4541 .setContentUpdateAnimation(
4542 AnimatedVisibility.newBuilder()
4543 .setEnterTransition(
4544 enterFadeIn(delay))))
4545 .setText(string(text)))
4546 .build();
4547 }
4548
4549 @NonNull
4550 private static EnterTransition.Builder enterFadeIn(int delay) {
4551 return EnterTransition.newBuilder().setFadeIn(fadeIn(delay));
4552 }
4553
4554 @NonNull
4555 private static FadeInTransition.Builder fadeIn(int delay) {
4556 return FadeInTransition.newBuilder()
Xiangyin Ma4780e272023-03-07 16:42:32 +00004557 .setAnimationSpec(
Achraf Feydi3e29ada2023-04-18 19:40:48 +00004558 AnimationSpec.newBuilder()
4559 .setAnimationParameters(
4560 AnimationParameters.newBuilder().setDelayMillis(delay)));
Neda Topoljanac4d26f822023-02-22 16:57:03 +00004561 }
4562
4563 private LayoutElement textFadeInSlideIn(String text) {
4564 return LayoutElement.newBuilder()
4565 .setText(
4566 textAnimVisibility(
4567 AnimatedVisibility.newBuilder()
4568 .setEnterTransition(
4569 EnterTransition.newBuilder()
4570 .setFadeIn(
Neda Topoljanac3a83ae72023-04-05 20:48:11 +00004571 FadeInTransition
4572 .getDefaultInstance())
Neda Topoljanac4d26f822023-02-22 16:57:03 +00004573 .setSlideIn(
4574 SlideInTransition.newBuilder()
4575 .build())),
4576 text))
4577 .build();
4578 }
4579
4580 private LayoutElement getTextElementWithExitAnimation(String text, int iterations) {
4581 return LayoutElement.newBuilder()
4582 .setText(
4583 textAnimVisibility(
4584 AnimatedVisibility.newBuilder()
4585 .setExitTransition(getFadeOutExitAnimation(iterations)),
4586 text))
4587 .build();
4588 }
4589
4590 private LayoutElement getMultipleTextElementWithExitAnimation(
4591 List<String> texts, int iterations) {
4592 Column.Builder column = Column.newBuilder();
4593 for (String text : texts) {
4594 column.addContents(
4595 LayoutElement.newBuilder()
4596 .setText(
4597 textAnimVisibility(
4598 AnimatedVisibility.newBuilder()
4599 .setExitTransition(
4600 getFadeOutExitAnimation(iterations)),
4601 text)));
4602 }
4603
4604 return LayoutElement.newBuilder().setColumn(column).build();
4605 }
4606
4607 private ExitTransition.Builder getFadeOutExitAnimation(int iterations) {
4608 return ExitTransition.newBuilder()
4609 .setFadeOut(
4610 FadeOutTransition.newBuilder()
4611 .setAnimationSpec(
4612 AnimationSpec.newBuilder()
4613 .setRepeatable(
4614 Repeatable.newBuilder()
4615 .setIterations(iterations)
4616 .build())));
4617 }
4618
4619 private LayoutElement getTextElementWithSlideInAnimation(String text, int snapTo) {
4620 return LayoutElement.newBuilder()
4621 .setBox(
4622 Box.newBuilder()
4623 .setWidth(expand())
4624 .setHeight(expand())
4625 .addContents(
4626 LayoutElement.newBuilder()
4627 .setText(
4628 textAnimVisibility(
4629 AnimatedVisibility.newBuilder()
4630 .setEnterTransition(
4631 slideIn(snapTo)),
4632 text))))
4633 .build();
4634 }
4635
4636 @NonNull
4637 private Text.Builder textAnimVisibility(AnimatedVisibility.Builder snapTo, String text) {
4638 return Text.newBuilder()
4639 .setModifiers(Modifiers.newBuilder().setContentUpdateAnimation(snapTo.build()))
4640 .setText(string(text).build());
4641 }
4642
4643 private EnterTransition.Builder slideIn(int snapTo) {
4644 return EnterTransition.newBuilder()
4645 .setSlideIn(
4646 SlideInTransition.newBuilder()
4647 .setDirection(SlideDirection.SLIDE_DIRECTION_LEFT_TO_RIGHT)
4648 .setInitialSlideBound(
4649 SlideBound.newBuilder()
4650 .setParentBound(
4651 SlideParentBound.newBuilder()
4652 .setSnapTo(
4653 SlideParentSnapOption
4654 .forNumber(snapTo))
4655 .build())
4656 .build()));
4657 }
4658
4659 @NonNull
4660 private static DpProp.Builder dp(float value) {
4661 return DpProp.newBuilder().setValue(value);
4662 }
4663
4664 @NonNull
Neda Topoljanac742e9e72023-10-06 22:18:35 +00004665 private static DimensionProto.SpProp sp(float value) {
4666 return DimensionProto.SpProp.newBuilder().setValue(value).build();
4667 }
4668
4669 @NonNull
Neda Topoljanac4d26f822023-02-22 16:57:03 +00004670 private static ContainerDimension.Builder expand() {
4671 return ContainerDimension.newBuilder()
4672 .setExpandedDimension(ExpandedDimensionProp.getDefaultInstance());
4673 }
4674
4675 @NonNull
4676 private static StrokeCapProp.Builder strokeCapButt() {
4677 return StrokeCapProp.newBuilder().setValue(LayoutElementProto.StrokeCap.STROKE_CAP_BUTT);
4678 }
4679
4680 @NonNull
4681 private static DegreesProp.Builder degrees(int value) {
4682 return DegreesProp.newBuilder().setValue(value);
4683 }
4684
4685 @NonNull
4686 private static ExpandedAngularDimensionProp expandAngular(float value) {
4687 return ExpandedAngularDimensionProp.newBuilder()
4688 .setLayoutWeight(FloatProp.newBuilder().setValue(value).build())
4689 .build();
4690 }
4691
4692 @NonNull
4693 private static StringProp.Builder string(String value) {
4694 return StringProp.newBuilder().setValue(value);
4695 }
4696
4697 @NonNull
Neda Topoljanac742e9e72023-10-06 22:18:35 +00004698 private static StringProp.Builder dynamicString(String value) {
4699 return StringProp.newBuilder()
4700 .setValue(value)
4701 .setDynamicValue(
4702 DynamicString.newBuilder()
4703 .setFixed(FixedString.newBuilder().setValue(value)));
4704 }
4705
4706 @NonNull
Neda Topoljanac4d26f822023-02-22 16:57:03 +00004707 private static ImageDimension.Builder expandImage() {
4708 return ImageDimension.newBuilder()
4709 .setExpandedDimension(ExpandedDimensionProp.getDefaultInstance());
4710 }
Neda Topoljanac742e9e72023-10-06 22:18:35 +00004711
4712 @NonNull
4713 private static List<DimensionProto.SpProp> buildSizesList(int[] presetSizes) {
4714 List<DimensionProto.SpProp> sizes = new ArrayList<>(3);
4715 for (int s: presetSizes) {
4716 sizes.add(sp(s));
4717 }
4718 return sizes;
4719 }
Neda Topoljanac4d26f822023-02-22 16:57:03 +00004720}