| /* |
| * Copyright 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.wear.protolayout.renderer.inflater; |
| |
| import static android.os.Looper.getMainLooper; |
| |
| import static androidx.test.core.app.ApplicationProvider.getApplicationContext; |
| import static androidx.wear.protolayout.proto.ModifiersProto.SlideParentSnapOption.SLIDE_PARENT_SNAP_TO_INSIDE; |
| import static androidx.wear.protolayout.proto.ModifiersProto.SlideParentSnapOption.SLIDE_PARENT_SNAP_TO_OUTSIDE; |
| import static androidx.wear.protolayout.renderer.R.id.clickable_id_tag; |
| import static androidx.wear.protolayout.renderer.helper.TestDsl.arc; |
| import static androidx.wear.protolayout.renderer.helper.TestDsl.arcText; |
| import static androidx.wear.protolayout.renderer.helper.TestDsl.box; |
| import static androidx.wear.protolayout.renderer.helper.TestDsl.column; |
| import static androidx.wear.protolayout.renderer.helper.TestDsl.dynamicFixedText; |
| import static androidx.wear.protolayout.renderer.helper.TestDsl.image; |
| import static androidx.wear.protolayout.renderer.helper.TestDsl.layout; |
| import static androidx.wear.protolayout.renderer.helper.TestDsl.row; |
| import static androidx.wear.protolayout.renderer.helper.TestDsl.text; |
| import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.TEXT_AUTOSIZES_LIMIT; |
| import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.getFrameLayoutGravity; |
| import static androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.getRenderedMetadata; |
| import static androidx.wear.protolayout.renderer.test.R.drawable.android_animated_24dp; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertThrows; |
| import static org.robolectric.Shadows.shadowOf; |
| |
| import static java.lang.Integer.MAX_VALUE; |
| |
| import android.app.Activity; |
| import android.app.Application; |
| import android.content.ComponentName; |
| import android.content.Intent; |
| import android.content.pm.ActivityInfo; |
| import android.graphics.Paint.Cap; |
| import android.graphics.Rect; |
| import android.graphics.drawable.AnimatedVectorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build.VERSION; |
| import android.os.Build.VERSION_CODES; |
| import android.os.Looper; |
| import android.os.SystemClock; |
| import android.text.TextUtils.TruncateAt; |
| import android.util.Pair; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.View.MeasureSpec; |
| import android.view.ViewGroup; |
| import android.view.animation.Animation; |
| import android.view.animation.Transformation; |
| import android.widget.FrameLayout; |
| import android.widget.FrameLayout.LayoutParams; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.Space; |
| import android.widget.TextView; |
| |
| import androidx.annotation.NonNull; |
| import androidx.core.content.ContextCompat; |
| import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; |
| import androidx.test.ext.junit.runners.AndroidJUnit4; |
| import androidx.vectordrawable.graphics.drawable.SeekableAnimatedVectorDrawable; |
| import androidx.wear.protolayout.expression.AppDataKey; |
| import androidx.wear.protolayout.expression.DynamicBuilders; |
| import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl; |
| import androidx.wear.protolayout.expression.pipeline.StateStore; |
| import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationParameters; |
| import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec; |
| import androidx.wear.protolayout.expression.proto.AnimationParameterProto.Repeatable; |
| import androidx.wear.protolayout.expression.proto.DynamicDataProto.DynamicDataValue; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicFloat; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicColor; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicFloat; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicInt32; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicString; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.Int32ToFloatOp; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.StateColorSource; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.StateFloatSource; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.StateInt32Source; |
| import androidx.wear.protolayout.expression.proto.DynamicProto.StateStringSource; |
| import androidx.wear.protolayout.expression.proto.FixedProto.FixedColor; |
| import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat; |
| import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32; |
| import androidx.wear.protolayout.expression.proto.FixedProto.FixedString; |
| import androidx.wear.protolayout.proto.ActionProto.Action; |
| import androidx.wear.protolayout.proto.ActionProto.AndroidActivity; |
| import androidx.wear.protolayout.proto.ActionProto.AndroidBooleanExtra; |
| import androidx.wear.protolayout.proto.ActionProto.AndroidDoubleExtra; |
| import androidx.wear.protolayout.proto.ActionProto.AndroidExtra; |
| import androidx.wear.protolayout.proto.ActionProto.AndroidIntExtra; |
| import androidx.wear.protolayout.proto.ActionProto.AndroidLongExtra; |
| import androidx.wear.protolayout.proto.ActionProto.AndroidStringExtra; |
| import androidx.wear.protolayout.proto.ActionProto.LaunchAction; |
| import androidx.wear.protolayout.proto.ActionProto.LoadAction; |
| import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignment; |
| import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignmentProp; |
| import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignment; |
| import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignmentProp; |
| import androidx.wear.protolayout.proto.ColorProto.ColorProp; |
| import androidx.wear.protolayout.proto.DimensionProto; |
| import androidx.wear.protolayout.proto.DimensionProto.ArcLineLength; |
| import androidx.wear.protolayout.proto.DimensionProto.ArcSpacerLength; |
| import androidx.wear.protolayout.proto.DimensionProto.ContainerDimension; |
| import androidx.wear.protolayout.proto.DimensionProto.DegreesProp; |
| import androidx.wear.protolayout.proto.DimensionProto.DpProp; |
| import androidx.wear.protolayout.proto.DimensionProto.ExpandedAngularDimensionProp; |
| import androidx.wear.protolayout.proto.DimensionProto.ExpandedDimensionProp; |
| import androidx.wear.protolayout.proto.DimensionProto.ExtensionDimension; |
| import androidx.wear.protolayout.proto.DimensionProto.ImageDimension; |
| import androidx.wear.protolayout.proto.DimensionProto.ProportionalDimensionProp; |
| import androidx.wear.protolayout.proto.DimensionProto.SpacerDimension; |
| import androidx.wear.protolayout.proto.DimensionProto.WrappedDimensionProp; |
| import androidx.wear.protolayout.proto.LayoutElementProto; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Arc; |
| import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement; |
| import androidx.wear.protolayout.proto.LayoutElementProto.ArcLine; |
| import androidx.wear.protolayout.proto.LayoutElementProto.ArcSpacer; |
| import androidx.wear.protolayout.proto.LayoutElementProto.ArcText; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Box; |
| import androidx.wear.protolayout.proto.LayoutElementProto.ColorFilter; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Column; |
| import androidx.wear.protolayout.proto.LayoutElementProto.ExtensionLayoutElement; |
| import androidx.wear.protolayout.proto.LayoutElementProto.FontStyle; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Image; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Layout; |
| import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement; |
| import androidx.wear.protolayout.proto.LayoutElementProto.MarqueeParameters; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Row; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Spacer; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Span; |
| import androidx.wear.protolayout.proto.LayoutElementProto.SpanImage; |
| import androidx.wear.protolayout.proto.LayoutElementProto.SpanText; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Spannable; |
| import androidx.wear.protolayout.proto.LayoutElementProto.StrokeCapProp; |
| import androidx.wear.protolayout.proto.LayoutElementProto.Text; |
| import androidx.wear.protolayout.proto.LayoutElementProto.TextOverflow; |
| import androidx.wear.protolayout.proto.LayoutElementProto.TextOverflowProp; |
| import androidx.wear.protolayout.proto.ModifiersProto.AnimatedVisibility; |
| import androidx.wear.protolayout.proto.ModifiersProto.Border; |
| import androidx.wear.protolayout.proto.ModifiersProto.Clickable; |
| import androidx.wear.protolayout.proto.ModifiersProto.EnterTransition; |
| import androidx.wear.protolayout.proto.ModifiersProto.ExitTransition; |
| import androidx.wear.protolayout.proto.ModifiersProto.FadeInTransition; |
| import androidx.wear.protolayout.proto.ModifiersProto.FadeOutTransition; |
| import androidx.wear.protolayout.proto.ModifiersProto.Modifiers; |
| import androidx.wear.protolayout.proto.ModifiersProto.Padding; |
| import androidx.wear.protolayout.proto.ModifiersProto.Semantics; |
| import androidx.wear.protolayout.proto.ModifiersProto.SemanticsRole; |
| import androidx.wear.protolayout.proto.ModifiersProto.SlideBound; |
| import androidx.wear.protolayout.proto.ModifiersProto.SlideDirection; |
| import androidx.wear.protolayout.proto.ModifiersProto.SlideInTransition; |
| import androidx.wear.protolayout.proto.ModifiersProto.SlideParentBound; |
| import androidx.wear.protolayout.proto.ModifiersProto.SlideParentSnapOption; |
| import androidx.wear.protolayout.proto.ModifiersProto.SpanModifiers; |
| import androidx.wear.protolayout.proto.ResourceProto.AndroidAnimatedImageResourceByResId; |
| import androidx.wear.protolayout.proto.ResourceProto.AndroidImageResourceByResId; |
| import androidx.wear.protolayout.proto.ResourceProto.AndroidSeekableAnimatedImageResourceByResId; |
| import androidx.wear.protolayout.proto.ResourceProto.AnimatedImageFormat; |
| import androidx.wear.protolayout.proto.ResourceProto.ImageFormat; |
| import androidx.wear.protolayout.proto.ResourceProto.ImageResource; |
| import androidx.wear.protolayout.proto.ResourceProto.InlineImageResource; |
| import androidx.wear.protolayout.proto.ResourceProto.Resources; |
| import androidx.wear.protolayout.proto.StateProto.State; |
| import androidx.wear.protolayout.proto.TriggerProto.OnVisibleTrigger; |
| import androidx.wear.protolayout.proto.TriggerProto.Trigger; |
| import androidx.wear.protolayout.proto.TypesProto.FloatProp; |
| import androidx.wear.protolayout.proto.TypesProto.Int32Prop; |
| import androidx.wear.protolayout.proto.TypesProto.StringProp; |
| import androidx.wear.protolayout.protobuf.ByteString; |
| import androidx.wear.protolayout.renderer.ProtoLayoutTheme; |
| import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline; |
| import androidx.wear.protolayout.renderer.helper.TestFingerprinter; |
| import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult; |
| import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.ViewGroupMutation; |
| import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.ViewMutationException; |
| import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.ViewProperties; |
| import androidx.wear.protolayout.renderer.test.R; |
| import androidx.wear.widget.ArcLayout; |
| import androidx.wear.widget.CurvedTextView; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.truth.Expect; |
| import com.google.common.util.concurrent.ListenableFuture; |
| |
| import org.junit.Ignore; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.robolectric.Robolectric; |
| import org.robolectric.shadows.ShadowChoreographer; |
| import org.robolectric.shadows.ShadowLooper; |
| import org.robolectric.shadows.ShadowPackageManager; |
| import org.robolectric.shadows.ShadowSystemClock; |
| |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.time.Duration; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.ExecutionException; |
| |
| @RunWith(AndroidJUnit4.class) |
| public class ProtoLayoutInflaterTest { |
| private static final String TEST_CLICKABLE_CLASS_NAME = "Hello"; |
| private static final String TEST_CLICKABLE_PACKAGE_NAME = "World"; |
| private static final String EXTRA_CLICKABLE_ID = "extra.clickable.id"; |
| |
| private static final int SCREEN_WIDTH = 400; |
| private static final int SCREEN_HEIGHT = 400; |
| |
| @Rule public final Expect expect = Expect.create(); |
| |
| private final StateStore mStateStore = new StateStore(ImmutableMap.of()); |
| private ProtoLayoutDynamicDataPipeline mDataPipeline; |
| |
| @Test |
| public void inflate_textView() { |
| String textContents = "Hello World"; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText(Text.newBuilder().setText(string(textContents))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // Check that there's a text element in the layout... |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| |
| // Text pulled from the proto. |
| expect.that(tv.getText().toString()).isEqualTo(textContents); |
| } |
| |
| @Test |
| public void inflate_textView_withColor() { |
| int color = 0xFF112233; |
| String textContents = "Hello World"; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setFontStyle( |
| FontStyle.newBuilder() |
| .setColor( |
| ColorProp.newBuilder() |
| .setArgb(color)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| expect.that(tv.getTextColors().getDefaultColor()).isEqualTo(color); |
| } |
| |
| @Test |
| public void inflate_textView_withoutText() { |
| LayoutElement root = LayoutElement.newBuilder().setText(Text.getDefaultInstance()).build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| expect.that(tv.getText().toString()).isEmpty(); |
| } |
| |
| @Test |
| public void inflate_textView_withEmptyValueForLayout() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText( |
| StringProp.newBuilder() |
| .setValue("abcde") |
| .setDynamicValue( |
| DynamicString.newBuilder() |
| .setFixed( |
| FixedString.newBuilder() |
| .setValue("Dynamic Fixed Text"))) |
| .setValueForLayout(""))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| FrameLayout sizedContainer = (FrameLayout) rootLayout.getChildAt(0); |
| expect.that(sizedContainer.getWidth()).isEqualTo(0); |
| } |
| |
| // obsoleteContentDescription is tested for backward compatibility |
| @SuppressWarnings("deprecation") |
| @Test |
| public void inflate_textView_withObsoleteSemanticsContentDescription() { |
| String textContents = "Hello World"; |
| String textDescription = "Hello World Text Element"; |
| Semantics semantics = |
| Semantics.newBuilder().setObsoleteContentDescription(textDescription).build(); |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setModifiers( |
| Modifiers.newBuilder().setSemantics(semantics))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // Check that there's a text element in the layout... |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| |
| // Check the text contents. |
| assertThat(tv.getText().toString()).isEqualTo(textContents); |
| |
| // Check the accessibility label. |
| AccessibilityNodeInfoCompat info = |
| AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo()); |
| assertThat(info.getContentDescription().toString()).isEqualTo(textDescription); |
| assertThat(info.isImportantForAccessibility()).isTrue(); |
| assertThat(tv.isImportantForAccessibility()).isTrue(); |
| assertThat(info.isFocusable()).isTrue(); |
| } |
| |
| // obsoleteContentDescription is tested for backward compatibility |
| @SuppressWarnings("deprecation") |
| @Test |
| public void inflate_textView_withSemanticsContentDescription() { |
| String textContents = "Hello World"; |
| String staticDescription = "StaticDescription"; |
| |
| StringProp descriptionProp = string(staticDescription).build(); |
| Semantics semantics = |
| Semantics.newBuilder() |
| .setObsoleteContentDescription("ObsoleteContentDescription") |
| .setContentDescription(descriptionProp) |
| .build(); |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setModifiers( |
| Modifiers.newBuilder().setSemantics(semantics))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // Check that there's a text element in the layout... |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| |
| // Check the text contents. |
| assertThat(tv.getText().toString()).isEqualTo(textContents); |
| |
| // Check the accessibility label. |
| AccessibilityNodeInfoCompat info = |
| AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo()); |
| assertThat(info.getContentDescription().toString()).isEqualTo(staticDescription); |
| assertThat(info.isImportantForAccessibility()).isTrue(); |
| assertThat(tv.isImportantForAccessibility()).isTrue(); |
| assertThat(info.isFocusable()).isTrue(); |
| } |
| |
| @Test |
| public void inflate_textView_withDynamicSemanticsDescription() { |
| String textContents = "Hello World"; |
| String staticDescription = "StaticDescription"; |
| String initialDynamicContentDescription = "content 1"; |
| String targetDynamicContentDescription = "content 2"; |
| String initialDynamicStateDescription = "state 1"; |
| String targetDynamicStateDescription = "state 2"; |
| |
| StringProp contentDescriptionProp = |
| string(staticDescription) |
| .setDynamicValue( |
| DynamicString.newBuilder() |
| .setStateSource( |
| StateStringSource.newBuilder() |
| .setSourceKey("content") |
| .build()) |
| .build()) |
| .build(); |
| StringProp stateDescriptionProp = |
| string(staticDescription) |
| .setDynamicValue( |
| DynamicString.newBuilder() |
| .setStateSource( |
| StateStringSource.newBuilder() |
| .setSourceKey("state") |
| .build()) |
| .build()) |
| .build(); |
| |
| Semantics semantics = |
| Semantics.newBuilder() |
| .setStateDescription(stateDescriptionProp) |
| .setContentDescription(contentDescriptionProp) |
| .build(); |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setModifiers( |
| Modifiers.newBuilder().setSemantics(semantics))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // Check that there's a text element in the layout... |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| |
| AccessibilityNodeInfoCompat info = |
| AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo()); |
| assertThat(info.isImportantForAccessibility()).isTrue(); |
| assertThat(tv.isImportantForAccessibility()).isTrue(); |
| assertThat(info.isFocusable()).isTrue(); |
| |
| AppDataKey<DynamicBuilders.DynamicString> keyContent = new AppDataKey<>("content"); |
| AppDataKey<DynamicBuilders.DynamicString> keyState = new AppDataKey<>("state"); |
| mStateStore.setAppStateEntryValuesProto( |
| ImmutableMap.of( |
| keyContent, |
| DynamicDataValue.newBuilder() |
| .setStringVal( |
| FixedString.newBuilder() |
| .setValue(initialDynamicContentDescription)) |
| .build(), |
| keyState, |
| DynamicDataValue.newBuilder() |
| .setStringVal( |
| FixedString.newBuilder() |
| .setValue(initialDynamicStateDescription)) |
| .build())); |
| |
| info = AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo()); |
| assertThat(mStateStore.getDynamicDataValuesProto(keyContent).getStringVal().getValue()) |
| .isEqualTo(initialDynamicContentDescription); |
| assertThat(info.getContentDescription().toString()) |
| .isEqualTo(initialDynamicContentDescription); |
| assertThat(info.getStateDescription().toString()).isEqualTo(initialDynamicStateDescription); |
| |
| mStateStore.setAppStateEntryValuesProto( |
| ImmutableMap.of( |
| keyContent, |
| DynamicDataValue.newBuilder() |
| .setStringVal( |
| FixedString.newBuilder() |
| .setValue(targetDynamicContentDescription)) |
| .build(), |
| keyState, |
| DynamicDataValue.newBuilder() |
| .setStringVal( |
| FixedString.newBuilder() |
| .setValue(targetDynamicStateDescription)) |
| .build())); |
| |
| info = AccessibilityNodeInfoCompat.wrap(tv.createAccessibilityNodeInfo()); |
| assertThat(info.getContentDescription().toString()) |
| .isEqualTo(targetDynamicContentDescription); |
| assertThat(info.getStateDescription().toString()).isEqualTo(targetDynamicStateDescription); |
| } |
| |
| @Test |
| public void inflate_box_withIllegalSize() { |
| LayoutElement textElement = |
| LayoutElement.newBuilder() |
| .setText(Text.newBuilder().setText(string("foo"))) |
| .build(); |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setBox( |
| Box.newBuilder() |
| // Outer box's width and height left at default value of |
| // "wrap" |
| .addContents( |
| LayoutElement.newBuilder() |
| .setBox( |
| // Inner box's width set to |
| // "expand". Having a single |
| // "expand" |
| // element in a "wrap" element is an |
| // undefined state, so the outer box |
| // should not be displayed. |
| Box.newBuilder() |
| .setWidth(expand()) |
| .addContents(textElement)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // Check that the outer box is not displayed. |
| assertThat(rootLayout.getChildCount()).isEqualTo(0); |
| } |
| |
| @Test |
| public void inflate_box_withSemanticsModifier() { |
| String textDescription = "this is a button"; |
| Semantics semantics = |
| Semantics.newBuilder() |
| .setContentDescription(string(textDescription)) |
| .setRole(SemanticsRole.SEMANTICS_ROLE_BUTTON) |
| .build(); |
| String text = "some button"; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setBox( |
| Box.newBuilder() |
| .addContents( |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(text)))) |
| .setModifiers( |
| Modifiers.newBuilder().setSemantics(semantics))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| View button = rootLayout.getChildAt(0); |
| AccessibilityNodeInfoCompat info = |
| AccessibilityNodeInfoCompat.wrap(button.createAccessibilityNodeInfo()); |
| expect.that(info.getContentDescription().toString()).contains(textDescription); |
| expect.that(info.getClassName().toString()).contains("android.widget.Button"); |
| expect.that(info.isImportantForAccessibility()).isTrue(); |
| assertThat(button.isImportantForAccessibility()).isTrue(); |
| } |
| |
| @Test |
| public void inflate_box_withSemanticsStateDescription() { |
| String textDescription = "this is a switch"; |
| String offState = "off"; |
| Semantics semantics = |
| Semantics.newBuilder() |
| .setContentDescription(string(textDescription)) |
| .setStateDescription(string(offState)) |
| .setRole(SemanticsRole.SEMANTICS_ROLE_SWITCH) |
| .build(); |
| String text = "a switch"; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setBox( |
| Box.newBuilder() |
| .addContents( |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(text)))) |
| .setModifiers( |
| Modifiers.newBuilder().setSemantics(semantics))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| View switchView = rootLayout.getChildAt(0); |
| AccessibilityNodeInfoCompat info = |
| AccessibilityNodeInfoCompat.wrap(switchView.createAccessibilityNodeInfo()); |
| expect.that(info.getContentDescription().toString()).contains(textDescription); |
| expect.that(info.getStateDescription().toString()).contains(offState); |
| expect.that(info.getClassName().toString()).contains("android.widget.Switch"); |
| expect.that(info.isImportantForAccessibility()).isTrue(); |
| assertThat(switchView.isImportantForAccessibility()).isTrue(); |
| } |
| |
| @Test |
| public void inflate_spacer() { |
| int width = 10; |
| int height = 20; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setSpacer( |
| Spacer.newBuilder() |
| .setHeight( |
| SpacerDimension.newBuilder() |
| .setLinearDimension(dp(height))) |
| .setWidth( |
| SpacerDimension.newBuilder() |
| .setLinearDimension(dp(width)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // Check that there's a single element in the layout... |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| View tv = rootLayout.getChildAt(0); |
| |
| // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine. |
| expect.that(tv.getMeasuredWidth()).isEqualTo(width); |
| expect.that(tv.getMeasuredHeight()).isEqualTo(height); |
| } |
| |
| @Test |
| public void inflate_spacerWithModifiers() { |
| int width = 10; |
| int height = 20; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setSpacer( |
| Spacer.newBuilder() |
| .setModifiers( |
| Modifiers.newBuilder() |
| .setBorder( |
| Border.newBuilder() |
| .setWidth(dp(2)) |
| .build())) |
| .setHeight( |
| SpacerDimension.newBuilder() |
| .setLinearDimension(dp(height))) |
| .setWidth( |
| SpacerDimension.newBuilder() |
| .setLinearDimension(dp(width)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // Check that there's a single element in the layout... |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| View tv = rootLayout.getChildAt(0); |
| |
| // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine. |
| expect.that(tv.getMeasuredWidth()).isEqualTo(width); |
| expect.that(tv.getMeasuredHeight()).isEqualTo(height); |
| } |
| |
| @Test |
| public void inflate_image_withoutDimensions() { |
| // Must match a resource ID in buildResources |
| String protoResId = "android"; |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage(Image.newBuilder().setResourceId(string(protoResId))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // An image without dimensions will not be displayed. |
| assertThat(rootLayout.getChildCount()).isEqualTo(0); |
| } |
| |
| @Test |
| public void inflate_image_withDimensions() { |
| // Must match a resource ID in buildResources |
| String protoResId = "android"; |
| |
| LayoutElement root = buildImage(protoResId, 30, 20); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| RatioViewWrapper iv = (RatioViewWrapper) rootLayout.getChildAt(0); |
| |
| expect.that(iv.getMeasuredWidth()).isEqualTo(30); |
| expect.that(iv.getMeasuredHeight()).isEqualTo(20); |
| } |
| |
| @Test |
| public void inflate_image_withInvalidRatio() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setHeight( |
| ImageDimension.newBuilder() |
| .setProportionalDimension( |
| ProportionalDimensionProp |
| .getDefaultInstance())) |
| .setWidth(expandImage())) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // An image with invalid ratio will not be displayed. |
| assertThat(rootLayout.getChildCount()).isEqualTo(0); |
| } |
| |
| @Test |
| public void inflate_image_byName() { |
| // Must match a resource ID in buildResources |
| String protoResId = "android_image_by_name"; |
| |
| LayoutElement root = buildImage(protoResId, 30, 20); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| RatioViewWrapper iv = (RatioViewWrapper) rootLayout.getChildAt(0); |
| |
| expect.that(iv.getMeasuredWidth()).isEqualTo(30); |
| expect.that(iv.getMeasuredHeight()).isEqualTo(20); |
| } |
| |
| @Test |
| public void inflate_clickableModifier_withLaunchAction() throws IOException { |
| final String packageName = "com.foo.protolayout.test"; |
| final String className = "com.foo.protolayout.test.TestActivity"; |
| final String textContents = "I am a clickable"; |
| |
| // Register the activity so the intent can be resolved. |
| ComponentName cn = new ComponentName(packageName, className); |
| ShadowPackageManager pkgManager = shadowOf(getApplicationContext().getPackageManager()); |
| ActivityInfo ai = pkgManager.addActivityIfNotPresent(cn); |
| ai.exported = true; |
| |
| String stringVal = "foobar"; |
| int int32Val = 123; |
| long int64Val = 1234567890123456789L; |
| double doubleVal = 0.1234; |
| |
| LaunchAction launchAction = |
| LaunchAction.newBuilder() |
| .setAndroidActivity( |
| AndroidActivity.newBuilder() |
| .setPackageName(packageName) |
| .setClassName(className) |
| .putKeyToExtra( |
| "stringValue", |
| AndroidExtra.newBuilder() |
| .setStringVal( |
| AndroidStringExtra.newBuilder() |
| .setValue(stringVal)) |
| .build()) |
| .putKeyToExtra( |
| "int32Value", |
| AndroidExtra.newBuilder() |
| .setIntVal( |
| AndroidIntExtra.newBuilder() |
| .setValue(int32Val)) |
| .build()) |
| .putKeyToExtra( |
| "int64Value", |
| AndroidExtra.newBuilder() |
| .setLongVal( |
| AndroidLongExtra.newBuilder() |
| .setValue(int64Val)) |
| .build()) |
| .putKeyToExtra( |
| "doubleValue", |
| AndroidExtra.newBuilder() |
| .setDoubleVal( |
| AndroidDoubleExtra.newBuilder() |
| .setValue(doubleVal)) |
| .build()) |
| .putKeyToExtra( |
| "boolValue", |
| AndroidExtra.newBuilder() |
| .setBooleanVal( |
| AndroidBooleanExtra.newBuilder() |
| .setValue(true)) |
| .build())) |
| .build(); |
| |
| Action action = Action.newBuilder().setLaunchAction(launchAction).build(); |
| |
| LayoutElement textElement = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setModifiers( |
| Modifiers.newBuilder() |
| .setClickable( |
| Clickable.newBuilder() |
| .setId("foo") |
| .setOnClick(action)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(textElement)).inflate(); |
| |
| // Should be just a text view as the root. |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| |
| // The clickable view must have the same tag as the corresponding layout clickable. |
| expect.that(tv.getTag(clickable_id_tag)).isEqualTo("foo"); |
| |
| // Ensure that the text still went through properly. |
| expect.that(tv.getText().toString()).isEqualTo(textContents); |
| |
| // Try and fire the intent. |
| tv.performClick(); |
| |
| Intent firedIntent = |
| shadowOf((Application) getApplicationContext()).getNextStartedActivity(); |
| expect.that(firedIntent.getComponent()).isEqualTo(cn); |
| expect.that(firedIntent.getStringExtra("stringValue")).isEqualTo(stringVal); |
| expect.that(firedIntent.getIntExtra("int32Value", 0)).isEqualTo(int32Val); |
| expect.that(firedIntent.getLongExtra("int64Value", 0)).isEqualTo(int64Val); |
| expect.that(firedIntent.getDoubleExtra("doubleValue", 0)).isEqualTo(doubleVal); |
| expect.that(firedIntent.getBooleanExtra("boolValue", false)).isEqualTo(true); |
| } |
| |
| @Test |
| public void inflate_clickableModifier_withLaunchAction_notExportedIsNotOp() { |
| final String packageName = "com.foo.protolayout.test"; |
| final String className = "com.foo.protolayout.test.TestActivity"; |
| final String textContents = "I am a clickable"; |
| |
| // Register the activity so the intent can be resolved. |
| ComponentName cn = new ComponentName(packageName, className); |
| ShadowPackageManager pkgManager = shadowOf(getApplicationContext().getPackageManager()); |
| ActivityInfo ai = pkgManager.addActivityIfNotPresent(cn); |
| |
| // Activity is not exported. Renderer shouldn't even try and call it. |
| ai.exported = false; |
| |
| LaunchAction launchAction = |
| LaunchAction.newBuilder() |
| .setAndroidActivity( |
| AndroidActivity.newBuilder() |
| .setPackageName(packageName) |
| .setClassName(className)) |
| .build(); |
| |
| Action action = Action.newBuilder().setLaunchAction(launchAction).build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setModifiers( |
| Modifiers.newBuilder() |
| .setClickable( |
| Clickable.newBuilder() |
| .setId("foo") |
| .setOnClick(action)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // Should be just a text view as the root. |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| |
| shadowOf((Application) getApplicationContext()).clearNextStartedActivities(); |
| |
| // Try and fire the intent. |
| tv.performClick(); |
| |
| expect.that(shadowOf((Application) getApplicationContext()).getNextStartedActivity()) |
| .isNull(); |
| } |
| |
| @Test |
| public void inflate_clickableModifier_withLaunchAction_requiresPermissionIsNoOp() { |
| final String packageName = "com.foo.protolayout.test"; |
| final String className = "com.foo.protolayout.test.TestActivity"; |
| final String textContents = "I am a clickable"; |
| |
| // Register the activity so the intent can be resolved. |
| ComponentName cn = new ComponentName(packageName, className); |
| ShadowPackageManager pkgManager = shadowOf(getApplicationContext().getPackageManager()); |
| ActivityInfo ai = pkgManager.addActivityIfNotPresent(cn); |
| |
| // Activity has a permission associated with it; shouldn't be called. |
| ai.exported = true; |
| ai.permission = "android.MY_SENSITIVE_PERMISSION"; |
| |
| LaunchAction launchAction = |
| LaunchAction.newBuilder() |
| .setAndroidActivity( |
| AndroidActivity.newBuilder() |
| .setPackageName(packageName) |
| .setClassName(className)) |
| .build(); |
| |
| Action action = Action.newBuilder().setLaunchAction(launchAction).build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setModifiers( |
| Modifiers.newBuilder() |
| .setClickable( |
| Clickable.newBuilder() |
| .setId("foo") |
| .setOnClick(action)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // Should be just a text view as the root. |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| |
| shadowOf((Application) getApplicationContext()).clearNextStartedActivities(); |
| |
| // Try and fire the intent. |
| tv.performClick(); |
| |
| expect.that(shadowOf((Application) getApplicationContext()).getNextStartedActivity()) |
| .isNull(); |
| } |
| |
| @Test |
| public void inflate_clickableModifier_withLoadAction() { |
| final String textContents = "I am a clickable"; |
| |
| Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setModifiers( |
| Modifiers.newBuilder() |
| .setClickable( |
| Clickable.newBuilder() |
| .setId("foo") |
| .setOnClick(action)))) |
| .build(); |
| |
| State.Builder receivedState = State.newBuilder(); |
| FrameLayout rootLayout = |
| renderer( |
| newRendererConfigBuilder( |
| fingerprintedLayout(root), resourceResolvers()) |
| .setLoadActionListener(receivedState::mergeFrom)) |
| .inflate(); |
| |
| // Should be just a text view as the root. |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| assertThat(rootLayout.getChildAt(0)).isInstanceOf(TextView.class); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| |
| // The clickable view must have the same tag as the corresponding layout clickable. |
| expect.that(tv.getTag(clickable_id_tag)).isEqualTo("foo"); |
| |
| // Ensure that the text still went through properly. |
| expect.that(tv.getText().toString()).isEqualTo(textContents); |
| |
| // Try and fire the intent. |
| tv.performClick(); |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| expect.that(receivedState.getLastClickableId()).isEqualTo("foo"); |
| } |
| |
| @Test |
| public void inflate_clickableModifier_withAndroidActivity_hasSourceBounds() { |
| final String packageName = "com.foo.protolayout.test"; |
| final String className = "com.foo.protolayout.test.TestActivity"; |
| final String textContents = "I am a clickable"; |
| |
| // Register the activity so the intent can be resolved. |
| ComponentName cn = new ComponentName(packageName, className); |
| ShadowPackageManager pkgManager = shadowOf(getApplicationContext().getPackageManager()); |
| ActivityInfo ai = pkgManager.addActivityIfNotPresent(cn); |
| ai.exported = true; |
| |
| LaunchAction launchAction = |
| LaunchAction.newBuilder() |
| .setAndroidActivity( |
| AndroidActivity.newBuilder() |
| .setPackageName(packageName) |
| .setClassName(className)) |
| .build(); |
| |
| Action action = Action.newBuilder().setLaunchAction(launchAction).build(); |
| |
| LayoutElement textElement = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setModifiers( |
| Modifiers.newBuilder() |
| .setClickable( |
| Clickable.newBuilder() |
| .setId("foo") |
| .setOnClick(action)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(textElement)).inflate(); |
| |
| // Need to run a layout / measure pass so that the Text element has some bounds... |
| rootLayout.measure( |
| MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, MeasureSpec.EXACTLY)); |
| rootLayout.layout(0, 0, rootLayout.getMeasuredWidth(), rootLayout.getMeasuredHeight()); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| tv.performClick(); |
| |
| Intent firedIntent = |
| shadowOf((Application) getApplicationContext()).getNextStartedActivity(); |
| |
| int[] screenLocation = new int[2]; |
| tv.getLocationOnScreen(screenLocation); |
| Rect screenLocationRect = |
| new Rect( |
| /* left= */ screenLocation[0], |
| /* top= */ screenLocation[1], |
| /* right= */ screenLocation[0] + tv.getWidth(), |
| /* bottom= */ screenLocation[1] + tv.getHeight()); |
| |
| expect.that(firedIntent.getSourceBounds()).isEqualTo(screenLocationRect); |
| } |
| |
| @Test |
| public void inflate_arc_withLineDrawnWithArcTo() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setLine( |
| ArcLine.newBuilder() |
| // Shorter than 360 degrees, |
| // so should be drawn as an |
| // arc: |
| .setLength(degrees(30)) |
| .setStrokeCap( |
| strokeCapButt()) |
| .setThickness(dp(12))))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| assertThat(arcLayout.getChildCount()).isEqualTo(1); |
| WearCurvedLineView line = (WearCurvedLineView) arcLayout.getChildAt(0); |
| assertThat(line.getSweepAngleDegrees()).isEqualTo(30); |
| assertThat(line.getStrokeCap()).isEqualTo(Cap.BUTT); |
| // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine: |
| assertThat(line.getThickness()).isEqualTo(12); |
| } |
| |
| @Test |
| public void inflate_arc_withLineDrawnWithAddOval() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setLine( |
| ArcLine.newBuilder() |
| // Longer than 360 degrees, |
| // so should be drawn as an |
| // oval: |
| .setLength(degrees(500)) |
| .setThickness(dp(12))))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| assertThat(arcLayout.getChildCount()).isEqualTo(1); |
| WearCurvedLineView line = (WearCurvedLineView) arcLayout.getChildAt(0); |
| assertThat(line.getSweepAngleDegrees()).isEqualTo(500); |
| // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine: |
| assertThat(line.getThickness()).isEqualTo(12); |
| } |
| |
| @Test |
| public void inflate_arc_withText() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setText( |
| ArcText.newBuilder() |
| .setText(string("text1")))) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setText( |
| ArcText.newBuilder() |
| .setText(string("text2"))))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| assertThat(arcLayout.getChildCount()).isEqualTo(2); |
| CurvedTextView textView1 = (CurvedTextView) arcLayout.getChildAt(0); |
| assertThat(textView1.getText()).isEqualTo("text1"); |
| CurvedTextView textView2 = (CurvedTextView) arcLayout.getChildAt(1); |
| assertThat(textView2.getText()).isEqualTo("text2"); |
| } |
| |
| @Test |
| public void inflate_arc_withText_autoSize_notSet() { |
| int lastSize = 12; |
| FontStyle.Builder style = FontStyle.newBuilder() |
| .addAllSize(buildSizesList(new int[]{10, 20, lastSize})); |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setText( |
| ArcText.newBuilder() |
| .setText(string("text1")) |
| .setFontStyle(style)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| CurvedTextView tv = (CurvedTextView) arcLayout.getChildAt(0); |
| assertThat(tv.getText()).isEqualTo("text1"); |
| expect.that(tv.getTextSize()).isEqualTo(lastSize); |
| } |
| |
| @Test |
| public void inflate_arc_withSpacer() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| arcLayoutElement( |
| ArcSpacer.newBuilder() |
| .setLength(degrees(90))))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| assertThat(arcLayout.getChildCount()).isEqualTo(1); |
| WearCurvedSpacer spacer = (WearCurvedSpacer) arcLayout.getChildAt(0); |
| assertThat(spacer.getSweepAngleDegrees()).isEqualTo(90); |
| // Dimensions are in DP, but the density is currently 1 in the tests, so this is fine: |
| assertThat(spacer.getThickness()).isEqualTo(20); |
| } |
| |
| @Test |
| public void inflate_arc_withMaxAngleAndWeights() { |
| ArcSpacerLength spacerLength = |
| ArcSpacerLength.newBuilder() |
| .setExpandedAngularDimension(expandAngular(1.0f)) |
| .build(); |
| ArcLineLength lineLength = |
| ArcLineLength.newBuilder().setExpandedAngularDimension(expandAngular(2.0f)).build(); |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .setMaxAngle(DegreesProp.newBuilder().setValue(90f).build()) |
| .addContents( |
| arcLayoutElement( |
| ArcSpacer.newBuilder() |
| .setAngularLength(spacerLength))) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setLine( |
| ArcLine.newBuilder() |
| .setAngularLength( |
| lineLength) |
| .setThickness(dp(12))))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| assertThat(rootLayout.getChildCount()).isEqualTo(1); |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| assertThat(arcLayout.isClockwise()).isTrue(); |
| assertThat(arcLayout.getMaxAngleDegrees()).isEqualTo(90f); |
| assertThat(arcLayout.getChildCount()).isEqualTo(2); |
| |
| WearCurvedSpacer spacer = (WearCurvedSpacer) arcLayout.getChildAt(0); |
| assertThat(spacer.getSweepAngleDegrees()).isEqualTo(30f); |
| |
| WearCurvedLineView line = (WearCurvedLineView) arcLayout.getChildAt(1); |
| assertThat(line.getSweepAngleDegrees()).isEqualTo(60f); |
| } |
| |
| @NonNull |
| private static ArcLayoutElement.Builder arcLayoutElement(ArcSpacer.Builder setAngularLength) { |
| return ArcLayoutElement.newBuilder().setSpacer(setAngularLength.setThickness(dp(20))); |
| } |
| |
| @Test |
| public void inflate_row() { |
| final String protoResId = "android"; |
| |
| LayoutElement image = buildImage(protoResId, 30, 20); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setRow(Row.newBuilder().addContents(image).addContents(image)) |
| .build(); |
| |
| FrameLayout layout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // There should be a child ViewGroup. Technically, we know it's a LinearLayout, but that |
| // could change in the future. For now, just ensure that the two images are laid out |
| // horizontally. |
| assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class); |
| ViewGroup firstChild = (ViewGroup) layout.getChildAt(0); |
| |
| assertThat(firstChild.getChildCount()).isEqualTo(2); |
| assertThat(firstChild.getChildAt(0)).isInstanceOf(RatioViewWrapper.class); |
| assertThat(firstChild.getChildAt(1)).isInstanceOf(RatioViewWrapper.class); |
| |
| RatioViewWrapper child1 = (RatioViewWrapper) firstChild.getChildAt(0); |
| RatioViewWrapper child2 = (RatioViewWrapper) firstChild.getChildAt(1); |
| |
| // There's no padding here... |
| expect.that(child2.getX()).isEqualTo(child1.getX() + child1.getMeasuredWidth()); |
| |
| // In this case, because both children are the same size, they should definitely share the |
| // same Y coordinate. |
| expect.that(child1.getY()).isEqualTo(child2.getY()); |
| } |
| |
| @Test |
| public void inflate_column() { |
| final String protoResId = "android"; |
| |
| LayoutElement image = buildImage(protoResId, 30, 20); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setColumn(Column.newBuilder().addContents(image).addContents(image)) |
| .build(); |
| |
| FrameLayout layout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class); |
| ViewGroup firstChild = (ViewGroup) layout.getChildAt(0); |
| |
| assertThat(firstChild.getChildCount()).isEqualTo(2); |
| assertThat(firstChild.getChildAt(0)).isInstanceOf(RatioViewWrapper.class); |
| assertThat(firstChild.getChildAt(1)).isInstanceOf(RatioViewWrapper.class); |
| |
| RatioViewWrapper child1 = (RatioViewWrapper) firstChild.getChildAt(0); |
| RatioViewWrapper child2 = (RatioViewWrapper) firstChild.getChildAt(1); |
| |
| // There's no padding here... |
| expect.that(child2.getY()).isEqualTo(child1.getY() + child1.getMeasuredHeight()); |
| |
| // In this case, because both children are the same size, they should definitely share the |
| // same X coordinate. |
| expect.that(child1.getX()).isEqualTo(child2.getX()); |
| } |
| |
| private static LayoutElement buildImage(String protoResId, float widthDp, float heightDp) { |
| return LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string(protoResId)) |
| .setWidth(linImageDim(dp(widthDp))) |
| .setHeight(linImageDim(dp(heightDp)))) |
| .build(); |
| } |
| |
| private static LayoutElement buildExampleRowLayoutWithAlignment(VerticalAlignment alignment) { |
| final String protoResId = "android"; |
| |
| LayoutElement image1 = buildImage(protoResId, 30, 30); |
| LayoutElement image2 = buildImage(protoResId, 30, 50); |
| |
| Row row = |
| Row.newBuilder() |
| .addContents(image1) |
| .addContents(image2) |
| .setVerticalAlignment( |
| VerticalAlignmentProp.newBuilder().setValue(alignment)) |
| .build(); |
| |
| // Gravity = top. |
| return LayoutElement.newBuilder().setRow(row).build(); |
| } |
| |
| @Test |
| public void inflate_row_withGravity() { |
| Map<VerticalAlignment, Integer> expectedY = |
| ImmutableMap.of( |
| VerticalAlignment.VERTICAL_ALIGN_TOP, 0, |
| VerticalAlignment.VERTICAL_ALIGN_CENTER, 10, |
| VerticalAlignment.VERTICAL_ALIGN_BOTTOM, 20); |
| |
| for (Map.Entry<VerticalAlignment, Integer> entry : expectedY.entrySet()) { |
| LayoutElement root = buildExampleRowLayoutWithAlignment(entry.getKey()); |
| FrameLayout topFrameLayout = renderer(fingerprintedLayout(root)).inflate(); |
| ViewGroup topViewGroup = (ViewGroup) topFrameLayout.getChildAt(0); |
| RatioViewWrapper image1 = (RatioViewWrapper) topViewGroup.getChildAt(0); |
| RatioViewWrapper image2 = (RatioViewWrapper) topViewGroup.getChildAt(1); |
| |
| // Image 1 is the smaller of the two, so its Y coordinate should move accordingly. |
| expect.that(image1.getY()).isEqualTo(image2.getY() + entry.getValue()); |
| } |
| } |
| |
| private static LayoutElement buildExampleColumnLayoutWithAlignment( |
| HorizontalAlignment alignment) { |
| final String resName = "android"; |
| |
| LayoutElement image1 = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string(resName)) |
| .setWidth(linImageDim(dp(30))) |
| .setHeight(linImageDim(dp(30)))) |
| .build(); |
| |
| LayoutElement image2 = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string(resName)) |
| .setWidth(linImageDim(dp(50))) |
| .setHeight(linImageDim(dp(30)))) |
| .build(); |
| |
| Column column = |
| Column.newBuilder() |
| .addContents(image1) |
| .addContents(image2) |
| .setHorizontalAlignment( |
| HorizontalAlignmentProp.newBuilder().setValue(alignment)) |
| .build(); |
| |
| // Gravity = top. |
| return LayoutElement.newBuilder().setColumn(column).build(); |
| } |
| |
| @Test |
| public void inflate_column_withGravity() { |
| Map<HorizontalAlignment, Integer> expectedX = |
| ImmutableMap.of( |
| HorizontalAlignment.HORIZONTAL_ALIGN_START, 0, |
| HorizontalAlignment.HORIZONTAL_ALIGN_CENTER, 10, |
| HorizontalAlignment.HORIZONTAL_ALIGN_END, 20); |
| |
| for (Map.Entry<HorizontalAlignment, Integer> entry : expectedX.entrySet()) { |
| LayoutElement root = buildExampleColumnLayoutWithAlignment(entry.getKey()); |
| FrameLayout topFrameLayout = renderer(fingerprintedLayout(root)).inflate(); |
| ViewGroup topViewGroup = (ViewGroup) topFrameLayout.getChildAt(0); |
| RatioViewWrapper image1 = (RatioViewWrapper) topViewGroup.getChildAt(0); |
| RatioViewWrapper image2 = (RatioViewWrapper) topViewGroup.getChildAt(1); |
| |
| // Image 1 is the smaller of the two, so its X coordinate should move accordingly. |
| expect.that(image1.getX()).isEqualTo(image2.getX() + entry.getValue()); |
| } |
| } |
| |
| private static LayoutElement buildExampleContainerLayoutWithAlignment( |
| HorizontalAlignment hAlign, VerticalAlignment vAlign) { |
| final String resName = "android"; |
| |
| LayoutElement image1 = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string(resName)) |
| .setWidth(linImageDim(dp(30))) |
| .setHeight(linImageDim(dp(30)))) |
| .build(); |
| |
| LayoutElement image2 = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string(resName)) |
| .setWidth(linImageDim(dp(50))) |
| .setHeight(linImageDim(dp(50)))) |
| .build(); |
| |
| Box box = |
| Box.newBuilder() |
| .addContents(image1) |
| .addContents(image2) |
| .setVerticalAlignment(VerticalAlignmentProp.newBuilder().setValue(vAlign)) |
| .setHorizontalAlignment( |
| HorizontalAlignmentProp.newBuilder().setValue(hAlign)) |
| .build(); |
| |
| // Gravity = top. |
| return LayoutElement.newBuilder().setBox(box).build(); |
| } |
| |
| @Test |
| public void inflate_stack_withAlignment() { |
| Map<HorizontalAlignment, Integer> expectedX = |
| ImmutableMap.of( |
| HorizontalAlignment.HORIZONTAL_ALIGN_START, 0, |
| HorizontalAlignment.HORIZONTAL_ALIGN_CENTER, 10, |
| HorizontalAlignment.HORIZONTAL_ALIGN_END, 20); |
| |
| Map<VerticalAlignment, Integer> expectedY = |
| ImmutableMap.of( |
| VerticalAlignment.VERTICAL_ALIGN_TOP, 0, |
| VerticalAlignment.VERTICAL_ALIGN_CENTER, 10, |
| VerticalAlignment.VERTICAL_ALIGN_BOTTOM, 20); |
| |
| for (Map.Entry<HorizontalAlignment, Integer> hEntry : expectedX.entrySet()) { |
| for (Map.Entry<VerticalAlignment, Integer> vEntry : expectedY.entrySet()) { |
| LayoutElement root = |
| buildExampleContainerLayoutWithAlignment(hEntry.getKey(), vEntry.getKey()); |
| FrameLayout topFrameLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| ViewGroup topViewGroup = (ViewGroup) topFrameLayout.getChildAt(0); |
| RatioViewWrapper image1 = (RatioViewWrapper) topViewGroup.getChildAt(0); |
| RatioViewWrapper image2 = (RatioViewWrapper) topViewGroup.getChildAt(1); |
| |
| // Image 1 is the smaller of the two, so its coordinates should move accordingly. |
| expect.that(image1.getX()).isEqualTo(image2.getX() + hEntry.getValue()); |
| expect.that(image1.getY()).isEqualTo(image2.getY() + vEntry.getValue()); |
| } |
| } |
| } |
| |
| @Test |
| public void inflate_layoutElement_noChild() { |
| // Just an empty layout. This is just to ensure that the renderer doesn't crash with a |
| // "barely valid" proto. |
| LayoutElement root = LayoutElement.getDefaultInstance(); |
| |
| renderer(fingerprintedLayout(root)).inflate(); |
| } |
| |
| @Test |
| public void buildClickableIntent_setsPackageName() { |
| LaunchAction launchAction = |
| LaunchAction.newBuilder() |
| .setAndroidActivity( |
| AndroidActivity.newBuilder() |
| .setClassName(TEST_CLICKABLE_CLASS_NAME) |
| .setPackageName(TEST_CLICKABLE_PACKAGE_NAME)) |
| .build(); |
| |
| Intent i = |
| ProtoLayoutInflater.buildLaunchActionIntent(launchAction, "", EXTRA_CLICKABLE_ID); |
| |
| expect.that(i.getComponent().getClassName()).isEqualTo(TEST_CLICKABLE_CLASS_NAME); |
| expect.that(i.getComponent().getPackageName()).isEqualTo(TEST_CLICKABLE_PACKAGE_NAME); |
| } |
| |
| @Test |
| public void buildClickableIntent_launchAction_containsClickableId() { |
| String testId = "HELLOWORLD"; |
| |
| LaunchAction launchAction = |
| LaunchAction.newBuilder() |
| .setAndroidActivity( |
| AndroidActivity.newBuilder() |
| .setClassName(TEST_CLICKABLE_CLASS_NAME) |
| .setPackageName(TEST_CLICKABLE_PACKAGE_NAME)) |
| .build(); |
| |
| Intent i = |
| ProtoLayoutInflater.buildLaunchActionIntent( |
| launchAction, testId, EXTRA_CLICKABLE_ID); |
| |
| expect.that(i.getStringExtra(EXTRA_CLICKABLE_ID)).isEqualTo(testId); |
| } |
| |
| @Test |
| public void buildClickableIntent_noClickableExtraIfNotSet() { |
| LaunchAction launchAction = |
| LaunchAction.newBuilder() |
| .setAndroidActivity( |
| AndroidActivity.newBuilder() |
| .setClassName(TEST_CLICKABLE_CLASS_NAME) |
| .setPackageName(TEST_CLICKABLE_PACKAGE_NAME)) |
| .build(); |
| |
| Intent i = |
| ProtoLayoutInflater.buildLaunchActionIntent(launchAction, "", EXTRA_CLICKABLE_ID); |
| |
| expect.that(i.hasExtra(EXTRA_CLICKABLE_ID)).isFalse(); |
| } |
| |
| @Test |
| public void inflate_imageView_noResourceId() { |
| LayoutElement root = |
| LayoutElement.newBuilder().setImage(Image.getDefaultInstance()).build(); |
| |
| renderer(fingerprintedLayout(root)).inflate(); |
| } |
| |
| @Test |
| public void inflate_imageView_resourceHasNoAndroidResource() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder().setResourceId(string("no_android_resource_set"))) |
| .build(); |
| |
| renderer(fingerprintedLayout(root)).inflate(); |
| } |
| |
| @Test |
| public void inflate_imageView_androidResourceDoesNotExist() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage(Image.newBuilder().setResourceId(string("does_not_exist"))) |
| .build(); |
| |
| renderer(fingerprintedLayout(root)).inflate(); |
| } |
| |
| @Test |
| public void inflate_imageView_resourceReferenceDoesNotExist() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage(Image.newBuilder().setResourceId(string("aaaaaaaaaaaaaa"))) |
| .build(); |
| |
| renderer(fingerprintedLayout(root)).inflate(); |
| } |
| |
| @Test |
| public void inflate_imageView_expandsToParentEvenWhenImageBitmapIsNotSet() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string("invalid")) |
| .setHeight(expandImage()) |
| .setWidth(expandImage())) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| RatioViewWrapper iv = (RatioViewWrapper) rootLayout.getChildAt(0); |
| |
| expect.that(iv.getMeasuredWidth()).isEqualTo(SCREEN_WIDTH); |
| expect.that(iv.getMeasuredHeight()).isEqualTo(SCREEN_HEIGHT); |
| } |
| |
| @Test |
| public void inflate_imageView_expandsToParentContainerEvenWhenImageBitmapIsNotSet() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setBox( |
| Box.newBuilder() |
| .setHeight(expand()) |
| .setWidth(expand()) |
| .setModifiers( |
| Modifiers.newBuilder() |
| .setPadding( |
| Padding.newBuilder() |
| .setTop(dp(50)))) |
| .addContents( |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId( |
| string("invalid")) |
| .setHeight(expandImage()) |
| .setWidth(expandImage())))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| FrameLayout boxLayout = (FrameLayout) rootLayout.getChildAt(0); |
| RatioViewWrapper iv = (RatioViewWrapper) boxLayout.getChildAt(0); |
| |
| expect.that(iv.getMeasuredWidth()).isEqualTo(SCREEN_WIDTH); |
| expect.that(iv.getMeasuredHeight()).isEqualTo(SCREEN_HEIGHT - 50); |
| } |
| |
| @Test |
| public void inflate_imageView_usesDimensionsEvenWhenImageBitmapIsNotSet() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string("invalid")) |
| .setHeight(linImageDim(dp(100))) |
| .setWidth(linImageDim(dp(100)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| RatioViewWrapper iv = (RatioViewWrapper) rootLayout.getChildAt(0); |
| |
| expect.that(iv.getMeasuredWidth()).isEqualTo(100); |
| expect.that(iv.getMeasuredHeight()).isEqualTo(100); |
| } |
| |
| @Test |
| public void inflate_imageView_largeImage_isIgnored() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string("large_image")) |
| .setHeight(linImageDim(dp(50))) |
| .setWidth(linImageDim(dp(50)))) |
| .build(); |
| Resources resources = |
| Resources.newBuilder() |
| .putIdToImage( |
| "large_image", |
| ImageResource.newBuilder() |
| .setInlineResource( |
| InlineImageResource.newBuilder() |
| .setFormat(ImageFormat.IMAGE_FORMAT_RGB_565) |
| .setWidthPx(10000) |
| .setHeightPx(10000) |
| .setData( |
| ByteString.copyFrom( |
| new byte[10000 * 10000]))) |
| .build()) |
| .build(); |
| ResourceResolvers.Builder resourceResolver = |
| StandardResourceResolvers.forLocalApp( |
| resources, |
| getApplicationContext(), |
| ContextCompat.getMainExecutor(getApplicationContext()), |
| true); |
| |
| FrameLayout rootLayout = |
| renderer(newRendererConfigBuilder(fingerprintedLayout(root), resourceResolver)) |
| .inflate(); |
| shadowOf(getMainLooper()).idle(); |
| |
| RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0); |
| ImageView iv = (ImageView) rvw.getChildAt(0); |
| expect.that(rvw.getMeasuredWidth()).isEqualTo(50); |
| expect.that(rvw.getMeasuredHeight()).isEqualTo(50); |
| expect.that(iv.getDrawable()).isNull(); |
| expect.that(iv.getMeasuredWidth()).isEqualTo(50); |
| expect.that(iv.getMeasuredHeight()).isEqualTo(50); |
| } |
| |
| @Test |
| public void inflate_imageView_nonAnimatingAvdResource_noAnimation() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId( |
| string("android_AVD_pretending_to_be_static")) |
| .setHeight(linImageDim(dp(50))) |
| .setWidth(linImageDim(dp(50))) |
| .build()) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0); |
| ImageView imageAVD = (ImageView) rvw.getChildAt(0); |
| Drawable drawableAVD = imageAVD.getDrawable(); |
| assertThat(drawableAVD).isInstanceOf(AnimatedVectorDrawable.class); |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| } |
| |
| @Test |
| public void inflate_imageView_withAVDResource() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string("android_AVD")) |
| .setHeight(linImageDim(dp(50))) |
| .setWidth(linImageDim(dp(50))) |
| .build()) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0); |
| ImageView imageAVD = (ImageView) rvw.getChildAt(0); |
| Drawable drawableAVD = imageAVD.getDrawable(); |
| assertThat(drawableAVD).isInstanceOf(AnimatedVectorDrawable.class); |
| } |
| |
| @Test |
| public void inflate_imageView_withSeekableAVDResource() { |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string("android_seekable_AVD")) |
| .setHeight(linImageDim(dp(50))) |
| .setWidth(linImageDim(dp(50))) |
| .build()) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0); |
| ImageView imageAVDSeekable = (ImageView) rvw.getChildAt(0); |
| Drawable drawableAVDSeekable = imageAVDSeekable.getDrawable(); |
| assertThat(drawableAVDSeekable).isInstanceOf(SeekableAnimatedVectorDrawable.class); |
| |
| mStateStore.setAppStateEntryValuesProto( |
| ImmutableMap.of( |
| new AppDataKey<DynamicBuilders.DynamicFloat>("anim_val"), |
| DynamicDataValue.newBuilder() |
| .setFloatVal(FixedFloat.newBuilder().setValue(0.44f)) |
| .build())); |
| shadowOf(getMainLooper()).idle(); |
| assertThat(((SeekableAnimatedVectorDrawable) drawableAVDSeekable).getCurrentPlayTime()) |
| .isEqualTo(440L); |
| } |
| |
| @Test |
| public void inflate_spannable_imageOccupiesSpace() { |
| LayoutElement rootWithoutImage = |
| LayoutElement.newBuilder() |
| .setSpannable( |
| Spannable.newBuilder() |
| .addSpans(textSpan("Foo")) |
| .addSpans(textSpan("Bar"))) |
| .build(); |
| |
| LayoutElement rootWithImage = |
| LayoutElement.newBuilder() |
| .setSpannable( |
| Spannable.newBuilder() |
| .addSpans(textSpan("Foo")) |
| .addSpans( |
| Span.newBuilder() |
| .setImage( |
| SpanImage.newBuilder() |
| .setResourceId( |
| string("android")) |
| .setHeight(dp(50)) |
| .setWidth(dp(50)))) |
| .addSpans(textSpan("Bar"))) |
| .build(); |
| |
| FrameLayout rootLayoutWithoutImage = |
| renderer(fingerprintedLayout(rootWithoutImage)).inflate(); |
| TextView tvInRootLayoutWithoutImage = (TextView) rootLayoutWithoutImage.getChildAt(0); |
| FrameLayout rootLayoutWithImage = renderer(fingerprintedLayout(rootWithImage)).inflate(); |
| TextView tvInRootLayoutWithImage = (TextView) rootLayoutWithImage.getChildAt(0); |
| |
| int widthDiff = |
| tvInRootLayoutWithImage.getMeasuredWidth() |
| - tvInRootLayoutWithoutImage.getMeasuredWidth(); |
| |
| // Check that the layout with the image is larger by exactly the image's width. |
| expect.that(widthDiff).isEqualTo(50); |
| |
| assertThat(tvInRootLayoutWithoutImage.getText().toString()).isEqualTo("FooBar"); |
| assertThat(tvInRootLayoutWithImage.getText().toString()).isEqualTo("Foo\u200D \u200DBar"); |
| } |
| |
| @Test |
| public void inflate_spannable_onClickCanFire() { |
| StringProp.Builder text = string("Hello World"); |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setSpannable( |
| Spannable.newBuilder() |
| .addSpans( |
| Span.newBuilder() |
| .setText( |
| SpanText.newBuilder() |
| .setText(text) |
| .setModifiers( |
| spanClickMod())))) |
| .build(); |
| |
| List<Boolean> hasFiredList = new ArrayList<>(); |
| FrameLayout rootLayout = |
| renderer( |
| newRendererConfigBuilder( |
| fingerprintedLayout(root), resourceResolvers()) |
| .setLoadActionListener(p -> hasFiredList.add(true)) |
| .setProtoLayoutTheme( |
| loadTheme(R.style.MyProtoLayoutSansSerifTheme))) |
| .inflate(); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| |
| // Dispatch a click event to the first View; it should trigger the LoadAction... |
| long startTime = SystemClock.uptimeMillis(); |
| MotionEvent evt = |
| MotionEvent.obtain( |
| /* downTime= */ startTime, |
| /* eventTime= */ startTime, |
| MotionEvent.ACTION_DOWN, |
| /* x= */ 5f, |
| /* y= */ 5f, |
| /* metaState= */ 0); |
| tv.dispatchTouchEvent(evt); |
| evt.recycle(); |
| |
| evt = |
| MotionEvent.obtain( |
| /* downTime= */ startTime, |
| /* eventTime= */ startTime + 100, |
| MotionEvent.ACTION_UP, |
| /* x= */ 5f, |
| /* y= */ 5f, |
| /* metaState= */ 0); |
| tv.dispatchTouchEvent(evt); |
| evt.recycle(); |
| |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| assertThat(hasFiredList).hasSize(1); |
| } |
| |
| @NonNull |
| private static SpanModifiers.Builder spanClickMod() { |
| return SpanModifiers.newBuilder() |
| .setClickable( |
| Clickable.newBuilder() |
| .setOnClick( |
| Action.newBuilder() |
| .setLoadAction(LoadAction.getDefaultInstance()))); |
| } |
| |
| @Test |
| public void inflate_textView_marqueeAnimation() { |
| String textContents = "Marquee Animation"; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setMaxLines(Int32Prop.newBuilder().setValue(1)) |
| .setOverflow( |
| TextOverflowProp.newBuilder() |
| .setValue( |
| TextOverflow.TEXT_OVERFLOW_MARQUEE) |
| .build())) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE); |
| expect.that(tv.isSelected()).isTrue(); |
| expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue(); |
| expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(-1); // Default value. |
| if (VERSION.SDK_INT >= VERSION_CODES.Q) { |
| expect.that(tv.isSingleLine()).isTrue(); |
| } |
| } |
| |
| @Test |
| public void inflate_textView_marquee_animationsDisabled() { |
| String textContents = "Marquee Animation"; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setMaxLines(Int32Prop.newBuilder().setValue(1)) |
| .setOverflow( |
| TextOverflowProp.newBuilder() |
| .setValue( |
| TextOverflow.TEXT_OVERFLOW_MARQUEE) |
| .build())) |
| .build(); |
| |
| FrameLayout rootLayout = |
| renderer( |
| newRendererConfigBuilder(fingerprintedLayout(root)) |
| .setAnimationEnabled(false)) |
| .inflate(); |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| expect.that(tv.getEllipsize()).isNull(); |
| } |
| |
| @Test |
| public void inflate_textView_marqueeAnimationInMultiLine() { |
| String textContents = "Marquee Animation"; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setMaxLines(Int32Prop.newBuilder().setValue(2)) |
| .setOverflow( |
| TextOverflowProp.newBuilder() |
| .setValue( |
| TextOverflow.TEXT_OVERFLOW_MARQUEE) |
| .build())) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE); |
| expect.that(tv.isSelected()).isFalse(); |
| expect.that(tv.isHorizontalFadingEdgeEnabled()).isFalse(); |
| if (VERSION.SDK_INT >= VERSION_CODES.Q) { |
| expect.that(tv.isSingleLine()).isFalse(); |
| } |
| } |
| |
| @Test |
| public void inflate_textView_marqueeAnimation_repeatLimit() { |
| String textContents = "Marquee Animation"; |
| int marqueeIterations = 5; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(textContents)) |
| .setMaxLines(Int32Prop.newBuilder().setValue(1)) |
| .setOverflow( |
| TextOverflowProp.newBuilder() |
| .setValue( |
| TextOverflow.TEXT_OVERFLOW_MARQUEE) |
| .build()) |
| .setMarqueeParameters( |
| MarqueeParameters.newBuilder() |
| .setIterations(marqueeIterations))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE); |
| expect.that(tv.isSelected()).isTrue(); |
| expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue(); |
| expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(marqueeIterations); |
| if (VERSION.SDK_INT >= VERSION_CODES.Q) { |
| expect.that(tv.isSingleLine()).isTrue(); |
| } |
| } |
| |
| @Test |
| public void inflate_textView_autosize_set() { |
| String text = "Test text"; |
| int[] presetSizes = new int[]{12, 20, 10}; |
| List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes); |
| |
| LayoutElement textElement = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(text)) |
| .setFontStyle( |
| FontStyle.newBuilder() |
| .addAllSize(sizes))) |
| .build(); |
| LayoutElement root = |
| LayoutElement.newBuilder().setBox( |
| Box.newBuilder() |
| .setWidth(expand()) |
| .setHeight(expand()) |
| .addContents(textElement)).build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0); |
| TextView tv = (TextView) firstChild.getChildAt(0); |
| |
| // TextView sorts preset sizes. |
| Arrays.sort(presetSizes); |
| expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM); |
| expect.that(tv.getAutoSizeTextAvailableSizes()).isEqualTo(presetSizes); |
| expect.that(tv.getTextSize()).isEqualTo(20); |
| } |
| |
| @Test |
| public void inflate_textView_autosize_setLimit_usesSingleSize() { |
| String text = "Test text"; |
| int sizesLength = TEXT_AUTOSIZES_LIMIT + 5; |
| int[] presetSizes = new int[sizesLength]; |
| int expectedLastSize = 120; |
| for (int i = 0; i < sizesLength - 1; i++) { |
| presetSizes[i] = i + 1; |
| } |
| presetSizes[sizesLength - 1] = expectedLastSize; |
| List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes); |
| |
| LayoutElement textElement = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(text)) |
| .setMaxLines(Int32Prop.newBuilder().setValue(4)) |
| .setFontStyle( |
| FontStyle.newBuilder() |
| .addAllSize(sizes))) |
| .build(); |
| LayoutElement root = |
| LayoutElement.newBuilder().setBox( |
| Box.newBuilder() |
| .setWidth(expand()) |
| .setHeight(expand()) |
| .addContents(textElement)).build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0); |
| TextView tv = (TextView) firstChild.getChildAt(0); |
| expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE); |
| expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty(); |
| expect.that(tv.getTextSize()).isEqualTo(expectedLastSize); |
| } |
| |
| @Test |
| public void inflate_textView_autosize_notSet() { |
| String text = "Test text"; |
| int size = 24; |
| List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{size}); |
| |
| LayoutElement textElement = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(text)) |
| .setFontStyle( |
| FontStyle.newBuilder() |
| .addAllSize(sizes))) |
| .build(); |
| LayoutElement root = |
| LayoutElement.newBuilder().setBox( |
| Box.newBuilder() |
| .setWidth(expand()) |
| .setHeight(expand()) |
| .addContents(textElement)).build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0); |
| TextView tv = (TextView) firstChild.getChildAt(0); |
| expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE); |
| expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty(); |
| expect.that(tv.getTextSize()).isEqualTo(size); |
| } |
| |
| @Test |
| public void inflate_textView_autosize_setDynamic_noop() { |
| String text = "Test text"; |
| int lastSize = 24; |
| List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{10, 30, lastSize}); |
| |
| LayoutElement textElement = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(dynamicString(text)) |
| .setFontStyle( |
| FontStyle.newBuilder() |
| .addAllSize(sizes))) |
| .build(); |
| LayoutElement root = |
| LayoutElement.newBuilder().setBox( |
| Box.newBuilder() |
| .setWidth(expand()) |
| .setHeight(expand()) |
| .addContents(textElement)).build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| ArrayList<View> textChildren = new ArrayList<>(); |
| rootLayout.findViewsWithText(textChildren, text, View.FIND_VIEWS_WITH_TEXT); |
| TextView tv = (TextView) textChildren.get(0); |
| expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE); |
| expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty(); |
| expect.that(tv.getTextSize()).isEqualTo(lastSize); |
| } |
| |
| @Test |
| public void inflate_textView_autosize_wrongSizes_noop() { |
| String text = "Test text"; |
| List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{0, -2, 0}); |
| |
| LayoutElement textElement = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string(text)) |
| .setFontStyle( |
| FontStyle.newBuilder() |
| .addAllSize(sizes))) |
| .build(); |
| LayoutElement root = |
| LayoutElement.newBuilder().setBox( |
| Box.newBuilder() |
| .setWidth(expand()) |
| .setHeight(expand()) |
| .addContents(textElement)).build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| ArrayList<View> textChildren = new ArrayList<>(); |
| rootLayout.findViewsWithText(textChildren, text, View.FIND_VIEWS_WITH_TEXT); |
| TextView tv = (TextView) textChildren.get(0); |
| expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE); |
| expect.that(tv.getAutoSizeTextAvailableSizes()).isEmpty(); |
| } |
| |
| @Test |
| public void inflate_spannable_marqueeAnimation() { |
| String text = "Marquee Animation"; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setSpannable( |
| Spannable.newBuilder() |
| .addSpans( |
| Span.newBuilder() |
| .setText( |
| SpanText.newBuilder() |
| .setText(string(text)))) |
| .setOverflow( |
| TextOverflowProp.newBuilder() |
| .setValue( |
| TextOverflow.TEXT_OVERFLOW_MARQUEE)) |
| .setMaxLines(Int32Prop.newBuilder().setValue(1))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE); |
| expect.that(tv.isSelected()).isTrue(); |
| expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue(); |
| expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(-1); // Default value. |
| if (VERSION.SDK_INT >= VERSION_CODES.Q) { |
| expect.that(tv.isSingleLine()).isTrue(); |
| } |
| } |
| |
| @Test |
| public void inflate_spantext_ignoresMultipleSizes() { |
| String text = "Test text"; |
| int firstSize = 12; |
| FontStyle.Builder style = FontStyle.newBuilder() |
| .addAllSize(buildSizesList(new int[]{firstSize, 10, 20})); |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setSpannable( |
| Spannable.newBuilder() |
| .addSpans( |
| Span.newBuilder() |
| .setText( |
| SpanText.newBuilder() |
| .setText(string(text)) |
| .setFontStyle(style)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| expect.that(tv.getAutoSizeTextType()).isEqualTo(TextView.AUTO_SIZE_TEXT_TYPE_NONE); |
| } |
| |
| @Test |
| public void inflate_spannable_marqueeAnimation_repeatLimit() { |
| String text = "Marquee Animation"; |
| int marqueeIterations = 5; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setSpannable( |
| Spannable.newBuilder() |
| .addSpans( |
| Span.newBuilder() |
| .setText( |
| SpanText.newBuilder() |
| .setText(string(text)))) |
| .setOverflow( |
| TextOverflowProp.newBuilder() |
| .setValue( |
| TextOverflow.TEXT_OVERFLOW_MARQUEE)) |
| .setMarqueeParameters( |
| MarqueeParameters.newBuilder() |
| .setIterations(marqueeIterations)) |
| .setMaxLines(Int32Prop.newBuilder().setValue(1))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE); |
| expect.that(tv.isSelected()).isTrue(); |
| expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue(); |
| expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(marqueeIterations); |
| if (VERSION.SDK_INT >= VERSION_CODES.Q) { |
| expect.that(tv.isSingleLine()).isTrue(); |
| } |
| } |
| |
| @Test |
| public void inflate_image_intrinsicSizeIsIgnored() { |
| Image.Builder image = |
| Image.newBuilder() |
| .setWidth(expandImage()) |
| .setHeight(expandImage()) |
| .setResourceId(string("large_image_120dp")); |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setBox( |
| Box.newBuilder() |
| .setWidth(wrap()) |
| .setHeight(wrap()) |
| .addContents( |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setWidth( |
| linImageDim( |
| dp(24f))) |
| .setHeight( |
| linImageDim( |
| dp(24f))) |
| .setResourceId( |
| string("android")))) |
| .addContents(LayoutElement.newBuilder().setImage(image))) |
| .build(); |
| |
| FrameLayout rootLayout = |
| renderer( |
| newRendererConfigBuilder(fingerprintedLayout(root)) |
| .setProtoLayoutTheme( |
| loadTheme(R.style.MyProtoLayoutSansSerifTheme))) |
| .inflate(); |
| |
| // Outer box should be 24dp |
| FrameLayout firstBox = (FrameLayout) rootLayout.getChildAt(0); |
| expect.that(firstBox.getWidth()).isEqualTo(24); |
| expect.that(firstBox.getHeight()).isEqualTo(24); |
| |
| // Both children (images) should have the same dimensions as the FrameLayout. |
| RatioViewWrapper rvw1 = (RatioViewWrapper) firstBox.getChildAt(0); |
| RatioViewWrapper rvw2 = (RatioViewWrapper) firstBox.getChildAt(1); |
| |
| expect.that(rvw1.getWidth()).isEqualTo(24); |
| expect.that(rvw1.getHeight()).isEqualTo(24); |
| |
| expect.that(rvw2.getWidth()).isEqualTo(24); |
| expect.that(rvw2.getHeight()).isEqualTo(24); |
| |
| ImageViewWithoutIntrinsicSizes image1 = (ImageViewWithoutIntrinsicSizes) rvw1.getChildAt(0); |
| ImageViewWithoutIntrinsicSizes image2 = (ImageViewWithoutIntrinsicSizes) rvw2.getChildAt(0); |
| |
| expect.that(image1.getWidth()).isEqualTo(24); |
| expect.that(image1.getHeight()).isEqualTo(24); |
| |
| expect.that(image2.getWidth()).isEqualTo(24); |
| expect.that(image2.getHeight()).isEqualTo(24); |
| } |
| |
| @NonNull |
| private static ImageDimension.Builder linImageDim(DpProp.Builder builderForValue) { |
| return ImageDimension.newBuilder().setLinearDimension(builderForValue); |
| } |
| |
| @NonNull |
| private static ContainerDimension.Builder wrap() { |
| return ContainerDimension.newBuilder() |
| .setWrappedDimension(WrappedDimensionProp.getDefaultInstance()); |
| } |
| |
| @Test |
| public void inflate_image_undefinedSizeIgnoresIntrinsicSize() { |
| // This can happen in the case that a layout is ever inflated into a Scrolling layout. In |
| // that case, the scrolling layout will measure all children with height = UNDEFINED, which |
| // can lead to an Image still using its intrinsic size. |
| String resId = "large_image_120dp"; |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setBox( |
| Box.newBuilder() |
| .setWidth(wrap()) |
| .setHeight(wrap()) |
| .addContents( |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setWidth( |
| linImageDim( |
| dp(24f))) |
| .setHeight( |
| linImageDim( |
| dp(24f))) |
| .setResourceId( |
| string("android")))) |
| .addContents( |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setWidth(expandImage()) |
| .setHeight(expandImage()) |
| .setResourceId( |
| string(resId))))) |
| .build(); |
| |
| FrameLayout rootLayout = |
| renderer( |
| newRendererConfigBuilder(fingerprintedLayout(root)) |
| .setProtoLayoutTheme( |
| loadTheme(R.style.MyProtoLayoutSansSerifTheme))) |
| .inflate(); |
| |
| // Re-measure the root layout with an UNDEFINED constraint... |
| int screenWidth = MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, MeasureSpec.EXACTLY); |
| int screenHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| rootLayout.measure(screenWidth, screenHeight); |
| rootLayout.layout(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); |
| |
| // Outer box should be 24dp |
| FrameLayout firstBox = (FrameLayout) rootLayout.getChildAt(0); |
| expect.that(firstBox.getWidth()).isEqualTo(24); |
| expect.that(firstBox.getHeight()).isEqualTo(24); |
| |
| // Both children (images) should have the same dimensions as the FrameLayout. |
| RatioViewWrapper rvw1 = (RatioViewWrapper) firstBox.getChildAt(0); |
| RatioViewWrapper rvw2 = (RatioViewWrapper) firstBox.getChildAt(1); |
| |
| expect.that(rvw1.getWidth()).isEqualTo(24); |
| expect.that(rvw1.getHeight()).isEqualTo(24); |
| |
| expect.that(rvw2.getWidth()).isEqualTo(24); |
| expect.that(rvw2.getHeight()).isEqualTo(24); |
| |
| ImageViewWithoutIntrinsicSizes image1 = (ImageViewWithoutIntrinsicSizes) rvw1.getChildAt(0); |
| ImageViewWithoutIntrinsicSizes image2 = (ImageViewWithoutIntrinsicSizes) rvw2.getChildAt(0); |
| |
| expect.that(image1.getWidth()).isEqualTo(24); |
| expect.that(image1.getHeight()).isEqualTo(24); |
| |
| expect.that(image2.getWidth()).isEqualTo(24); |
| expect.that(image2.getHeight()).isEqualTo(24); |
| } |
| |
| @Test |
| public void inflate_arcLine_usesValueForLayout() { |
| DynamicFloat arcLength = |
| DynamicFloat.newBuilder().setFixed(FixedFloat.newBuilder().setValue(45f)).build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setLine( |
| ArcLine.newBuilder() |
| // Shorter than 360 degrees, |
| // so should be drawn as an |
| // arc: |
| .setLength( |
| degreesDynamic( |
| arcLength, |
| 180f)) |
| .setThickness(dp(12))))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| SizedArcContainer sizedContainer = (SizedArcContainer) arcLayout.getChildAt(0); |
| WearCurvedLineView line = (WearCurvedLineView) sizedContainer.getChildAt(0); |
| assertThat(sizedContainer.getSweepAngleDegrees()).isEqualTo(180f); |
| assertThat(line.getLineSweepAngleDegrees()).isEqualTo(45f); |
| assertThat(line.getMaxSweepAngleDegrees()).isEqualTo(180f); |
| } |
| |
| @Test |
| public void inflate_arcLine_usesZeroValueForLayout() { |
| DynamicFloat arcLength = |
| DynamicFloat.newBuilder().setFixed(FixedFloat.newBuilder().setValue(45f)).build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setLine( |
| ArcLine.newBuilder() |
| .setLength( |
| degreesDynamic( |
| arcLength, |
| 0f)) |
| .setThickness(dp(12))))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| SizedArcContainer sizedContainer = (SizedArcContainer) arcLayout.getChildAt(0); |
| expect.that(sizedContainer.getSweepAngleDegrees()).isEqualTo(0f); |
| WearCurvedLineView line = (WearCurvedLineView) sizedContainer.getChildAt(0); |
| expect.that(line.getMaxSweepAngleDegrees()).isEqualTo(0f); |
| } |
| |
| @Test |
| public void inflate_arcLine_dynamicData_updatesArcLength() { |
| AppDataKey<DynamicBuilders.DynamicInt32> keyFoo = new AppDataKey<>("foo"); |
| mStateStore.setAppStateEntryValuesProto( |
| ImmutableMap.of( |
| keyFoo, |
| DynamicDataValue.newBuilder() |
| .setInt32Val(FixedInt32.newBuilder().setValue(10)) |
| .build())); |
| |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| DynamicFloat arcLength = |
| DynamicFloat.newBuilder() |
| .setInt32ToFloatOperation( |
| Int32ToFloatOp.newBuilder() |
| .setInput( |
| DynamicInt32.newBuilder() |
| .setStateSource( |
| StateInt32Source.newBuilder() |
| .setSourceKey("foo")))) |
| .build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setLine( |
| ArcLine.newBuilder() |
| // Shorter than 360 degrees, |
| // so should be drawn as an |
| // arc: |
| .setLength( |
| degreesDynamic( |
| arcLength, |
| 180f)) |
| .setThickness(dp(12))))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| SizedArcContainer sizedContainer = (SizedArcContainer) arcLayout.getChildAt(0); |
| WearCurvedLineView line = (WearCurvedLineView) sizedContainer.getChildAt(0); |
| assertThat(line.getLineSweepAngleDegrees()).isEqualTo(10); |
| |
| mStateStore.setAppStateEntryValuesProto( |
| ImmutableMap.of( |
| keyFoo, |
| DynamicDataValue.newBuilder() |
| .setInt32Val(FixedInt32.newBuilder().setValue(20)) |
| .build())); |
| |
| assertThat(line.getLineSweepAngleDegrees()).isEqualTo(20); |
| } |
| |
| @Test |
| public void inflate_arcLine_withoutValueForLayout_noLegacyMode_usesZero() { |
| DynamicFloat arcLength = |
| DynamicFloat.newBuilder().setFixed(FixedFloat.newBuilder().setValue(45f)).build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setLine( |
| ArcLine.newBuilder() |
| // Shorter than 360 degrees, |
| // so should be drawn as an |
| // arc: |
| .setLength( |
| degreesDynamic( |
| arcLength)) |
| .setThickness(dp(12))))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| SizedArcContainer sizedContainer = (SizedArcContainer) arcLayout.getChildAt(0); |
| WearCurvedLineView line = (WearCurvedLineView) sizedContainer.getChildAt(0); |
| assertThat(sizedContainer.getSweepAngleDegrees()).isEqualTo(0f); |
| assertThat(line.getLineSweepAngleDegrees()).isEqualTo(45f); |
| assertThat(line.getMaxSweepAngleDegrees()).isEqualTo(0); |
| } |
| |
| @NonNull |
| private static DegreesProp.Builder degreesDynamic(DynamicFloat arcLength) { |
| return DegreesProp.newBuilder().setDynamicValue(arcLength); |
| } |
| |
| @NonNull |
| private static DegreesProp.Builder degreesDynamic( |
| DynamicFloat arcLength, float valueForLayout) { |
| return DegreesProp.newBuilder() |
| .setValueForLayout(valueForLayout) |
| .setDynamicValue(arcLength); |
| } |
| |
| @Test |
| public void inflate_arcLine_withoutValueForLayout_legacyMode_usesArcLength() { |
| DynamicFloat arcLength = |
| DynamicFloat.newBuilder().setFixed(FixedFloat.newBuilder().setValue(45f)).build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setArc( |
| Arc.newBuilder() |
| .setAnchorAngle(degrees(0).build()) |
| .addContents( |
| ArcLayoutElement.newBuilder() |
| .setLine( |
| ArcLine.newBuilder() |
| // Shorter than 360 degrees, |
| // so should be drawn as an |
| // arc: |
| .setLength( |
| degreesDynamic( |
| arcLength)) |
| .setThickness(dp(12))))) |
| .build(); |
| |
| FrameLayout rootLayout = |
| renderer( |
| newRendererConfigBuilder(fingerprintedLayout(root)) |
| .setAllowLayoutChangingBindsWithoutDefault(true)) |
| .inflate(); |
| |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| ArcLayout arcLayout = (ArcLayout) rootLayout.getChildAt(0); |
| WearCurvedLineView line = (WearCurvedLineView) arcLayout.getChildAt(0); |
| assertThat(line.getSweepAngleDegrees()).isEqualTo(45f); |
| assertThat(line.getLineSweepAngleDegrees()).isEqualTo(45f); |
| } |
| |
| @Test |
| public void inflate_text_dynamicColor_updatesColor() { |
| AppDataKey<DynamicBuilders.DynamicColor> keyFoo = new AppDataKey<>("foo"); |
| mStateStore.setAppStateEntryValuesProto( |
| ImmutableMap.of( |
| keyFoo, |
| DynamicDataValue.newBuilder() |
| .setColorVal(FixedColor.newBuilder().setArgb(0xFFFFFFFF)) |
| .build())); |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| DynamicColor color = |
| DynamicColor.newBuilder() |
| .setStateSource(StateColorSource.newBuilder().setSourceKey("foo")) |
| .build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setText(string("Hello World")) |
| .setFontStyle( |
| FontStyle.newBuilder() |
| .setColor( |
| ColorProp.newBuilder() |
| .setDynamicValue(color)))) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| TextView tv = (TextView) rootLayout.getChildAt(0); |
| assertThat(tv.getCurrentTextColor()).isEqualTo(0xFFFFFFFF); |
| |
| mStateStore.setAppStateEntryValuesProto( |
| ImmutableMap.of( |
| keyFoo, |
| DynamicDataValue.newBuilder() |
| .setColorVal(FixedColor.newBuilder().setArgb(0x11111111)) |
| .build())); |
| |
| assertThat(tv.getCurrentTextColor()).isEqualTo(0x11111111); |
| } |
| |
| @Test |
| public void inflate_image_dynamicTint_changesTintColor() { |
| // Must match a resource ID in buildResources |
| String protoResId = "android"; |
| |
| mStateStore.setAppStateEntryValuesProto( |
| ImmutableMap.of( |
| new AppDataKey<DynamicBuilders.DynamicColor>("tint"), |
| DynamicDataValue.newBuilder() |
| .setColorVal(FixedColor.newBuilder().setArgb(0xFFFFFFFF)) |
| .build())); |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| DynamicColor color = |
| DynamicColor.newBuilder() |
| .setStateSource(StateColorSource.newBuilder().setSourceKey("tint")) |
| .build(); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setImage( |
| Image.newBuilder() |
| .setResourceId(string(protoResId)) |
| .setColorFilter( |
| ColorFilter.newBuilder() |
| .setTint( |
| ColorProp.newBuilder() |
| .setDynamicValue(color))) |
| .setHeight(expandImage()) |
| .setWidth(expandImage())) |
| .build(); |
| |
| FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| shadowOf(Looper.getMainLooper()).idle(); |
| |
| RatioViewWrapper rvw = (RatioViewWrapper) rootLayout.getChildAt(0); |
| ImageView iv = (ImageView) rvw.getChildAt(0); |
| assertThat(iv.getImageTintList().getDefaultColor()).isEqualTo(0xFFFFFFFF); |
| } |
| |
| @Test |
| public void inflate_extension_onlySpaceIfNoExtension() { |
| byte[] payload = "Hello World".getBytes(StandardCharsets.UTF_8); |
| int size = 5; |
| |
| ExtensionDimension dim = |
| ExtensionDimension.newBuilder().setLinearDimension(dp(size)).build(); |
| LayoutElement rootElement = |
| LayoutElement.newBuilder() |
| .setExtension( |
| ExtensionLayoutElement.newBuilder() |
| .setExtensionId("foo") |
| .setPayload(ByteString.copyFrom(payload)) |
| .setWidth(dim) |
| .setHeight(dim)) |
| .build(); |
| |
| FrameLayout inflatedLayout = renderer(fingerprintedLayout(rootElement)).inflate(); |
| |
| assertThat(inflatedLayout.getChildCount()).isEqualTo(1); |
| assertThat(inflatedLayout.getChildAt(0)).isInstanceOf(Space.class); |
| |
| Space s = (Space) inflatedLayout.getChildAt(0); |
| assertThat(s.getMeasuredWidth()).isEqualTo(size); |
| assertThat(s.getMeasuredHeight()).isEqualTo(size); |
| } |
| |
| @Test |
| public void inflate_rendererExtension_withExtension_callsExtension() { |
| List<Pair<byte[], String>> invokedExtensions = new ArrayList<>(); |
| |
| final byte[] payload = "Hello World".getBytes(StandardCharsets.UTF_8); |
| final int size = 5; |
| final String extensionId = "foo"; |
| |
| ExtensionDimension dim = |
| ExtensionDimension.newBuilder().setLinearDimension(dp(size)).build(); |
| LayoutElement rootElement = |
| LayoutElement.newBuilder() |
| .setExtension( |
| ExtensionLayoutElement.newBuilder() |
| .setExtensionId(extensionId) |
| .setPayload(ByteString.copyFrom(payload)) |
| .setWidth(dim) |
| .setHeight(dim)) |
| .build(); |
| |
| FrameLayout inflatedLayout = |
| renderer( |
| newRendererConfigBuilder(fingerprintedLayout(rootElement)) |
| .setExtensionViewProvider( |
| (extensionPayload, id) -> { |
| invokedExtensions.add( |
| new Pair<>(extensionPayload, id)); |
| TextView returnedView = |
| new TextView(getApplicationContext()); |
| returnedView.setText("testing"); |
| |
| return returnedView; |
| })) |
| .inflate(); |
| |
| assertThat(inflatedLayout.getChildCount()).isEqualTo(1); |
| assertThat(inflatedLayout.getChildAt(0)).isInstanceOf(TextView.class); |
| |
| TextView tv = (TextView) inflatedLayout.getChildAt(0); |
| assertThat(tv.getText().toString()).isEqualTo("testing"); |
| |
| assertThat(invokedExtensions).hasSize(1); |
| assertThat(invokedExtensions.get(0).first).isEqualTo(payload); |
| assertThat(invokedExtensions.get(0).second).isEqualTo(extensionId); |
| } |
| |
| @Test |
| public void inflateThenMutate_withChangeToText_causesUpdate() { |
| Layout layout1 = |
| layout( |
| column( // 1 |
| text("Hello"), // 1.1 |
| text("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1 = (TextView) column.getChildAt(0); |
| TextView tv2 = (TextView) column.getChildAt(1); |
| assertThat(column.getChildCount()).isEqualTo(2); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| |
| // Produce a new layout with only one Text element changed. |
| Layout layout2 = |
| layout( |
| column( // 1 |
| text("Hello"), // 1.1 |
| text("Mars") // 1.2 |
| )); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0); |
| TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1); |
| |
| // Unchanged views should be left exactly the same: |
| assertThat(columnAfterMutation).isSameInstanceAs(column); |
| assertThat(columnAfterMutation.getChildCount()).isEqualTo(2); |
| assertThat(tv1AfterMutation).isSameInstanceAs(tv1); |
| // Overall content should match layout2: |
| assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText().toString()).isEqualTo("Mars"); |
| } |
| |
| @Test |
| public void inflateThenMutate_withChangeToImageAndText_causesUpdate() { |
| Layout layout1 = |
| layout( |
| column( // 1 |
| text("Hello"), // 1.1 |
| row( // 1.2 |
| image( // 1.2.1 |
| props -> { |
| props.heightDp = 50; |
| props.widthDp = 50; |
| }, |
| "android"), |
| text("World") // 1.2.2 |
| ))); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1 = (TextView) column.getChildAt(0); |
| ViewGroup row = (ViewGroup) column.getChildAt(1); |
| ImageView image = (ImageView) ((ViewGroup) row.getChildAt(0)).getChildAt(0); |
| TextView tv2 = (TextView) row.getChildAt(1); |
| assertThat(column.getChildCount()).isEqualTo(2); |
| assertThat(row.getChildCount()).isEqualTo(2); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| // Can't get android resource ID from image, so use the size to infer that we start with the |
| // correct one. |
| assertThat(image.getDrawable().getIntrinsicHeight()).isEqualTo(24); |
| assertThat(image.getDrawable().getIntrinsicWidth()).isEqualTo(24); |
| assertThat(image.getMeasuredHeight()).isEqualTo(50); |
| assertThat(image.getMeasuredWidth()).isEqualTo(50); |
| |
| // Produce a new layout with one Text element and one Image changed. |
| Layout layout2 = |
| layout( |
| column( // 1 |
| text("Hello"), // 1.1 |
| row( // 1.2 |
| image( // 1.2.1 |
| props -> { |
| props.heightDp = 50; |
| props.widthDp = 50; |
| }, |
| "large_image_120dp"), |
| text("Mars") // 1.2.2 |
| ))); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0); |
| ViewGroup rowAfterMutation = (ViewGroup) columnAfterMutation.getChildAt(1); |
| ImageView imageAfterMutation = |
| (ImageView) ((ViewGroup) rowAfterMutation.getChildAt(0)).getChildAt(0); |
| TextView tv2AfterMutation = (TextView) row.getChildAt(1); |
| |
| // Unchanged views should be left exactly the same: |
| assertThat(columnAfterMutation).isSameInstanceAs(column); |
| assertThat(columnAfterMutation.getChildCount()).isEqualTo(2); |
| assertThat(tv1AfterMutation).isSameInstanceAs(tv1); |
| assertThat(rowAfterMutation).isSameInstanceAs(row); |
| assertThat(rowAfterMutation.getChildCount()).isEqualTo(2); |
| // Overall content should match layout2: |
| assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText().toString()).isEqualTo("Mars"); |
| // Can't get android resource ID from image, so use the size to infer that the image has |
| // been |
| // correctly updated to a different one: |
| assertThat(imageAfterMutation.getDrawable().getIntrinsicHeight()).isEqualTo(120); |
| assertThat(imageAfterMutation.getDrawable().getIntrinsicWidth()).isEqualTo(120); |
| assertThat(imageAfterMutation.getMeasuredHeight()).isEqualTo(50); |
| assertThat(imageAfterMutation.getMeasuredWidth()).isEqualTo(50); |
| } |
| |
| @Test |
| public void inflateThenMutate_withChangeToProps_causesUpdate() { |
| Layout layout1 = |
| layout( |
| column( // 1 |
| props -> props.widthDp = 55, |
| text("Hello"), // 1.1 |
| text("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1 = (TextView) column.getChildAt(0); |
| TextView tv2 = (TextView) column.getChildAt(1); |
| assertThat(column.getMeasuredWidth()).isEqualTo(55); |
| assertThat(column.getChildCount()).isEqualTo(2); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| |
| // Produce a new layout with only the props of the container changed. |
| Layout layout2 = |
| layout( |
| column( // 1 |
| props -> props.widthDp = 123, |
| text("Hello"), // 1.1 |
| text("World") // 1.2 |
| )); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| |
| // Check contents after mutation |
| ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0); |
| TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1); |
| assertThat(columnAfterMutation.getMeasuredWidth()).isEqualTo(123); |
| assertThat(columnAfterMutation.getChildCount()).isEqualTo(2); |
| assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText().toString()).isEqualTo("World"); |
| assertThat(tv1AfterMutation).isSameInstanceAs(tv1); |
| assertThat(tv2AfterMutation).isSameInstanceAs(tv2); |
| } |
| |
| @Test |
| public void inflateThenMutate_withChangeToPropsAndOneChild_doesntUpdateAllChildren() { |
| Layout layout1 = |
| layout( |
| column( // 1 |
| props -> props.widthDp = 55, |
| text("Hello"), // 1.1 |
| text("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1 = (TextView) column.getChildAt(0); |
| TextView tv2 = (TextView) column.getChildAt(1); |
| assertThat(column.getMeasuredWidth()).isEqualTo(55); |
| assertThat(column.getChildCount()).isEqualTo(2); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| |
| // Produce a new layout with the props of the container and one child changed. |
| Layout layout2 = |
| layout( |
| column( // 1 |
| props -> props.widthDp = 123, |
| text("Hello"), // 1.1 |
| text("MARS") // 1.2 |
| )); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| |
| // Check contents after mutation |
| ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0); |
| TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1); |
| assertThat(columnAfterMutation.getMeasuredWidth()).isEqualTo(123); |
| assertThat(columnAfterMutation.getChildCount()).isEqualTo(2); |
| assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText().toString()).isEqualTo("MARS"); |
| assertThat(tv1AfterMutation).isSameInstanceAs(tv1); |
| } |
| |
| @Test |
| public void inflateThenMutate_withNoChange_producesNoOpMutation() { |
| Layout layout = |
| layout( |
| column( // 1 |
| text("Hello"), // 1.1 |
| text("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1 = (TextView) column.getChildAt(0); |
| TextView tv2 = (TextView) column.getChildAt(1); |
| assertThat(column.getChildCount()).isEqualTo(2); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| |
| // Compute the mutation for the same layout |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isTrue(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0); |
| TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1); |
| |
| // Everything should be exactly the same: |
| assertThat(columnAfterMutation).isSameInstanceAs(column); |
| assertThat(columnAfterMutation.getChildCount()).isEqualTo(2); |
| assertThat(tv1AfterMutation).isSameInstanceAs(tv1); |
| assertThat(tv2AfterMutation).isSameInstanceAs(tv2); |
| assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText().toString()).isEqualTo("World"); |
| } |
| |
| @Test |
| public void inflateThenMutate_withDifferentNumberOfChildren_causesUpdate() { |
| Layout layout1 = |
| layout( |
| column( // 1 |
| text("Hello"), // 1.1 |
| text("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| |
| // Check the pre-mutation layout |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1 = (TextView) column.getChildAt(0); |
| TextView tv2 = (TextView) column.getChildAt(1); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| |
| Layout layout2 = |
| layout( |
| column( // 1 |
| text("Hello"), // 1.1 |
| text("World"), // 1.2 |
| text("and"), // 1.3 |
| text("Mars") // 1.4 |
| )); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1AfterMutation = (TextView) columnAfterMutation.getChildAt(0); |
| TextView tv2AfterMutation = (TextView) columnAfterMutation.getChildAt(1); |
| TextView tv3AfterMutation = (TextView) columnAfterMutation.getChildAt(2); |
| TextView tv4AfterMutation = (TextView) columnAfterMutation.getChildAt(3); |
| |
| // Check contents after mutation |
| assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText().toString()).isEqualTo("World"); |
| assertThat(tv3AfterMutation.getText().toString()).isEqualTo("and"); |
| assertThat(tv4AfterMutation.getText().toString()).isEqualTo("Mars"); |
| } |
| |
| @Test |
| public void inflateThenMutate_withDynamicText_dataPipelineIsUpdated() { |
| Layout layout1 = |
| layout( |
| column( // 1 |
| dynamicFixedText("Hello"), // 1.1 |
| dynamicFixedText("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| assertThat(renderer.getDataPipelineSize()).isEqualTo(2); |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| assertThat(column.getChildCount()).isEqualTo(2); |
| |
| FrameLayout tv1Wrapper = (FrameLayout) column.getChildAt(0); |
| FrameLayout tv2Wrapper = (FrameLayout) column.getChildAt(1); |
| TextView tv1 = (TextView) tv1Wrapper.getChildAt(0); |
| TextView tv2 = (TextView) tv2Wrapper.getChildAt(0); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| |
| // Produce a new layout with the second Text element changed. |
| |
| Layout layout2 = |
| layout( |
| column( // 1 |
| dynamicFixedText("Hello"), // 1.1 |
| dynamicFixedText("Mars") // 1.2 |
| )); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| assertThat(renderer.getDataPipelineSize()).isEqualTo(2); |
| ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| assertThat(columnAfterMutation.getChildCount()).isEqualTo(2); |
| FrameLayout tv1WrapperAfterMutation = (FrameLayout) columnAfterMutation.getChildAt(0); |
| FrameLayout tv2WrapperAfterMutation = (FrameLayout) columnAfterMutation.getChildAt(1); |
| TextView tv1AfterMutation = (TextView) tv1WrapperAfterMutation.getChildAt(0); |
| TextView tv2AfterMutation = (TextView) tv2WrapperAfterMutation.getChildAt(0); |
| |
| // Unchanged views should be left exactly the same: |
| assertThat(columnAfterMutation).isSameInstanceAs(column); |
| assertThat(tv1WrapperAfterMutation).isSameInstanceAs(tv1Wrapper); |
| assertThat(tv1AfterMutation).isSameInstanceAs(tv1); |
| // Overall content should match layout2: |
| assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText().toString()).isEqualTo("Mars"); |
| } |
| |
| @Test |
| public void inflateThenMutate_withSelfMutation_dataPipelineIsPreserved() { |
| Layout layout1 = |
| layout( |
| column( // 1 |
| props -> props.widthDp = 10, |
| dynamicFixedText("Hello"), // 1.1 |
| dynamicFixedText("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| assertThat(renderer.getDataPipelineSize()).isEqualTo(2); |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| assertThat(column.getChildCount()).isEqualTo(2); |
| |
| FrameLayout tv1Wrapper = (FrameLayout) column.getChildAt(0); |
| TextView tv1 = (TextView) tv1Wrapper.getChildAt(0); |
| FrameLayout tv2Wrapper = (FrameLayout) column.getChildAt(1); |
| TextView tv2 = (TextView) tv2Wrapper.getChildAt(0); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| |
| // Produce a new layout with the column width changed. |
| Layout layout2 = |
| layout( |
| column( // 1 |
| props -> props.widthDp = 20, |
| dynamicFixedText("Hello"), // 1.1 |
| dynamicFixedText("World") // 1.2 |
| )); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| assertThat(renderer.getDataPipelineSize()).isEqualTo(2); |
| ViewGroup columnAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| assertThat(columnAfterMutation.getChildCount()).isEqualTo(2); |
| FrameLayout tv1WrapperAfterMutation = (FrameLayout) columnAfterMutation.getChildAt(0); |
| TextView tv1AfterMutation = (TextView) tv1WrapperAfterMutation.getChildAt(0); |
| FrameLayout tv2WrapperAfterMutation = (FrameLayout) columnAfterMutation.getChildAt(1); |
| TextView tv2AfterMutation = (TextView) tv2WrapperAfterMutation.getChildAt(0); |
| |
| // Unchanged views should be left exactly the same: |
| expect.that(tv1AfterMutation).isSameInstanceAs(tv1); |
| expect.that(tv2AfterMutation).isSameInstanceAs(tv2); |
| expect.that(tv1WrapperAfterMutation).isSameInstanceAs(tv1Wrapper); |
| expect.that(tv2WrapperAfterMutation).isSameInstanceAs(tv2Wrapper); |
| // Overall content should match layout2: |
| assertThat(tv1AfterMutation.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText().toString()).isEqualTo("World"); |
| } |
| |
| @Test |
| public void reInflate_dataPipelineIsReset() { |
| Layout layout = |
| layout( |
| column( // 1 |
| dynamicFixedText("Hello"), // 1.1 |
| dynamicFixedText("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| assertThat(renderer.getDataPipelineSize()).isEqualTo(2); |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| assertThat(column.getChildCount()).isEqualTo(2); |
| |
| FrameLayout tv1Wrapper = (FrameLayout) column.getChildAt(0); |
| FrameLayout tv2Wrapper = (FrameLayout) column.getChildAt(1); |
| TextView tv1 = (TextView) tv1Wrapper.getChildAt(0); |
| TextView tv2 = (TextView) tv2Wrapper.getChildAt(0); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| |
| // Re-inflate and check that the number of nodes is the same as the previous inflation. |
| renderer.inflate(); |
| assertThat(renderer.getDataPipelineSize()).isEqualTo(2); |
| } |
| |
| @Test |
| public void inflateWithNoFingerprint_producesNoRenderingMetadata() { |
| Layout layout1WithNoFingerprints = |
| layout(text("Hello")).toBuilder().clearFingerprint().build(); |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1WithNoFingerprints); |
| |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| TextView tv = (TextView) inflatedViewParent.getChildAt(0); |
| |
| assertThat(tv.getText().toString()).isEqualTo("Hello"); |
| assertThat(getRenderedMetadata(inflatedViewParent)).isNull(); |
| } |
| |
| @Test |
| public void inflateArcThenMutate_withChangeToText_causesUpdate() { |
| Layout layout1 = |
| layout( |
| arc( // 1 |
| arcText("Hello"), // 1.1 |
| arcText("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| assertThat(inflatedViewParent.getChildCount()).isEqualTo(1); |
| ArcLayout arcLayout = (ArcLayout) inflatedViewParent.getChildAt(0); |
| assertThat(arcLayout.getChildCount()).isEqualTo(2); |
| CurvedTextView tv1 = (CurvedTextView) arcLayout.getChildAt(0); |
| CurvedTextView tv2 = (CurvedTextView) arcLayout.getChildAt(1); |
| assertThat(tv1.getText()).isEqualTo("Hello"); |
| assertThat(tv2.getText()).isEqualTo("World"); |
| |
| // Produce a new layout with only one Text element changed. |
| Layout layout2 = |
| layout( |
| arc( // 1 |
| arcText("Hello"), // 1.1 |
| arcText("Mars") // 1.2 |
| )); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| assertThat(inflatedViewParent.getChildCount()).isEqualTo(1); |
| ArcLayout arcLayoutAfterMutation = (ArcLayout) inflatedViewParent.getChildAt(0); |
| |
| // Overall content should match layout2: |
| CurvedTextView tv1AfterMutation = (CurvedTextView) arcLayout.getChildAt(0); |
| assertThat(tv1AfterMutation.getText()).isEqualTo("Hello"); |
| CurvedTextView tv2AfterMutation = (CurvedTextView) arcLayout.getChildAt(1); |
| assertThat(tv2AfterMutation.getText()).isEqualTo("Mars"); |
| |
| // Unchanged views should be left exactly the same: |
| assertThat(arcLayoutAfterMutation).isSameInstanceAs(arcLayout); |
| assertThat(arcLayoutAfterMutation.getChildCount()).isEqualTo(2); |
| assertThat(tv1AfterMutation).isSameInstanceAs(tv1); |
| } |
| |
| @Test |
| public void inflateArcThenMutate_withChangeToProps_causesUpdate() throws Exception { |
| Layout layout1 = |
| layout( |
| arc( // 1 |
| arcText("Hello"), // 1.1 |
| arcText("World") // 1.2 |
| )); |
| |
| // Check the premutation layout |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| assertThat(inflatedViewParent.getChildCount()).isEqualTo(1); |
| ArcLayout arcLayout = (ArcLayout) inflatedViewParent.getChildAt(0); |
| assertThat(arcLayout.getAnchorAngleDegrees()).isEqualTo(0); |
| assertThat(arcLayout.getChildCount()).isEqualTo(2); |
| CurvedTextView tv1 = (CurvedTextView) arcLayout.getChildAt(0); |
| CurvedTextView tv2 = (CurvedTextView) arcLayout.getChildAt(1); |
| assertThat(tv1.getText()).isEqualTo("Hello"); |
| assertThat(tv2.getText()).isEqualTo("World"); |
| |
| Layout layout2 = |
| layout( |
| arc( // 1 |
| props -> props.anchorAngleDegrees = 35, |
| arcText("Hello"), // 1.1 |
| arcText("World") // 1.2 |
| )); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.mRenderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| |
| // Check the post-mutation layout |
| ArcLayout arcLayoutAfterMutation = (ArcLayout) inflatedViewParent.getChildAt(0); |
| assertThat(arcLayoutAfterMutation.getChildCount()).isEqualTo(2); |
| assertThat(arcLayoutAfterMutation.getAnchorAngleDegrees()).isEqualTo(35); |
| CurvedTextView tv1AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(0); |
| CurvedTextView tv2AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(1); |
| assertThat(tv1AfterMutation.getText()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText()).isEqualTo("World"); |
| assertThat(tv1AfterMutation).isSameInstanceAs(tv1); |
| assertThat(tv2AfterMutation).isSameInstanceAs(tv2); |
| } |
| |
| @Test |
| @Ignore("b/262537912") |
| public void viewChangesWhileComputingMutation_applyMutationFails() throws Exception { |
| Layout layout1 = |
| layout( |
| arc( // 1 |
| arcText("Hello"), // 1.1 |
| arcText("World") // 1.2 |
| )); |
| Layout layout2 = |
| layout( |
| arc( // 1 |
| props -> props.anchorAngleDegrees = 35, |
| arcText("Hello"), // 1.1 |
| arcText("World") // 1.2 |
| )); |
| Layout layout3 = |
| layout( |
| arc( // 1 |
| arcText("Hello") // 1.1 |
| )); |
| // Check the premutation layout |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent1 = renderer.inflate(); |
| // Compute the mutation |
| ViewGroupMutation mutation2 = |
| renderer.mRenderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent1), layout2, ViewProperties.EMPTY); |
| ViewGroupMutation mutation3 = |
| renderer.mRenderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent1), layout3, ViewProperties.EMPTY); |
| |
| renderer.mRenderer.applyMutation(inflatedViewParent1, mutation3).get(); |
| assertThrows( |
| ViewMutationException.class, |
| () -> renderer.mRenderer.applyMutation(inflatedViewParent1, mutation2).get()); |
| } |
| |
| @Test |
| public void inflateArcThenMutate_withDifferentNumberOfChildren_causesUpdate() { |
| Layout layout1 = |
| layout( |
| arc( // 1 |
| arcText("Hello"), // 1.1 |
| arcText("World") // 1.2 |
| )); |
| |
| // Check the premutation layout |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| assertThat(inflatedViewParent.getChildCount()).isEqualTo(1); |
| ArcLayout arcLayout = (ArcLayout) inflatedViewParent.getChildAt(0); |
| assertThat(arcLayout.getChildCount()).isEqualTo(2); |
| CurvedTextView tv1 = (CurvedTextView) arcLayout.getChildAt(0); |
| CurvedTextView tv2 = (CurvedTextView) arcLayout.getChildAt(1); |
| assertThat(tv1.getText()).isEqualTo("Hello"); |
| assertThat(tv2.getText()).isEqualTo("World"); |
| |
| Layout layout2 = |
| layout( |
| arc( // 1 |
| arcText("Hello"), // 1.1 |
| arcText("World"), // 1.2 |
| arcText("and"), // 1.3 |
| arcText("Mars") // 1.4 |
| )); |
| |
| // Compute the mutation |
| ViewGroupMutation mutation = |
| renderer.computeMutation(getRenderedMetadata(inflatedViewParent), layout2); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isFalse(); |
| |
| // Apply the mutation |
| boolean mutationResult = renderer.applyMutation(inflatedViewParent, mutation); |
| assertThat(mutationResult).isTrue(); |
| |
| // Check the post-mutation layout |
| ArcLayout arcLayoutAfterMutation = (ArcLayout) inflatedViewParent.getChildAt(0); |
| assertThat(arcLayoutAfterMutation.getChildCount()).isEqualTo(4); |
| CurvedTextView tv1AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(0); |
| CurvedTextView tv2AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(1); |
| CurvedTextView tv3AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(2); |
| CurvedTextView tv4AfterMutation = (CurvedTextView) arcLayoutAfterMutation.getChildAt(3); |
| assertThat(tv1AfterMutation.getText()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation.getText()).isEqualTo("World"); |
| assertThat(tv3AfterMutation.getText()).isEqualTo("and"); |
| assertThat(tv4AfterMutation.getText()).isEqualTo("Mars"); |
| } |
| |
| @Test |
| public void inflateAndMutateTwice_causesTwoUpdates() throws Exception { |
| Layout layout1 = |
| layout( |
| column( // 1 |
| text("Hello"), // 1.1 |
| text("World") // 1.2 |
| )); |
| |
| // Do the initial inflation. |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| ViewGroup column = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1 = (TextView) column.getChildAt(0); |
| TextView tv2 = (TextView) column.getChildAt(1); |
| assertThat(tv1.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2.getText().toString()).isEqualTo("World"); |
| |
| Layout layout2 = |
| layout( |
| column( // 1 |
| text("Goodbye"), // 1.1 |
| text("World") // 1.2 |
| )); |
| |
| // Apply first mutation |
| ViewGroupMutation mutation1 = |
| renderer.mRenderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY); |
| assertThat(mutation1).isNotNull(); |
| assertThat(mutation1.isNoOp()).isFalse(); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation1).get(); |
| |
| ViewGroup columnAfterMutation1 = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1AfterMutation1 = (TextView) columnAfterMutation1.getChildAt(0); |
| TextView tv2AfterMutation1 = (TextView) columnAfterMutation1.getChildAt(1); |
| assertThat(tv1AfterMutation1.getText().toString()).isEqualTo("Goodbye"); |
| assertThat(tv2AfterMutation1.getText().toString()).isEqualTo("World"); |
| |
| Layout layout3 = |
| layout( |
| column( // 1 |
| text("Hello"), // 1.1 |
| text("Mars") // 1.2 |
| )); |
| |
| // Apply second mutation |
| ViewGroupMutation mutation2 = |
| renderer.mRenderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), layout3, ViewProperties.EMPTY); |
| assertThat(mutation2).isNotNull(); |
| assertThat(mutation2.isNoOp()).isFalse(); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation2).get(); |
| |
| ViewGroup columnAfterMutation2 = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView tv1AfterMutation2 = (TextView) columnAfterMutation2.getChildAt(0); |
| TextView tv2AfterMutation2 = (TextView) columnAfterMutation2.getChildAt(1); |
| assertThat(tv1AfterMutation2.getText().toString()).isEqualTo("Hello"); |
| assertThat(tv2AfterMutation2.getText().toString()).isEqualTo("Mars"); |
| } |
| |
| @Test |
| public void inflateArcThenMutate_withNoChange_producesNoOpMutation() { |
| Layout layout = |
| layout( |
| arc( // 1 |
| arcText("Hello"), // 1.1 |
| arcText("World") // 1.2 |
| )); |
| |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| RenderedMetadata renderedMetadata = getRenderedMetadata(inflatedViewParent); |
| |
| // Compute the mutation for the same layout |
| ViewGroupMutation mutation = renderer.computeMutation(renderedMetadata, layout); |
| assertThat(mutation).isNotNull(); |
| assertThat(mutation.isNoOp()).isTrue(); |
| } |
| |
| @Test |
| public void inflateArcWithNoFingerprint_producesNoRenderingMetadata() { |
| Layout layout1WithNoFingerprints = |
| layout(arc(arcText("Hello"))).toBuilder().clearFingerprint().build(); |
| // Check that we have the initial layout correctly rendered |
| Renderer renderer = renderer(layout1WithNoFingerprints); |
| |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| |
| assertThat(getRenderedMetadata(inflatedViewParent)).isNull(); |
| } |
| |
| @Test |
| public void boxWithChild_childChanges_appliesGravityToUpdatedChild() throws Exception { |
| Layout layout1 = |
| layout( |
| box( // 1 |
| boxProps -> { |
| boxProps.horizontalAlignment = |
| HorizontalAlignment.HORIZONTAL_ALIGN_CENTER; |
| boxProps.verticalAlignment = |
| VerticalAlignment.VERTICAL_ALIGN_CENTER; |
| }, |
| text("Hello") // 1.1 |
| )); |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| Layout layout2 = |
| layout( |
| box( // 1 |
| boxProps -> { |
| boxProps.horizontalAlignment = |
| HorizontalAlignment.HORIZONTAL_ALIGN_CENTER; |
| boxProps.verticalAlignment = |
| VerticalAlignment.VERTICAL_ALIGN_CENTER; |
| }, |
| text("World") // 1.1 |
| )); |
| |
| ViewGroupMutation mutation = |
| renderer.mRenderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| |
| ViewGroup boxAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView textAfterMutation = (TextView) boxAfterMutation.getChildAt(0); |
| LayoutParams layoutParamsAfterMutation = (LayoutParams) textAfterMutation.getLayoutParams(); |
| assertThat(layoutParamsAfterMutation.gravity) |
| .isEqualTo( |
| getFrameLayoutGravity( |
| HorizontalAlignment.HORIZONTAL_ALIGN_CENTER, |
| VerticalAlignment.VERTICAL_ALIGN_CENTER)); |
| } |
| |
| @Test |
| public void boxWithChild_boxChanges_appliesNewGravityToChild() throws Exception { |
| Layout layout1 = |
| layout( |
| box( // 1 |
| boxProps -> { |
| boxProps.horizontalAlignment = |
| HorizontalAlignment.HORIZONTAL_ALIGN_CENTER; |
| boxProps.verticalAlignment = |
| VerticalAlignment.VERTICAL_ALIGN_CENTER; |
| }, |
| text("Hello") // 1.1 |
| )); |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| Layout layout2 = |
| layout( |
| box( // 1 |
| boxProps -> { |
| // A different set of alignments. |
| boxProps.horizontalAlignment = |
| HorizontalAlignment.HORIZONTAL_ALIGN_LEFT; |
| boxProps.verticalAlignment = |
| VerticalAlignment.VERTICAL_ALIGN_BOTTOM; |
| }, |
| text("Hello") // 1.1 |
| )); |
| |
| ViewGroupMutation mutation = |
| renderer.mRenderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| |
| ViewGroup boxAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView textAfterMutation = (TextView) boxAfterMutation.getChildAt(0); |
| LayoutParams layoutParamsAfterMutation = (LayoutParams) textAfterMutation.getLayoutParams(); |
| assertThat(layoutParamsAfterMutation.gravity) |
| .isEqualTo( |
| getFrameLayoutGravity( |
| HorizontalAlignment.HORIZONTAL_ALIGN_LEFT, |
| VerticalAlignment.VERTICAL_ALIGN_BOTTOM)); |
| } |
| |
| @Test |
| public void boxWithChild_bothChange_appliesNewGravityToUpdatedChild() throws Exception { |
| Layout layout1 = |
| layout( |
| box( // 1 |
| boxProps -> { |
| boxProps.horizontalAlignment = |
| HorizontalAlignment.HORIZONTAL_ALIGN_CENTER; |
| boxProps.verticalAlignment = |
| VerticalAlignment.VERTICAL_ALIGN_CENTER; |
| }, |
| text("Hello") // 1.1 |
| )); |
| // Do the initial inflation. |
| Renderer renderer = renderer(layout1); |
| ViewGroup inflatedViewParent = renderer.inflate(); |
| Layout layout2 = |
| layout( |
| box( // 1 |
| boxProps -> { |
| // A different set of alignments. |
| boxProps.horizontalAlignment = |
| HorizontalAlignment.HORIZONTAL_ALIGN_LEFT; |
| boxProps.verticalAlignment = |
| VerticalAlignment.VERTICAL_ALIGN_BOTTOM; |
| }, |
| text("World") // 1.1 |
| )); |
| |
| ViewGroupMutation mutation = |
| renderer.mRenderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), layout2, ViewProperties.EMPTY); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| |
| ViewGroup boxAfterMutation = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView textAfterMutation = (TextView) boxAfterMutation.getChildAt(0); |
| LayoutParams layoutParamsAfterMutation = (LayoutParams) textAfterMutation.getLayoutParams(); |
| assertThat(layoutParamsAfterMutation.gravity) |
| .isEqualTo( |
| getFrameLayoutGravity( |
| HorizontalAlignment.HORIZONTAL_ALIGN_LEFT, |
| VerticalAlignment.VERTICAL_ALIGN_BOTTOM)); |
| } |
| |
| private static Span textSpan(String text) { |
| return Span.newBuilder() |
| .setText(SpanText.newBuilder().setText(string(text)).build()) |
| .build(); |
| } |
| |
| private ResourceResolvers.Builder resourceResolvers() { |
| return StandardResourceResolvers.forLocalApp( |
| buildResources(), |
| getApplicationContext(), |
| ContextCompat.getMainExecutor(getApplicationContext()), |
| true); |
| } |
| |
| private static Layout fingerprintedLayout(LayoutElement rootElement) { |
| return TestFingerprinter.getDefault().buildLayoutWithFingerprints(rootElement); |
| } |
| |
| private static ProtoLayoutTheme loadTheme(int themeResId) { |
| return new ProtoLayoutThemeImpl(getApplicationContext(), themeResId); |
| } |
| |
| ProtoLayoutInflater.Config.Builder newRendererConfigBuilder(Layout layout) { |
| return newRendererConfigBuilder(layout, resourceResolvers()); |
| } |
| |
| ProtoLayoutInflater.Config.Builder newRendererConfigBuilder( |
| Layout layout, ResourceResolvers.Builder resourceResolvers) { |
| return new ProtoLayoutInflater.Config.Builder( |
| getApplicationContext(), layout, resourceResolvers.build()) |
| .setClickableIdExtra(EXTRA_CLICKABLE_ID) |
| .setLoadActionListener(p -> {}) |
| .setLoadActionExecutor(ContextCompat.getMainExecutor(getApplicationContext())) |
| .setApplyFontVariantBodyAsDefault(true); |
| } |
| |
| private Renderer renderer(Layout layout) { |
| return renderer(newRendererConfigBuilder(layout), new FixedQuotaManagerImpl(MAX_VALUE)); |
| } |
| |
| // Renderer using a dataPipeline with default values. |
| private Renderer renderer(ProtoLayoutInflater.Config.Builder rendererConfigBuilder) { |
| return renderer(rendererConfigBuilder, new FixedQuotaManagerImpl(MAX_VALUE)); |
| } |
| |
| @SuppressWarnings("RestrictTo") |
| private Renderer renderer( |
| ProtoLayoutInflater.Config.Builder rendererConfigBuilder, |
| FixedQuotaManagerImpl quotaManager) { |
| mDataPipeline = |
| new ProtoLayoutDynamicDataPipeline( |
| /* platformDataProviders= */ ImmutableMap.of(), |
| mStateStore, |
| quotaManager, |
| new FixedQuotaManagerImpl(MAX_VALUE)); |
| rendererConfigBuilder.setDynamicDataPipeline(mDataPipeline); |
| return new Renderer(rendererConfigBuilder.build(), mDataPipeline); |
| } |
| |
| @SuppressWarnings("RestrictTo") |
| private static final class Renderer { |
| final ProtoLayoutInflater mRenderer; |
| final ProtoLayoutDynamicDataPipeline mDataPipeline; |
| |
| Renderer( |
| ProtoLayoutInflater.Config rendererConfig, |
| ProtoLayoutDynamicDataPipeline dataPipeline) { |
| this.mRenderer = new ProtoLayoutInflater(rendererConfig); |
| this.mDataPipeline = dataPipeline; |
| } |
| |
| FrameLayout inflate() { |
| FrameLayout rootLayout = new FrameLayout(getApplicationContext()); |
| // This needs to be an attached view to test animations in data pipeline. |
| Robolectric.buildActivity(Activity.class).setup().get().setContentView(rootLayout); |
| InflateResult inflateResult = mRenderer.inflate(rootLayout); |
| if (inflateResult != null) { |
| inflateResult.updateDynamicDataPipeline(/* isReattaching= */ false); |
| } |
| shadowOf(Looper.getMainLooper()).idle(); |
| doLayout(rootLayout); |
| |
| return rootLayout; |
| } |
| |
| ViewGroupMutation computeMutation(RenderedMetadata renderedMetadata, Layout targetLayout) { |
| return mRenderer.computeMutation(renderedMetadata, targetLayout, ViewProperties.EMPTY); |
| } |
| |
| boolean applyMutation(ViewGroup parent, ViewGroupMutation mutation) { |
| try { |
| ListenableFuture<Void> applyMutationFuture = |
| mRenderer.applyMutation(parent, mutation); |
| shadowOf(Looper.getMainLooper()).idle(); |
| applyMutationFuture.get(); |
| doLayout(parent); |
| return true; |
| } catch (ViewMutationException | ExecutionException | InterruptedException ex) { |
| return false; |
| } |
| } |
| |
| int getDataPipelineSize() { |
| return mDataPipeline.size(); |
| } |
| |
| private void doLayout(View rootLayout) { |
| // Run a layout pass etc. This is required for basically everything that tries to make |
| // assertions about width/height, or relative placement. |
| int screenWidth = MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, MeasureSpec.EXACTLY); |
| int screenHeight = MeasureSpec.makeMeasureSpec(SCREEN_HEIGHT, MeasureSpec.EXACTLY); |
| rootLayout.measure(screenWidth, screenHeight); |
| rootLayout.layout(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); |
| } |
| } |
| |
| private static Resources buildResources() { |
| return Resources.newBuilder() |
| .putIdToImage( |
| "android", |
| ImageResource.newBuilder() |
| .setAndroidResourceByResId( |
| AndroidImageResourceByResId.newBuilder() |
| .setResourceId(R.drawable.android_24dp)) |
| .build()) |
| .putIdToImage( |
| "android_AVD", |
| ImageResource.newBuilder() |
| .setAndroidAnimatedResourceByResId( |
| AndroidAnimatedImageResourceByResId.newBuilder() |
| .setAnimatedImageFormat( |
| AnimatedImageFormat |
| .ANIMATED_IMAGE_FORMAT_AVD) |
| .setResourceId(android_animated_24dp) |
| .setStartTrigger(onVisibleTrigger())) |
| .build()) |
| .putIdToImage( |
| "android_AVD_pretending_to_be_static", |
| ImageResource.newBuilder() |
| .setAndroidResourceByResId( |
| AndroidImageResourceByResId.newBuilder() |
| .setResourceId(android_animated_24dp)) |
| .build()) |
| .putIdToImage( |
| "android_seekable_AVD", |
| ImageResource.newBuilder() |
| .setAndroidSeekableAnimatedResourceByResId( |
| AndroidSeekableAnimatedImageResourceByResId.newBuilder() |
| .setAnimatedImageFormat( |
| AnimatedImageFormat |
| .ANIMATED_IMAGE_FORMAT_AVD) |
| .setResourceId(android_animated_24dp) |
| .setProgress( |
| DynamicFloat.newBuilder() |
| .setAnimatableDynamic( |
| stateDynamicFloat()) |
| .build()) |
| .build()) |
| .build()) |
| .putIdToImage( |
| "does_not_exist", |
| ImageResource.newBuilder() |
| .setAndroidResourceByResId( |
| AndroidImageResourceByResId.newBuilder().setResourceId(-1)) |
| .build()) |
| .putIdToImage( |
| "large_image_120dp", |
| ImageResource.newBuilder() |
| .setAndroidResourceByResId( |
| AndroidImageResourceByResId.newBuilder() |
| .setResourceId(R.drawable.ic_channel_foreground)) |
| .build()) |
| .putIdToImage("no_android_resource_set", ImageResource.getDefaultInstance()) |
| .build(); |
| } |
| |
| @NonNull |
| private static Trigger onVisibleTrigger() { |
| return Trigger.newBuilder() |
| .setOnVisibleTrigger(OnVisibleTrigger.getDefaultInstance()) |
| .build(); |
| } |
| |
| @NonNull |
| private static AnimatableDynamicFloat.Builder stateDynamicFloat() { |
| return AnimatableDynamicFloat.newBuilder() |
| .setInput( |
| DynamicFloat.newBuilder() |
| .setStateSource( |
| StateFloatSource.newBuilder().setSourceKey("anim_val"))); |
| } |
| |
| @Test |
| public void inflate_row_withLayoutWeight() { |
| final String protoResId = "android"; |
| |
| LayoutElement image = buildImage(protoResId, 30, 30); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setRow( |
| Row.newBuilder() |
| .setWidth(expand()) |
| .setHeight(expand()) |
| .addContents( |
| LayoutElement.newBuilder() |
| .setRow( |
| Row.newBuilder() |
| .setWidth(expandWeight()) |
| .addContents(image) |
| .build()))) |
| .build(); |
| |
| FrameLayout layout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // There should be a child ViewGroup which is a LinearLayout. |
| assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class); |
| ViewGroup firstChild = (ViewGroup) layout.getChildAt(0); |
| ViewGroup rowWithWeight = (ViewGroup) firstChild.getChildAt(0); |
| |
| LinearLayout.LayoutParams linearLayoutParams = |
| (LinearLayout.LayoutParams) rowWithWeight.getLayoutParams(); |
| |
| expect.that(linearLayoutParams.weight).isEqualTo(10.0f); |
| } |
| |
| @NonNull |
| private static ContainerDimension expandWeight() { |
| return ContainerDimension.newBuilder() |
| .setExpandedDimension( |
| ExpandedDimensionProp.newBuilder() |
| .setLayoutWeight(FloatProp.newBuilder().setValue(10.0f).build()) |
| .build()) |
| .build(); |
| } |
| |
| @Test |
| public void inflate_column_withLayoutWeight() { |
| final String protoResId = "android"; |
| |
| LayoutElement image = buildImage(protoResId, 30, 30); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setColumn( |
| Column.newBuilder() |
| .setWidth(expand()) |
| .setHeight(expand()) |
| .addContents( |
| LayoutElement.newBuilder() |
| .setColumn( |
| Column.newBuilder() |
| .setHeight(expandWeight()) |
| .addContents(image) |
| .build()))) |
| .build(); |
| |
| FrameLayout layout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // There should be a child ViewGroup which is a LinearLayout. |
| assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class); |
| ViewGroup firstChild = (ViewGroup) layout.getChildAt(0); |
| ViewGroup columnWithWeight = (ViewGroup) firstChild.getChildAt(0); |
| |
| LinearLayout.LayoutParams linearLayoutParams = |
| (LinearLayout.LayoutParams) columnWithWeight.getLayoutParams(); |
| |
| expect.that(linearLayoutParams.weight).isEqualTo(10.0f); |
| } |
| |
| @Test |
| public void inflate_box_withLayoutWeight() { |
| final String protoResId = "android"; |
| |
| LayoutElement image = buildImage(protoResId, 30, 30); |
| |
| LayoutElement root = |
| LayoutElement.newBuilder() |
| .setRow( |
| Row.newBuilder() |
| .setWidth(expand()) |
| .setHeight(expand()) |
| .addContents( |
| LayoutElement.newBuilder() |
| .setBox( |
| Box.newBuilder() |
| .setWidth(expandWeight()) |
| .addContents(image) |
| .build()))) |
| .build(); |
| |
| FrameLayout layout = renderer(fingerprintedLayout(root)).inflate(); |
| |
| // There should be a child ViewGroup which is a LinearLayout. |
| assertThat(layout.getChildAt(0)).isInstanceOf(ViewGroup.class); |
| ViewGroup firstChild = (ViewGroup) layout.getChildAt(0); |
| ViewGroup boxWithWeight = (ViewGroup) firstChild.getChildAt(0); |
| |
| LinearLayout.LayoutParams linearLayoutParams = |
| (LinearLayout.LayoutParams) boxWithWeight.getLayoutParams(); |
| |
| expect.that(linearLayoutParams.weight).isEqualTo(10.0f); |
| } |
| |
| @Test |
| public void enterTransition_noQuota_notPlayed() throws Exception { |
| Renderer renderer = |
| renderer( |
| newRendererConfigBuilder(fingerprintedLayout(textFadeIn("Hello"))), |
| new FixedQuotaManagerImpl(/* quotaCap= */ 0)); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout(textFadeIn("World"))); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| } |
| |
| @Test |
| public void enterTransition_animationEnabled_hasEnterAnimation() throws Exception { |
| Renderer renderer = renderer(fingerprintedLayout(textFadeIn("Hello"))); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout(textFadeIn("World"))); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| |
| // Idle for running code for starting animations. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| // Idle for calling the onStart listener so that animation has started status. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1); |
| } |
| |
| @Test |
| public void multipleEnterTransition_animationEnabled_correctlyReleaseQuota() throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| LayoutElement.newBuilder() |
| .setColumn( |
| Column.newBuilder() |
| .addContents(textFadeIn("Hello")) |
| .addContents(textFadeInSlideIn("Hello2")) |
| .build()) |
| .build())); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| LayoutElement.newBuilder() |
| .setColumn( |
| Column.newBuilder() |
| .addContents(textFadeIn("World")) |
| .addContents(textFadeInSlideIn("World2")) |
| .build()) |
| .build())); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| |
| // Idle for running code for starting animations. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| // Idle for calling the onStart listener so that animation has started status. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(/* fadeInx2 + slideIn */ 3); |
| |
| // This is needed to let enter animations finish. |
| ShadowChoreographer.setPaused(false); |
| ShadowSystemClock.advanceBy(Duration.ofSeconds(1)); |
| shadowOf(getMainLooper()).idle(); |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| assertThat(mDataPipeline.isAllQuotaReleased()).isTrue(); |
| } |
| |
| @Test |
| public void multipleEnterTransition_withDelay_animationEnabled_notOverlapping_correctlyPlays() |
| throws Exception { |
| Renderer renderer = |
| renderer( |
| newRendererConfigBuilder( |
| fingerprintedLayout( |
| LayoutElement.newBuilder() |
| .setColumn( |
| Column.newBuilder() |
| .addContents( |
| textFadeIn( |
| "Hello", |
| /* delay= */ 0)) |
| .addContents( |
| textFadeIn( |
| "Hello2", |
| /* delay= */ 600))) |
| .build())), |
| new FixedQuotaManagerImpl(/* quotaCap= */ 1)); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| LayoutElement.newBuilder() |
| .setColumn( |
| Column.newBuilder() |
| .addContents( |
| textFadeIn("World", /* delay= */ 0)) |
| .addContents( |
| textFadeIn( |
| "World2", |
| /* delay= */ 600))) |
| .build())); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| |
| // First content transition animation Idle for running code for starting animations. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| // Idle for calling the onStart listener so that animation has started status. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1); |
| |
| // Second content transition animation Idle for running code for starting animations. |
| shadowOf(getMainLooper()).idleFor(Duration.ofMillis(500)); |
| // Idle for calling the onStart listener so that animation has started status. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| // Quota cap is 1, but since the first animating is finished, this should be played too. |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1); |
| |
| // This is needed to let enter animations finish. |
| ShadowChoreographer.setPaused(false); |
| ShadowSystemClock.advanceBy(Duration.ofSeconds(1)); |
| shadowOf(getMainLooper()).idle(); |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| assertThat(mDataPipeline.isAllQuotaReleased()).isTrue(); |
| } |
| |
| @Test |
| public void multipleEnterTransition_withDelay_animationEnabled_overlapping_playesOne() |
| throws Exception { |
| Renderer renderer = |
| renderer( |
| newRendererConfigBuilder( |
| fingerprintedLayout( |
| LayoutElement.newBuilder() |
| .setColumn( |
| Column.newBuilder() |
| .addContents( |
| textFadeIn( |
| "Hello", |
| /* delay= */ 400)) |
| .addContents( |
| textFadeIn( |
| "Hello2", |
| /* delay= */ 600))) |
| .build())), |
| new FixedQuotaManagerImpl(/* quotaCap= */ 1)); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| LayoutElement.newBuilder() |
| .setColumn( |
| Column.newBuilder() |
| .addContents( |
| textFadeIn( |
| "World", /* delay= */ 400)) |
| .addContents( |
| textFadeIn( |
| "World2", |
| /* delay= */ 600))) |
| .build())); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| |
| // First content transition animation Idle for running code for starting animations. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(500)); |
| ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
| // Idle for calling the onStart listener so that animation has started status. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| // Since we've run delayed tasks, second animation also got a chance to be run, but quota |
| // prevented it. |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1); |
| |
| // This is needed to let enter animations finish. |
| ShadowChoreographer.setPaused(false); |
| ShadowSystemClock.advanceBy(Duration.ofSeconds(1)); |
| shadowOf(getMainLooper()).idle(); |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| assertThat(mDataPipeline.isAllQuotaReleased()).isTrue(); |
| } |
| |
| @Test |
| public void enterTransition_animationDisabled_noEnterAnimations() throws Exception { |
| Renderer renderer = |
| renderer( |
| newRendererConfigBuilder(fingerprintedLayout(textFadeIn("Hello"))) |
| .setAnimationEnabled(false)); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout(textFadeIn("World"))); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| } |
| |
| @Test |
| public void enterTransition_notFullyVisible_noEnterAnimation() throws Exception { |
| Renderer renderer = renderer(fingerprintedLayout(textFadeIn("Hello"))); |
| mDataPipeline.setFullyVisible(false); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout(textFadeIn("World"))); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| } |
| |
| @Test |
| public void exitTransition_noQuota_notPlayed() throws Exception { |
| Renderer renderer = |
| renderer( |
| newRendererConfigBuilder( |
| fingerprintedLayout( |
| getTextElementWithExitAnimation( |
| "Hello", /* iterations= */ 1))), |
| new FixedQuotaManagerImpl(/* quotaCap= */ 0)); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout(textFadeIn("World"))); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| } |
| |
| @Test |
| public void exitTransition_animationEnabled_hasExitAnimation() throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("Hello", /* iterations= */ 1))); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout(textFadeIn("World"))); |
| ListenableFuture<Void> applyMutationFuture = |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation); |
| |
| // Idle for running code for starting animations. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| // Idle for calling the onStart listener so that animation has started status. |
| shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1); |
| |
| // Waiting on onAnimationEnd listener so that future is resolved. |
| ShadowChoreographer.setPaused(false); |
| shadowOf(getMainLooper()).idleFor(Duration.ofSeconds(5)); |
| |
| applyMutationFuture.get(); |
| } |
| |
| @Test |
| public void exitTransition_indefiniteRepeatable_ignored() throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("Hello", /* iterations= */ 0))); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("World", /* iterations= */ 0))); |
| ListenableFuture<Void> applyMutationFuture = |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| shadowOf(getMainLooper()).idle(); |
| applyMutationFuture.get(); |
| } |
| |
| @Test |
| public void exitTransition_animationDisabled_noExitAnimations() throws Exception { |
| Renderer renderer = |
| renderer( |
| newRendererConfigBuilder( |
| fingerprintedLayout( |
| getTextElementWithExitAnimation( |
| "Hello", /* iterations= */ 1))) |
| .setAnimationEnabled(false)); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("World", /* iterations= */ 1))); |
| ListenableFuture<Void> applyMutationFuture = |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| shadowOf(getMainLooper()).idle(); |
| applyMutationFuture.get(); |
| } |
| |
| @Test |
| public void exitTransition_notFullyVisible_noExitAnimation() throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("Hello", /* iterations= */ 1))); |
| mDataPipeline.setFullyVisible(false); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("World", /* iterations= */ 1))); |
| ListenableFuture<Void> applyMutationFuture = |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| shadowOf(getMainLooper()).idle(); |
| applyMutationFuture.get(); |
| } |
| |
| @Test |
| public void exitTransition_noChangeToLayoutContentWhileExitAnimationIsPlaying() |
| throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("Hello", /* iterations= */ 10))); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("World", /* iterations= */ 10))); |
| ListenableFuture<Void> applyMutationFuture = |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation); |
| |
| shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100)); |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1); |
| assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString()) |
| .isEqualTo("Hello"); |
| |
| ShadowChoreographer.setPaused(false); |
| shadowOf(getMainLooper()).idleFor(Duration.ofSeconds(5)); |
| applyMutationFuture.get(); |
| } |
| |
| @Test |
| public void exitTransition_afterExitAnimationsEnd_newLayoutGetApplied() throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("Hello", /* iterations= */ 10))); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("World", /* iterations= */ 10))); |
| ListenableFuture<Void> applyMutationFuture = |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation); |
| shadowOf(getMainLooper()).idle(); |
| |
| applyMutationFuture.get(); |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString()) |
| .isEqualTo("World"); |
| } |
| |
| @Test |
| public void exitTransition_removedNodes_triggersExitAnimation() throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| getMultipleTextElementWithExitAnimation( |
| Arrays.asList("Hello", "World"), /* iterations= */ 10))); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getMultipleTextElementWithExitAnimation( |
| Arrays.asList("Hello"), /* iterations= */ 10))); |
| ListenableFuture<Void> applyMutationFuture = |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation); |
| |
| shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100)); |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(2); |
| |
| ShadowChoreographer.setPaused(false); |
| shadowOf(getMainLooper()).idleFor(Duration.ofSeconds(5)); |
| applyMutationFuture.get(); |
| } |
| |
| @Test |
| public void layoutGetsApplied_whenApplyingSecondMutation_beforeExitAnimationsAreFinished() |
| throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("Hello", /* iterations= */ 10))); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getTextElementWithExitAnimation("World", /* iterations= */ 10))); |
| ListenableFuture<Void> applyMutationFuture = |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation); |
| |
| shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100)); |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1); |
| |
| ViewGroupMutation secondMutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getTextElementWithExitAnimation( |
| "Second mutation", |
| /* iterations= */ 10))); |
| |
| ListenableFuture<Void> applySecondMutationFuture = |
| renderer.mRenderer.applyMutation(inflatedViewParent, secondMutation); |
| |
| // the previous mutation should be finished |
| assertThat(applyMutationFuture.isDone()).isTrue(); |
| assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString()) |
| .isEqualTo("World"); |
| |
| shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100)); |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(1); |
| |
| ShadowChoreographer.setPaused(false); |
| shadowOf(getMainLooper()).idleFor(Duration.ofSeconds(5)); |
| applySecondMutationFuture.get(); |
| |
| assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0); |
| assertThat(((TextView) inflatedViewParent.getChildAt(0)).getText().toString()) |
| .isEqualTo("Second mutation"); |
| } |
| |
| @Test |
| public void slideInTransition_snapToOutside_startsFromOutsideParentBounds() throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| getTextElementWithSlideInAnimation( |
| "Hello", |
| /* snapTo= */ SLIDE_PARENT_SNAP_TO_OUTSIDE.getNumber()))); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getTextElementWithSlideInAnimation( |
| "World", |
| /* snapTo= */ SLIDE_PARENT_SNAP_TO_OUTSIDE.getNumber()))); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| ViewGroup box = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView textView = (TextView) box.getChildAt(0); |
| assertSlideInInitialOffset( |
| box, |
| textView, |
| inflatedViewParent.getLeft() - (textView.getLeft() + textView.getWidth())); |
| } |
| |
| @Test |
| public void slideInTransition_snapToInside_startsFromInsideParentBounds() throws Exception { |
| Renderer renderer = |
| renderer( |
| fingerprintedLayout( |
| getTextElementWithSlideInAnimation( |
| "Hello", |
| /* snapTo= */ SLIDE_PARENT_SNAP_TO_INSIDE.getNumber()))); |
| mDataPipeline.setFullyVisible(true); |
| FrameLayout inflatedViewParent = renderer.inflate(); |
| shadowOf(getMainLooper()).idle(); |
| ShadowChoreographer.setPaused(true); |
| ShadowChoreographer.setFrameDelay(Duration.ofMillis(15)); |
| |
| ViewGroupMutation mutation = |
| renderer.computeMutation( |
| getRenderedMetadata(inflatedViewParent), |
| fingerprintedLayout( |
| getTextElementWithSlideInAnimation( |
| "World", |
| /* snapTo= */ SLIDE_PARENT_SNAP_TO_INSIDE.getNumber()))); |
| renderer.mRenderer.applyMutation(inflatedViewParent, mutation).get(); |
| shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100)); |
| |
| ViewGroup box = (ViewGroup) inflatedViewParent.getChildAt(0); |
| TextView textView = (TextView) box.getChildAt(0); |
| assertSlideInInitialOffset( |
| box, textView, inflatedViewParent.getLeft() - textView.getLeft()); |
| } |
| |
| private void assertSlideInInitialOffset( |
| ViewGroup box, TextView textView, float expectedInitialOffset) { |
| Animation animation = textView.getAnimation(); |
| animation.initialize( |
| textView.getWidth(), textView.getHeight(), box.getWidth(), box.getHeight()); |
| Transformation transformation = new Transformation(); |
| textView.getAnimation().getTransformation(0, transformation); |
| float[] matrix = new float[9]; |
| transformation.getMatrix().getValues(matrix); |
| assertThat(matrix[2]).isWithin(0.1f).of(expectedInitialOffset); |
| } |
| |
| private LayoutElement textFadeIn(String text) { |
| return textFadeIn(text, 0); |
| } |
| |
| private LayoutElement textFadeIn(String text, int delay) { |
| return LayoutElement.newBuilder() |
| .setText( |
| Text.newBuilder() |
| .setModifiers( |
| Modifiers.newBuilder() |
| .setContentUpdateAnimation( |
| AnimatedVisibility.newBuilder() |
| .setEnterTransition( |
| enterFadeIn(delay)))) |
| .setText(string(text))) |
| .build(); |
| } |
| |
| @NonNull |
| private static EnterTransition.Builder enterFadeIn(int delay) { |
| return EnterTransition.newBuilder().setFadeIn(fadeIn(delay)); |
| } |
| |
| @NonNull |
| private static FadeInTransition.Builder fadeIn(int delay) { |
| return FadeInTransition.newBuilder() |
| .setAnimationSpec( |
| AnimationSpec.newBuilder() |
| .setAnimationParameters( |
| AnimationParameters.newBuilder().setDelayMillis(delay))); |
| } |
| |
| private LayoutElement textFadeInSlideIn(String text) { |
| return LayoutElement.newBuilder() |
| .setText( |
| textAnimVisibility( |
| AnimatedVisibility.newBuilder() |
| .setEnterTransition( |
| EnterTransition.newBuilder() |
| .setFadeIn( |
| FadeInTransition |
| .getDefaultInstance()) |
| .setSlideIn( |
| SlideInTransition.newBuilder() |
| .build())), |
| text)) |
| .build(); |
| } |
| |
| private LayoutElement getTextElementWithExitAnimation(String text, int iterations) { |
| return LayoutElement.newBuilder() |
| .setText( |
| textAnimVisibility( |
| AnimatedVisibility.newBuilder() |
| .setExitTransition(getFadeOutExitAnimation(iterations)), |
| text)) |
| .build(); |
| } |
| |
| private LayoutElement getMultipleTextElementWithExitAnimation( |
| List<String> texts, int iterations) { |
| Column.Builder column = Column.newBuilder(); |
| for (String text : texts) { |
| column.addContents( |
| LayoutElement.newBuilder() |
| .setText( |
| textAnimVisibility( |
| AnimatedVisibility.newBuilder() |
| .setExitTransition( |
| getFadeOutExitAnimation(iterations)), |
| text))); |
| } |
| |
| return LayoutElement.newBuilder().setColumn(column).build(); |
| } |
| |
| private ExitTransition.Builder getFadeOutExitAnimation(int iterations) { |
| return ExitTransition.newBuilder() |
| .setFadeOut( |
| FadeOutTransition.newBuilder() |
| .setAnimationSpec( |
| AnimationSpec.newBuilder() |
| .setRepeatable( |
| Repeatable.newBuilder() |
| .setIterations(iterations) |
| .build()))); |
| } |
| |
| private LayoutElement getTextElementWithSlideInAnimation(String text, int snapTo) { |
| return LayoutElement.newBuilder() |
| .setBox( |
| Box.newBuilder() |
| .setWidth(expand()) |
| .setHeight(expand()) |
| .addContents( |
| LayoutElement.newBuilder() |
| .setText( |
| textAnimVisibility( |
| AnimatedVisibility.newBuilder() |
| .setEnterTransition( |
| slideIn(snapTo)), |
| text)))) |
| .build(); |
| } |
| |
| @NonNull |
| private Text.Builder textAnimVisibility(AnimatedVisibility.Builder snapTo, String text) { |
| return Text.newBuilder() |
| .setModifiers(Modifiers.newBuilder().setContentUpdateAnimation(snapTo.build())) |
| .setText(string(text).build()); |
| } |
| |
| private EnterTransition.Builder slideIn(int snapTo) { |
| return EnterTransition.newBuilder() |
| .setSlideIn( |
| SlideInTransition.newBuilder() |
| .setDirection(SlideDirection.SLIDE_DIRECTION_LEFT_TO_RIGHT) |
| .setInitialSlideBound( |
| SlideBound.newBuilder() |
| .setParentBound( |
| SlideParentBound.newBuilder() |
| .setSnapTo( |
| SlideParentSnapOption |
| .forNumber(snapTo)) |
| .build()) |
| .build())); |
| } |
| |
| @NonNull |
| private static DpProp.Builder dp(float value) { |
| return DpProp.newBuilder().setValue(value); |
| } |
| |
| @NonNull |
| private static DimensionProto.SpProp sp(float value) { |
| return DimensionProto.SpProp.newBuilder().setValue(value).build(); |
| } |
| |
| @NonNull |
| private static ContainerDimension.Builder expand() { |
| return ContainerDimension.newBuilder() |
| .setExpandedDimension(ExpandedDimensionProp.getDefaultInstance()); |
| } |
| |
| @NonNull |
| private static StrokeCapProp.Builder strokeCapButt() { |
| return StrokeCapProp.newBuilder().setValue(LayoutElementProto.StrokeCap.STROKE_CAP_BUTT); |
| } |
| |
| @NonNull |
| private static DegreesProp.Builder degrees(int value) { |
| return DegreesProp.newBuilder().setValue(value); |
| } |
| |
| @NonNull |
| private static ExpandedAngularDimensionProp expandAngular(float value) { |
| return ExpandedAngularDimensionProp.newBuilder() |
| .setLayoutWeight(FloatProp.newBuilder().setValue(value).build()) |
| .build(); |
| } |
| |
| @NonNull |
| private static StringProp.Builder string(String value) { |
| return StringProp.newBuilder().setValue(value); |
| } |
| |
| @NonNull |
| private static StringProp.Builder dynamicString(String value) { |
| return StringProp.newBuilder() |
| .setValue(value) |
| .setDynamicValue( |
| DynamicString.newBuilder() |
| .setFixed(FixedString.newBuilder().setValue(value))); |
| } |
| |
| @NonNull |
| private static ImageDimension.Builder expandImage() { |
| return ImageDimension.newBuilder() |
| .setExpandedDimension(ExpandedDimensionProp.getDefaultInstance()); |
| } |
| |
| @NonNull |
| private static List<DimensionProto.SpProp> buildSizesList(int[] presetSizes) { |
| List<DimensionProto.SpProp> sizes = new ArrayList<>(3); |
| for (int s: presetSizes) { |
| sizes.add(sp(s)); |
| } |
| return sizes; |
| } |
| } |