Refactor ProtoLayoutViewInstance to return ListenableFuture
This will also fix minor bugs with closing/detaching view and
correctly wait for result in TileRenderer.
Also, this updates TileRenderer and its usage to be async.
Bug: 271076323
Test: N/A
Relnote: "ProtoLayoutViewInstance now returns ListenableFuture."
Change-Id: I2f2b9b0229dbf684445e4afad7a115de5892d869
diff --git a/wear/protolayout/protolayout-renderer/api/restricted_current.txt b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
index f78374c..97e844a 100644
--- a/wear/protolayout/protolayout-renderer/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
@@ -5,18 +5,27 @@
ctor public ProtoLayoutViewInstance(androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config);
method public void close() throws java.lang.Exception;
method @UiThread public void detach(android.view.ViewGroup);
- method @UiThread public void renderAndAttach(androidx.wear.protolayout.proto.LayoutElementProto.Layout, androidx.wear.protolayout.proto.ResourceProto.Resources, android.view.ViewGroup);
+ method @UiThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> renderAndAttach(androidx.wear.protolayout.proto.LayoutElementProto.Layout, androidx.wear.protolayout.proto.ResourceProto.Resources, android.view.ViewGroup);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class ProtoLayoutViewInstance.Config {
method public com.google.common.util.concurrent.ListeningExecutorService getBgExecutorService();
method public String getClickableIdExtra();
+ method public androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.LoadActionListener getLoadActionListener();
method public androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway? getSensorGateway();
method public androidx.wear.protolayout.expression.pipeline.StateStore? getStateStore();
method public android.content.Context getUiContext();
method public com.google.common.util.concurrent.ListeningExecutorService getUiExecutorService();
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class ProtoLayoutViewInstance.Config.Builder {
+ ctor public ProtoLayoutViewInstance.Config.Builder(android.content.Context, com.google.common.util.concurrent.ListeningExecutorService, com.google.common.util.concurrent.ListeningExecutorService, String);
+ method public androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config build();
+ method public androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config.Builder setLoadActionListener(androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.LoadActionListener);
+ method public androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config.Builder setSensorGateway(androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway);
+ method public androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config.Builder setStateStore(androidx.wear.protolayout.expression.pipeline.StateStore);
+ }
+
public static interface ProtoLayoutViewInstance.LoadActionListener {
method public void onClick(androidx.wear.protolayout.proto.StateProto.State);
}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
index a655bed..5b2971f 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
@@ -39,7 +39,7 @@
import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.ResourceProto;
-import androidx.wear.protolayout.proto.StateProto;
+import androidx.wear.protolayout.proto.StateProto.State;
import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
import androidx.wear.protolayout.renderer.ProtoLayoutVisibilityState;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
@@ -55,6 +55,7 @@
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.SettableFuture;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
@@ -64,7 +65,6 @@
* inflated on a background thread, the first time it is attached to the carousel. As much of the
* inflation as possible will be done in the background, with only the final attachment of the
* generated layout to a parent container done on the UI thread.
- *
*/
@RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
public class ProtoLayoutViewInstance implements AutoCloseable {
@@ -79,7 +79,7 @@
*
* @param nextState The state that the next layout should be in.
*/
- void onClick(@NonNull StateProto.State nextState);
+ void onClick(@NonNull State nextState);
}
private static final int DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS = 4;
@@ -144,7 +144,6 @@
/**
* This is used to provide a {@link ResourceResolvers} object to the {@link
* ProtoLayoutViewInstance} allowing it to query {@link ResourceProto.Resources} when needed.
- *
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public interface ResourceResolversProvider {
@@ -202,7 +201,6 @@
return Futures.immediateVoidFuture();
}
}
-
/** Result of a {@link #renderOrComputeMutations} call when a failure has happened. */
static final class FailedRenderResult implements RenderResult {
@Override
@@ -219,7 +217,6 @@
return Futures.immediateVoidFuture();
}
}
-
/**
* Result of a {@link #renderOrComputeMutations} call when the layout has been inflated into a
* new parent.
@@ -243,13 +240,16 @@
@NonNull ViewGroup parent,
@Nullable ViewGroup prevInflateParent,
boolean isReattaching) {
- checkNotNull(
- mNewInflateParentData.mInflateResult,
- TAG + " - inflated result was null, but inflating into new parent requested.")
- .updateDynamicDataPipeline(isReattaching);
+ InflateResult inflateResult =
+ checkNotNull(
+ mNewInflateParentData.mInflateResult,
+ TAG
+ + " - inflated result was null, but inflating into new parent"
+ + " requested.");
+ inflateResult.updateDynamicDataPipeline(isReattaching);
parent.removeAllViews();
parent.addView(
- checkNotNull(mNewInflateParentData.mInflateResult).inflateParent,
+ inflateResult.inflateParent,
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
return Futures.immediateVoidFuture();
}
@@ -344,20 +344,14 @@
return mUiContext;
}
- /**
- * Returns the Android Resources object for the renderer package.
- *
- */
+ /** Returns the Android Resources object for the renderer package. */
@RestrictTo(Scope.LIBRARY)
@NonNull
public Resources getRendererResources() {
return mRendererResources;
}
- /**
- * Returns provider for resource resolver.
- *
- */
+ /** Returns provider for resolving resources. */
@RestrictTo(Scope.LIBRARY)
@NonNull
public ResourceResolversProvider getResourceResolversProvider() {
@@ -382,12 +376,8 @@
return mStateStore;
}
- /**
- * Returns listener for load actions.
- *
- */
+ /** Returns listener for load actions. */
@NonNull
- @RestrictTo(Scope.LIBRARY)
public LoadActionListener getLoadActionListener() {
return mLoadActionListener;
}
@@ -410,56 +400,38 @@
return mClickableIdExtra;
}
- /**
- * Returns whether animations are enabled.
- *
- */
+ /** Returns whether animations are enabled. */
@RestrictTo(Scope.LIBRARY)
public boolean getAnimationEnabled() {
return mAnimationEnabled;
}
- /**
- * Returns how many animations can be concurrently run.
- *
- */
+ /** Returns how many animations can be concurrently run. */
@RestrictTo(Scope.LIBRARY)
public int getRunningAnimationsLimit() {
return mRunningAnimationsLimit;
}
- /**
- * Returns whether updates are enabled.
- *
- */
+ /** Returns whether updates are enabled. */
@RestrictTo(Scope.LIBRARY)
public boolean getUpdatesEnabled() {
return mUpdatesEnabled;
}
- /**
- * Returns whether adaptive updates are enabled.
- *
- */
+ /** Returns whether adaptive updates are enabled. */
@RestrictTo(Scope.LIBRARY)
public boolean getAdaptiveUpdateRatesEnabled() {
return mAdaptiveUpdateRatesEnabled;
}
- /**
- * Returns whether view is fully visible.
- *
- */
+ /** Returns whether view is fully visible. */
@RestrictTo(Scope.LIBRARY)
public boolean getIsViewFullyVisible() {
return mIsViewFullyVisible;
}
- /**
- * Builder for {@link Config}.
- *
- */
- @RestrictTo(Scope.LIBRARY)
+ /** Builder for {@link Config}. */
+ @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
public static final class Builder {
@NonNull private final Context mUiContext;
@Nullable private Resources mRendererResources;
@@ -497,10 +469,7 @@
this.mClickableIdExtra = clickableIdExtra;
}
- /**
- * Sets provider for resolving resources.
- *
- */
+ /** Sets provider for resolving resources. */
@NonNull
@RestrictTo(Scope.LIBRARY)
public Builder setResourceResolverProvider(
@@ -514,12 +483,10 @@
* retrieved with {@link
* android.content.pm.PackageManager#getResourcesForApplication(String)}. If not
* specified, this is retrieved from the Ui Context.
- *
*/
@NonNull
@RestrictTo(Scope.LIBRARY)
- public Builder setRendererResources(
- @NonNull Resources rendererResources) {
+ public Builder setRendererResources(@NonNull Resources rendererResources) {
this.mRendererResources = rendererResources;
return this;
}
@@ -544,10 +511,8 @@
/**
* Sets the listener for clicks that will cause contents to be reloaded. Defaults to
* no-op.
- *
*/
@NonNull
- @RestrictTo(Scope.LIBRARY)
public Builder setLoadActionListener(@NonNull LoadActionListener loadActionListener) {
this.mLoadActionListener = loadActionListener;
return this;
@@ -556,7 +521,6 @@
/**
* Sets whether animation are enabled. If disabled, none of the animation will be
* played.
- *
*/
@RestrictTo(Scope.LIBRARY)
@NonNull
@@ -565,10 +529,7 @@
return this;
}
- /**
- * Sets the limit to how much concurrently running animations are allowed.
- *
- */
+ /** Sets the limit to how much concurrently running animations are allowed. */
@RestrictTo(Scope.LIBRARY)
@NonNull
public Builder setRunningAnimationsLimit(int runningAnimationsLimit) {
@@ -576,10 +537,7 @@
return this;
}
- /**
- * Sets whether sending updates is enabled.
- *
- */
+ /** Sets whether sending updates is enabled. */
@RestrictTo(Scope.LIBRARY)
@NonNull
public Builder setUpdatesEnabled(boolean updatesEnabled) {
@@ -587,10 +545,7 @@
return this;
}
- /**
- * Sets whether adaptive updates rates is enabled.
- *
- */
+ /** Sets whether adaptive updates rates is enabled. */
@RestrictTo(Scope.LIBRARY)
@NonNull
public Builder setAdaptiveUpdateRatesEnabled(boolean adaptiveUpdateRatesEnabled) {
@@ -598,10 +553,7 @@
return this;
}
- /**
- * Sets whether the view is fully visible.
- *
- */
+ /** Sets whether the view is fully visible. */
@RestrictTo(Scope.LIBRARY)
@NonNull
public Builder setIsViewFullyVisible(boolean isViewFullyVisible) {
@@ -787,18 +739,29 @@
* <p>Note also that this method must be called from the UI thread;
*/
@UiThread
- @SuppressWarnings(
- "ReferenceEquality") // layout == prevLayout is intentional (and enough in this case)
- public void renderAndAttach(
+ @SuppressWarnings({
+ "ReferenceEquality",
+ "ExecutorTaskName"
+ }) // layout == prevLayout is intentional (and enough in this case)
+ @NonNull
+ public ListenableFuture<Void> renderAndAttach(
@NonNull Layout layout,
@NonNull ResourceProto.Resources resources,
@NonNull ViewGroup parent) {
- if (mAttachParent == parent && layout == mPrevLayout) {
- return;
+ if (mAttachParent == null) {
+ mAttachParent = parent;
+ mAttachParent.removeAllViews();
+ // Preload it with the previous layout if we have one.
+ if (mInflateParent != null) {
+ mAttachParent.addView(mInflateParent);
+ }
+ } else if (mAttachParent != parent) {
+ throw new IllegalStateException("ProtoLayoutViewInstance is already attached!");
}
- if (mAttachParent != null && mAttachParent != parent) {
- throw new IllegalStateException("ProtoLayoutViewInstance is already attached!");
+ if (layout == mPrevLayout && mInflateParent != null) {
+ // Nothing to do.
+ return Futures.immediateVoidFuture();
}
boolean isReattaching = false;
@@ -807,7 +770,7 @@
// There is an ongoing rendering operation. We'll skip this request as a missed
// frame.
Log.w(TAG, "Skipped layout update: previous layout update hasn't finished yet.");
- return;
+ return Futures.immediateCancelledFuture();
} else if (layout == mPrevLayout && mCanReattachWithoutRendering) {
isReattaching = true;
} else {
@@ -815,57 +778,69 @@
}
}
- @Nullable ViewGroup prevInflateParent = getOnlyChildViewGroup(parent);
+ @Nullable ViewGroup prevInflateParent = getOnlyChildViewGroup(mAttachParent);
@Nullable
RenderedMetadata prevRenderedMetadata =
prevInflateParent != null
? ProtoLayoutInflater.getRenderedMetadata(prevInflateParent)
: null;
- mAttachParent = parent;
if (mRenderFuture == null) {
mPrevLayout = layout;
mRenderFuture =
- mBgExecutorService.submit(
- () ->
- renderOrComputeMutations(
- layout, resources, prevRenderedMetadata));
+ mBgExecutorService.submit(() ->
+ renderOrComputeMutations(
+ layout, resources, prevRenderedMetadata));
mCanReattachWithoutRendering = false;
}
- if (!mRenderFuture.isDone()) {
+ SettableFuture<Void> result = SettableFuture.create();
+ if (!checkNotNull(mRenderFuture).isDone()) {
mRenderFuture.addListener(
() -> {
// Ensure that this inflater is attached to the same parent as when this
- // listener was created. If not, something has re-attached us in the
- // time it took for the inflater to execute.
+ // listener was created. If not, something has re-attached us in the time it
+ // took for the inflater to execute.
if (mAttachParent == parent) {
try {
- postInflate(
- parent,
- prevInflateParent,
- checkNotNull(mRenderFuture).get(),
- /* isReattaching= */ false,
- layout,
- resources);
- } catch (ExecutionException | InterruptedException e) {
+ result.setFuture(
+ postInflate(
+ parent,
+ prevInflateParent,
+ checkNotNull(mRenderFuture).get(),
+ /* isReattaching= */ false,
+ layout,
+ resources));
+ } catch (ExecutionException
+ | InterruptedException
+ | CancellationException e) {
Log.e(TAG, "Failed to render layout", e);
+ result.setException(e);
}
+ } else {
+ Log.w(
+ TAG,
+ "Layout is rendered, but inflater is no longer attached to the"
+ + " same parent. Cancelling inflation.");
+ result.cancel(/* mayInterruptIfRunning= */ false);
}
},
mUiExecutorService);
} else {
try {
- postInflate(
- parent,
- prevInflateParent,
- mRenderFuture.get(),
- isReattaching,
- layout,
- resources);
+ result.setFuture(
+ postInflate(
+ parent,
+ prevInflateParent,
+ mRenderFuture.get(),
+ isReattaching,
+ layout,
+ resources));
} catch (ExecutionException | InterruptedException | CancellationException e) {
Log.e(TAG, "Failed to render layout", e);
+ result.setException(e);
}
}
+ return result;
}
@Nullable
@@ -880,7 +855,9 @@
}
@UiThread
- private void postInflate(
+ @SuppressWarnings("ExecutorTaskName")
+ @NonNull
+ private ListenableFuture<Void> postInflate(
@NonNull ViewGroup parent,
@Nullable ViewGroup prevInflateParent,
@NonNull RenderResult renderResult,
@@ -892,40 +869,50 @@
if (renderResult instanceof InflatedIntoNewParentRenderResult) {
InflateParentData newInflateParentData =
((InflatedIntoNewParentRenderResult) renderResult).mNewInflateParentData;
- mInflateParent = checkNotNull(
- newInflateParentData.mInflateResult,
- TAG + " - inflated result was null, but inflating was requested.")
- .inflateParent;
+ mInflateParent =
+ checkNotNull(
+ newInflateParentData.mInflateResult,
+ TAG
+ + " - inflated result was null, but inflating was"
+ + " requested.")
+ .inflateParent;
}
ListenableFuture<Void> postInflateFuture =
renderResult.postInflate(parent, prevInflateParent, isReattaching);
+ SettableFuture<Void> result = SettableFuture.create();
if (!postInflateFuture.isDone()) {
postInflateFuture.addListener(
() -> {
try {
- postInflateFuture.get();
- } catch (ExecutionException | InterruptedException e) {
- handlePostInflateFailure(
- e, layout, resources, prevInflateParent, parent);
+ result.set(postInflateFuture.get());
+ } catch (ExecutionException
+ | InterruptedException
+ | CancellationException e) {
+ result.setFuture(
+ handlePostInflateFailure(
+ e, layout, resources, prevInflateParent, parent));
}
},
mUiExecutorService);
} else {
try {
postInflateFuture.get();
+ return Futures.immediateVoidFuture();
} catch (ExecutionException
| InterruptedException
| CancellationException
| ViewMutationException e) {
- handlePostInflateFailure(e, layout, resources, prevInflateParent, parent);
+ return handlePostInflateFailure(e, layout, resources, prevInflateParent, parent);
}
}
+ return result;
}
@UiThread
@SuppressWarnings("ReferenceEquality") // layout == prevLayout is intentional
- private void handlePostInflateFailure(
+ @NonNull
+ private ListenableFuture<Void> handlePostInflateFailure(
@NonNull Throwable error,
@NonNull Layout layout,
@NonNull ResourceProto.Resources resources,
@@ -940,11 +927,12 @@
// Clear rendering metadata and prevLayout to force a full reinflation.
ProtoLayoutInflater.clearRenderedMetadata(checkNotNull(prevInflateParent));
mPrevLayout = null;
- renderAndAttach(layout, resources, parent);
+ return renderAndAttach(layout, resources, parent);
}
} else {
Log.e(TAG, "postInflate failed.", error);
}
+ return Futures.immediateFailedFuture(error);
}
/**
@@ -958,10 +946,6 @@
throw new IllegalStateException("Layout is not attached to parent " + parent);
}
detachInternal();
-
- if (mInflateParent != null) {
- parent.removeView(mInflateParent);
- }
}
@UiThread
@@ -970,13 +954,23 @@
mRenderFuture.cancel(/* mayInterruptIfRunning= */ false);
}
setLayoutVisibility(ProtoLayoutVisibilityState.VISIBILITY_STATE_INVISIBLE);
+
+ ViewGroup inflateParent = mInflateParent;
+ if (inflateParent != null) {
+ ViewGroup parent = (ViewGroup) inflateParent.getParent();
+ if (mAttachParent != null && mAttachParent != parent) {
+ Log.w(TAG, "inflateParent was attached to the wrong parent.");
+ }
+ if (parent != null) {
+ parent.removeView(inflateParent);
+ }
+ }
mAttachParent = null;
}
/**
* Sets whether updates are enabled for this layout. When disabled, updates through the data
* pipeline (e.g. health updates) will be suppressed.
- *
*/
@RestrictTo(Scope.LIBRARY)
@UiThread
@@ -987,9 +981,7 @@
}
}
- /** Sets the visibility state for this layout.
- *
- */
+ /** Sets the visibility state for this layout. */
@RestrictTo(Scope.LIBRARY)
@UiThread
public void setLayoutVisibility(@ProtoLayoutVisibilityState int visibility) {
@@ -1013,6 +1005,8 @@
@Override
public void close() throws Exception {
detachInternal();
+ mRenderFuture = null;
+ mPrevLayout = null;
if (mDataPipeline != null) {
mDataPipeline.close();
}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index 625a0d7..55a4fc1 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -21,6 +21,9 @@
import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.ROOT_NODE_ID;
import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.getParentNodePosId;
+import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
+import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
+
import static java.lang.Math.max;
import static java.lang.Math.round;
@@ -79,7 +82,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
-import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.content.ContextCompat;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
@@ -175,8 +177,8 @@
import androidx.wear.widget.ArcLayout;
import androidx.wear.widget.CurvedTextView;
-import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -216,10 +218,9 @@
/**
* Default maximum raw byte size for a bitmap drawable.
*
- * @see <a href="https://cs.android.com/android/_/android/platform/frameworks/base
- * /+/d01036ee5893357db577c961119fb85825247f03:graphics/java/android/graphics/
- * RecordingCanvas.java;l=44;bpv=1;bpt=0;drc=00af5271dabd578397176eda0cd7a66c55fac59a"> The
- * framework enforced max size</a>
+ * @see <a
+ * href="https://cs.android.com/android/_/android/platform/frameworks/base/+/d01036ee5893357db577c961119fb85825247f03:graphics/java/android/graphics/RecordingCanvas.java;l=44;bpv=1;bpt=0;drc=00af5271dabd578397176eda0cd7a66c55fac59a">
+ * The framework enforced max size</a>
*/
private static final int DEFAULT_MAX_BITMAP_RAW_SIZE = 20 * 1024 * 1024;
@@ -241,8 +242,7 @@
// This is pretty badly named; TruncateAt specifies where to place the ellipsis (or whether to
// marquee). Disabling truncation with null actually disables the _ellipsis_, but text will
// still be truncated.
- @Nullable
- private static final TruncateAt TEXT_OVERFLOW_DEFAULT = null;
+ @Nullable private static final TruncateAt TEXT_OVERFLOW_DEFAULT = null;
private static final int TEXT_COLOR_DEFAULT = 0xFFFFFFFF;
private static final int TEXT_MAX_LINES_DEFAULT = 1;
@@ -253,14 +253,12 @@
.setWrappedDimension(WrappedDimensionProp.getDefaultInstance())
.build();
- @ArcLayout.AnchorType
- private static final int ARC_ANCHOR_DEFAULT = ArcLayout.ANCHOR_CENTER;
+ @ArcLayout.AnchorType private static final int ARC_ANCHOR_DEFAULT = ArcLayout.ANCHOR_CENTER;
// White
private static final int LINE_COLOR_DEFAULT = 0xFFFFFFFF;
- static final PendingLayoutParams NO_OP_PENDING_LAYOUT_PARAMS =
- layoutParams -> layoutParams;
+ static final PendingLayoutParams NO_OP_PENDING_LAYOUT_PARAMS = layoutParams -> layoutParams;
final Context mUiContext;
@@ -277,8 +275,7 @@
private final boolean mAllowLayoutChangingBindsWithoutDefault;
final String mClickableIdExtra;
- @Nullable
- final Executor mLoadActionExecutor;
+ @Nullable final Executor mLoadActionExecutor;
final LoadActionListener mLoadActionListener;
final boolean mAnimationEnabled;
@@ -319,7 +316,7 @@
* Update the DynamicDataPipeline with new nodes that were stored during the layout update.
*
* @param isReattaching if True, this layout is being reattached and will skip content
- * transition animations.
+ * transition animations.
*/
@UiThread
public void updateDynamicDataPipeline(boolean isReattaching) {
@@ -359,17 +356,15 @@
private int mNumMissingChildren;
/**
- * @param view The {@link View} that has been inflated.
- * @param layoutParams The {@link LayoutParams} that must be used when attaching the
- * inflated view to a parent.
- * @param childLayoutParams The {@link LayoutParams} that must be applied to children
- * carried over from a previous layout.
+ * @param view The {@link View} that has been inflated.
+ * @param layoutParams The {@link LayoutParams} that must be used when attaching the
+ * inflated view to a parent.
+ * @param childLayoutParams The {@link LayoutParams} that must be applied to children
+ * carried over from a previous layout.
* @param numMissingChildren Non-zero if {@code view} is a {@link ViewGroup} whose children
- * have not been added. This means that before using this view
- * in a layout, its children
- * must be copied from the {@link ViewGroup} that represents
- * the previous version of
- * this layout element.
+ * have not been added. This means that before using this view in a layout, its children
+ * must be copied from the {@link ViewGroup} that represents the previous version of
+ * this layout element.
*/
InflatedView(
View view,
@@ -441,8 +436,7 @@
* the renderer.
*/
private static final class ParentViewWrapper {
- @Nullable
- private final ViewGroup mParent;
+ @Nullable private final ViewGroup mParent;
private final ViewProperties mParentProps;
ParentViewWrapper(ViewGroup parent, LayoutParams parentLayoutParams) {
@@ -487,24 +481,15 @@
/** Config class for ProtoLayoutInflater */
public static final class Config {
- @NonNull
- private final Context mUiContext;
- @NonNull
- private final Layout mLayout;
- @NonNull
- private final ResourceResolvers mLayoutResourceResolvers;
- @Nullable
- private final Executor mLoadActionExecutor;
- @NonNull
- private final LoadActionListener mLoadActionListener;
- @NonNull
- private final Resources mRendererResources;
- @NonNull
- private final ProtoLayoutTheme mProtoLayoutTheme;
- @Nullable
- private final ProtoLayoutDynamicDataPipeline mDataPipeline;
- @NonNull
- private final String mClickableIdExtra;
+ @NonNull private final Context mUiContext;
+ @NonNull private final Layout mLayout;
+ @NonNull private final ResourceResolvers mLayoutResourceResolvers;
+ @Nullable private final Executor mLoadActionExecutor;
+ @NonNull private final LoadActionListener mLoadActionListener;
+ @NonNull private final Resources mRendererResources;
+ @NonNull private final ProtoLayoutTheme mProtoLayoutTheme;
+ @Nullable private final ProtoLayoutDynamicDataPipeline mDataPipeline;
+ @NonNull private final String mClickableIdExtra;
private final boolean mAnimationEnabled;
private final boolean mAllowLayoutChangingBindsWithoutDefault;
@@ -612,33 +597,23 @@
/** Builder for the Config class. */
public static final class Builder {
- @NonNull
- private final Context mUiContext;
- @NonNull
- private final Layout mLayout;
- @NonNull
- private final ResourceResolvers mLayoutResourceResolvers;
- @Nullable
- private Executor mLoadActionExecutor;
- @Nullable
- private LoadActionListener mLoadActionListener;
- @NonNull
- private Resources mRendererResources;
- @Nullable
- private ProtoLayoutTheme mProtoLayoutTheme;
- @Nullable
- private ProtoLayoutDynamicDataPipeline mDataPipeline = null;
+ @NonNull private final Context mUiContext;
+ @NonNull private final Layout mLayout;
+ @NonNull private final ResourceResolvers mLayoutResourceResolvers;
+ @Nullable private Executor mLoadActionExecutor;
+ @Nullable private LoadActionListener mLoadActionListener;
+ @NonNull private Resources mRendererResources;
+ @Nullable private ProtoLayoutTheme mProtoLayoutTheme;
+ @Nullable private ProtoLayoutDynamicDataPipeline mDataPipeline = null;
private boolean mAnimationEnabled = true;
private boolean mAllowLayoutChangingBindsWithoutDefault = false;
- @Nullable
- private String mClickableIdExtra;
+ @Nullable private String mClickableIdExtra;
/**
- * @param uiContext A {@link Context} suitable for interacting with UI
- * with.
- * @param layout The layout to be rendered.
+ * @param uiContext A {@link Context} suitable for interacting with UI with.
+ * @param layout The layout to be rendered.
* @param layoutResourceResolvers Resolvers for the resources used for rendering this
- * layout.
+ * layout.
*/
public Builder(
@NonNull Context uiContext,
@@ -748,8 +723,7 @@
}
if (mLoadActionListener == null) {
- mLoadActionListener = p -> {
- };
+ mLoadActionListener = p -> {};
}
if (mProtoLayoutTheme == null) {
this.mProtoLayoutTheme = ProtoLayoutThemeImpl.defaultTheme(mUiContext);
@@ -1182,7 +1156,8 @@
clickable
.getOnClick()
.getLoadAction(),
- clickable.getId()))));
+ clickable
+ .getId()))));
break;
case VALUE_NOT_SET:
break;
@@ -1879,9 +1854,10 @@
// bottom of FrameLayout#onMeasure).
//
// To work around this (without copying the whole of FrameLayout just to change a "1" to
- // "0"), we add a Space element in if there is one MATCH_PARENT child. This has a tiny cost
- // to the measure pass, and negligible cost to layout/draw (since it doesn't take part in
- // those passes).
+ // "0"),
+ // we add a Space element in if there is one MATCH_PARENT child. This has a tiny cost to the
+ // measure pass, and negligible cost to layout/draw (since it doesn't take part in those
+ // passes).
int numMatchParentChildren = 0;
for (int i = 0; i < frame.getChildCount(); i++) {
LayoutParams lp = frame.getChildAt(i).getLayoutParams();
@@ -1923,11 +1899,12 @@
spaceWrapperLayoutParams.height = LayoutParams.WRAP_CONTENT;
// Technically speaking, this logic isn't 100% accurate. In legacy size-changing mode
- // (before value_for_layout was introduced), apps may not set value_for_layout. That's
- // fine; the needsSizeWrapper checks will catch that. It's possible that one dimension
- // has value_for_layout set though, and the other relies on legacy size changing mode.
- // We don't deal with that case; if value_for_layout is present on one dimension, and
- // both are dynamic, then it must be set on both dimensions.
+ // (before
+ // value_for_layout was introduced), apps may not set value_for_layout. That's fine; the
+ // needsSizeWrapper checks will catch that. It's possible that one dimension has
+ // value_for_layout set though, and the other relies on legacy size changing mode. We
+ // don't deal with that case; if value_for_layout is present on one dimension, and both
+ // are dynamic, then it must be set on both dimensions.
if (spacer.getWidth().getLinearDimension().hasDynamicValue()) {
float widthForLayout = spacer.getWidth().getLinearDimension().getValueForLayout();
spaceWrapperLayoutParams.width = safeDpToPx(widthForLayout);
@@ -1940,13 +1917,13 @@
int gravity =
horizontalAlignmentToGravity(
- spacer.getWidth()
- .getLinearDimension()
- .getHorizontalAlignmentForLayout())
+ spacer.getWidth()
+ .getLinearDimension()
+ .getHorizontalAlignmentForLayout())
| verticalAlignmentToGravity(
- spacer.getHeight()
- .getLinearDimension()
- .getVerticalAlignmentForLayout());
+ spacer.getHeight()
+ .getLinearDimension()
+ .getVerticalAlignmentForLayout());
FrameLayout.LayoutParams frameLayoutLayoutParams =
new FrameLayout.LayoutParams(layoutParams);
frameLayoutLayoutParams.gravity = gravity;
@@ -2057,30 +2034,31 @@
lengthDegrees = max(0, angularLength.getDegrees().getValue());
break;
- case EXPANDED_ANGULAR_DIMENSION: {
- float weight =
- angularLength
- .getExpandedAngularDimension()
- .getLayoutWeight()
- .getValue();
- if (weight == 0 && thicknessPx == 0) {
- return null;
+ case EXPANDED_ANGULAR_DIMENSION:
+ {
+ float weight =
+ angularLength
+ .getExpandedAngularDimension()
+ .getLayoutWeight()
+ .getValue();
+ if (weight == 0 && thicknessPx == 0) {
+ return null;
+ }
+ layoutParams.setWeight(weight);
+
+ space.setThickness(thicknessPx);
+
+ View wrappedView =
+ applyModifiersToArcLayoutView(
+ space, spacer.getModifiers(), posId, pipelineMaker);
+ parentViewWrapper.maybeAddView(wrappedView, layoutParams);
+
+ return new InflatedView(
+ wrappedView,
+ parentViewWrapper
+ .getParentProperties()
+ .applyPendingChildLayoutParams(layoutParams));
}
- layoutParams.setWeight(weight);
-
- space.setThickness(thicknessPx);
-
- View wrappedView =
- applyModifiersToArcLayoutView(
- space, spacer.getModifiers(), posId, pipelineMaker);
- parentViewWrapper.maybeAddView(wrappedView, layoutParams);
-
- return new InflatedView(
- wrappedView,
- parentViewWrapper
- .getParentProperties()
- .applyPendingChildLayoutParams(layoutParams));
- }
case INNER_NOT_SET:
break;
@@ -2137,12 +2115,13 @@
text.getText(),
t -> {
// Underlines are applied using a Spannable here, rather than setting paint bits
- // (or using Paint#setTextUnderline). When multiple fonts are mixed on the same
- // line
+ // (or
+ // using Paint#setTextUnderline). When multiple fonts are mixed on the same line
// (especially when mixing anything with NotoSans-CJK), multiple underlines can
- // appear.
- // Using UnderlineSpan instead though causes the correct behaviour to happen
- // (only a single underline).
+ // appear. Using UnderlineSpan instead though causes the correct behaviour to
+ // happen
+ // (only a
+ // single underline).
SpannableStringBuilder ssb = new SpannableStringBuilder();
ssb.append(t);
@@ -2357,7 +2336,7 @@
// Both dimensions can't be ratios.
if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION
&& image.getHeight().getInnerCase()
- == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
+ == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
Log.w(TAG, "Both width and height were proportional for image " + protoResId);
return null;
}
@@ -2438,7 +2417,7 @@
if (trigger != null
&& trigger.getInnerCase()
- == Trigger.InnerCase.ON_CONDITION_MET_TRIGGER) {
+ == Trigger.InnerCase.ON_CONDITION_MET_TRIGGER) {
OnConditionMetTrigger conditionTrigger = trigger.getOnConditionMetTrigger();
pipelineMaker
.get()
@@ -2474,10 +2453,10 @@
try {
if (mLayoutResourceResolvers.hasPlaceholderDrawable(protoResId)) {
if (setImageDrawable(
- imageView,
- mLayoutResourceResolvers.getPlaceholderDrawableOrThrow(
- protoResId),
- protoResId)
+ imageView,
+ mLayoutResourceResolvers.getPlaceholderDrawableOrThrow(
+ protoResId),
+ protoResId)
== null) {
Log.w(TAG, "Failed to set the placeholder for " + protoResId);
}
@@ -2528,7 +2507,7 @@
* Set drawable to the image view.
*
* @return Returns the drawable if it is successfully retrieved from the drawable future and set
- * to the image view; otherwise returns null to indicate the failure of setting drawable.
+ * to the image view; otherwise returns null to indicate the failure of setting drawable.
*/
@Nullable
private static Drawable setImageDrawable(
@@ -2545,14 +2524,14 @@
* Set drawable to the image view.
*
* @return Returns the drawable if it is successfully set to the image view; otherwise returns
- * null to indicate the failure of setting drawable.
+ * null to indicate the failure of setting drawable.
*/
@Nullable
private static Drawable setImageDrawable(
ImageView imageView, Drawable drawable, String protoResId) {
if (drawable instanceof BitmapDrawable
&& ((BitmapDrawable) drawable).getBitmap().getByteCount()
- > DEFAULT_MAX_BITMAP_RAW_SIZE) {
+ > DEFAULT_MAX_BITMAP_RAW_SIZE) {
Log.w(TAG, "Ignoring image " + protoResId + " as it's too large.");
return null;
}
@@ -2622,16 +2601,17 @@
handleProp(length, lineView::setLineSweepAngleDegrees, posId, pipelineMaker);
break;
- case EXPANDED_ANGULAR_DIMENSION: {
- ExpandedAngularDimensionProp expandedAngularDimension =
- angularLength.getExpandedAngularDimension();
- layoutParams.setWeight(
- expandedAngularDimension.hasLayoutWeight()
- ? expandedAngularDimension.getLayoutWeight().getValue()
- : 1.0f);
- length = DegreesProp.getDefaultInstance();
- break;
- }
+ case EXPANDED_ANGULAR_DIMENSION:
+ {
+ ExpandedAngularDimensionProp expandedAngularDimension =
+ angularLength.getExpandedAngularDimension();
+ layoutParams.setWeight(
+ expandedAngularDimension.hasLayoutWeight()
+ ? expandedAngularDimension.getLayoutWeight().getValue()
+ : 1.0f);
+ length = DegreesProp.getDefaultInstance();
+ break;
+ }
default:
length = DegreesProp.getDefaultInstance();
@@ -3412,7 +3392,7 @@
return true;
case INNER_NOT_SET:
return false;
- default: // TODO(b/178359365): Remove default case
+ default: // TODO(b/276703002): Remove default case
return false;
}
}
@@ -3447,7 +3427,7 @@
return true;
case INNER_NOT_SET:
return false;
- default: // TODO(b/178359365): Remove default case
+ default: // TODO(b/276703002): Remove default case
return false;
}
}
@@ -3504,11 +3484,10 @@
*
* @param parent The view to attach the layout into.
* @return The {@link InflateResult} class containing the first child that was inflated,
- * animations to be played, and new nodes for the dynamic data pipeline. Callers should use
- * {@link InflateResult#startAnimations} and {@link InflateResult#updateDynamicDataPipeline}
- * to apply those changes using a UI Thread.
- * <p>This may be null if the proto is empty the top-level LayoutElement has no inner set,
- * or the top-level LayoutElement contains an unsupported inner type.
+ * animations to be played, and new nodes for the dynamic data pipeline. Callers should use
+ * {@link InflateResult#updateDynamicDataPipeline} to apply those changes using a UI Thread.
+ * <p>This may be null if the proto is empty the top-level LayoutElement has no inner set,
+ * or the top-level LayoutElement contains an unsupported inner type.
*/
@Nullable
public InflateResult inflate(@NonNull ViewGroup parent) {
@@ -3557,10 +3536,9 @@
* <p>Can be called from a background thread.
*
* @param prevRenderedMetadata The metadata for the previous rendering of this view, either
- * using {@code inflate} or {@code applyMutation}. This can be
- * retrieved by calling {@link
- * #getRenderedMetadata} on the previous layout view parent.
- * @param targetLayout The target layout that the mutation should result in.
+ * using {@code inflate} or {@code applyMutation}. This can be retrieved by calling {@link
+ * #getRenderedMetadata} on the previous layout view parent.
+ * @param targetLayout The target layout that the mutation should result in.
* @return The mutation that will produce the target layout.
*/
@Nullable
@@ -3672,19 +3650,19 @@
RenderedMetadata prevRenderedMetadata = getRenderedMetadata(parent);
if (prevRenderedMetadata != null
&& !ProtoLayoutDiffer.areNodesEquivalent(
- prevRenderedMetadata.getTreeFingerprint().getRoot(),
- groupMutation.mPreMutationRootNodeFingerprint)) {
+ prevRenderedMetadata.getTreeFingerprint().getRoot(),
+ groupMutation.mPreMutationRootNodeFingerprint)) {
// be considered unequal. Log.e(TAG, "View has changed. Skipping mutation."); return
// false;
}
if (groupMutation.isNoOp()) {
// Nothing to do.
- return Futures.immediateVoidFuture();
+ return immediateVoidFuture();
}
if (groupMutation.mPipelineMaker.isPresent()) {
- ResolvableFuture<Void> result = ResolvableFuture.create();
+ SettableFuture<Void> result = SettableFuture.create();
groupMutation
.mPipelineMaker
.get()
@@ -3703,9 +3681,9 @@
} else {
try {
applyMutationInternal(parent, groupMutation);
- return Futures.immediateVoidFuture();
+ return immediateVoidFuture();
} catch (ViewMutationException ex) {
- return Futures.immediateFailedFuture(ex);
+ return immediateFailedFuture(ex);
}
}
}
@@ -3763,7 +3741,9 @@
if (prevMetadataObject instanceof RenderedMetadata) {
return (RenderedMetadata) prevMetadataObject;
} else {
- Log.w(TAG, "Incompatible prevMetadataObject");
+ if (prevMetadataObject != null) {
+ Log.w(TAG, "Incompatible prevMetadataObject");
+ }
return null;
}
}
@@ -3900,10 +3880,11 @@
break;
}
mLoadActionExecutor.execute(
- () ->
- mLoadActionListener.onClick(
- buildState(
- action.getLoadAction(), mClickable.getId())));
+ () ->
+ mLoadActionListener.onClick(
+ buildState(
+ action.getLoadAction(),
+ mClickable.getId())));
break;
case VALUE_NOT_SET:
break;
@@ -3984,11 +3965,9 @@
}
@Override
- public void startScroll(int startX, int startY, int dx, int dy) {
- }
+ public void startScroll(int startX, int startY, int dx, int dy) {}
@Override
- public void startScroll(int startX, int startY, int dx, int dy, int duration) {
- }
+ public void startScroll(int startX, int startY, int dx, int dy, int duration) {}
}
}
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
index 93d21d6..ca3f94e 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
@@ -79,55 +79,129 @@
}
@Test
- public void adaptiveUpdateRatesDisabled_attach_reinflatesCompletely() {
+ public void adaptiveUpdateRatesDisabled_attach_reinflatesCompletely() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
- mInstanceUnderTest.renderAndAttach(
- layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+
List<View> layout1 = findViewsWithText(mRootContainer, TEXT1);
assertThat(layout1).hasSize(1);
- mInstanceUnderTest.renderAndAttach(
- layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
+ result =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result);
assertThat(findViewsWithText(mRootContainer, TEXT1)).containsNoneIn(layout1);
assertThat(findViewsWithText(mRootContainer, TEXT3)).isNotEmpty();
}
@Test
- public void adaptiveUpdateRatesEnabled_attach_appliesDiffOnly() {
+ public void adaptiveUpdateRatesEnabled_attach_appliesDiffOnly() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- mInstanceUnderTest.renderAndAttach(
- layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+
List<View> layout1 = findViewsWithText(mRootContainer, TEXT1);
assertThat(layout1).hasSize(1);
- mInstanceUnderTest.renderAndAttach(
- layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
+ result =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result);
// Assert that only the modified text is reinflated.
assertThat(findViewsWithText(mRootContainer, TEXT1)).containsExactlyElementsIn(layout1);
assertThat(findViewsWithText(mRootContainer, TEXT3)).isNotEmpty();
}
@Test
- public void adaptiveUpdateRatesEnabled_attach_withDynamicValue_appliesDiffOnly() {
+ public void reattach_usesCachedLayoutForDiffUpdate() throws Exception {
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+ List<View> layout1 = findViewsWithText(mRootContainer, TEXT1);
+ assertThat(layout1).hasSize(1);
+
+ mInstanceUnderTest.detach(mRootContainer);
+
+ result =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+ // Assert that only the modified text is reinflated.
+ assertThat(findViewsWithText(mRootContainer, TEXT1)).containsExactlyElementsIn(layout1);
+ assertThat(findViewsWithText(mRootContainer, TEXT3)).isNotEmpty();
+ }
+
+ @Test
+ public void adaptiveUpdateRatesEnabled_applyingDiffToDetachedContainer_returnsNothing()
+ throws Exception {
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+
+ // First one that does the full layout update.
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+
+ List<View> layout1 = findViewsWithText(mRootContainer, TEXT1);
+ assertThat(layout1).hasSize(1);
+
+ // Second one that applies mutation only.
+ result =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
+ // Detach so it can't apply update.
+ mInstanceUnderTest.detach(mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(result.isCancelled()).isTrue();
+ assertThat(mRootContainer.getChildCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void adaptiveUpdateRatesEnabled_attach_withDynamicValue_appliesDiffOnly()
+ throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
// Render the first layout.
Layout layout1 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT2)));
- mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+
List<View> textView1 = findViewsWithText(mRootContainer, TEXT1);
assertThat(textView1).hasSize(1);
assertThat(findViewsWithText(mRootContainer, TEXT2)).hasSize(1);
Layout layout2 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT3)));
- mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ // Make sure future is computing result.
+ assertThat(result.isDone()).isFalse();
shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
// Assert that only the modified text is reinflated.
assertThat(findViewsWithText(mRootContainer, TEXT1)).containsExactlyElementsIn(textView1);
assertThat(findViewsWithText(mRootContainer, TEXT2)).isEmpty();
@@ -135,28 +209,34 @@
}
@Test
- public void adaptiveUpdateRatesEnabled_ongoingRendering_skipsNewLayout() {
+ public void adaptiveUpdateRatesEnabled_ongoingRendering_skipsNewLayout() throws Exception {
FrameLayout container = new FrameLayout(mApplicationContext);
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- mInstanceUnderTest.renderAndAttach(
- layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container);
+ ListenableFuture<Void> result1 =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container);
+ assertThat(result1.isDone()).isFalse();
- mInstanceUnderTest.renderAndAttach(
- layout(column(text(TEXT1), text(TEXT3))), RESOURCES, container);
+ ListenableFuture<Void> result2 =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT3))), RESOURCES, container);
shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result1);
+ assertThat(result2.isCancelled()).isTrue();
// Assert that only the modified text is reinflated.
assertThat(findViewsWithText(container, TEXT2)).hasSize(1);
assertThat(findViewsWithText(container, TEXT3)).isEmpty();
}
@Test
- public void attachingToANewContainer_withoutDetach_throws() {
+ public void attachingToANewContainer_withoutDetach_throws() throws Exception {
FrameLayout container1 = new FrameLayout(mApplicationContext);
FrameLayout container2 = new FrameLayout(mApplicationContext);
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- mInstanceUnderTest.renderAndAttach(
- layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container1);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(
+ layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container1);
assertThrows(
IllegalStateException.class,
@@ -164,64 +244,96 @@
mInstanceUnderTest.renderAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container2));
shadowOf(Looper.getMainLooper()).idle();
+
+ // Check the result from first attach.
+ assertNoException(result);
}
@Test
- public void renderingToADetachedContainer_isNoOp() {
+ public void renderingToADetachedContainer_isNoOp() throws Exception {
FrameLayout container1 = new FrameLayout(mApplicationContext);
FrameLayout container2 = new FrameLayout(mApplicationContext);
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container1);
+ ListenableFuture<Void> result1 =
+ mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container1);
mInstanceUnderTest.detach(container1);
- mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container2);
+ ListenableFuture<Void> result2 =
+ mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container2);
shadowOf(Looper.getMainLooper()).idle();
+ assertThat(result1.isCancelled()).isTrue();
+ assertThat(result2.isDone()).isTrue();
+ assertNoException(result2);
assertThat(findViewsWithText(container1, TEXT1)).isEmpty();
assertThat(findViewsWithText(container2, TEXT1)).hasSize(1);
}
@Test
- public void adaptiveUpdateRatesDisabled_sameLayoutReference_subsequentRendering_isNoOp() {
+ public void adaptiveUpdateRatesDisabled_sameLayoutReference_subsequentRendering_isNoOp()
+ throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ assertNoException(result);
+ result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+
+ shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result);
assertThat(shadowOf(Looper.getMainLooper()).isIdle()).isTrue();
}
@Test
- public void adaptiveUpdateRatesEnabled_afterNoChange_reattach_sameLayoutReference_rerenders() {
+ public void adaptiveUpdateRatesEnabled_afterNoChange_reattach_sameLayoutReference_isNoOp()
+ throws Exception {
Layout layout1 = layout(text(TEXT1));
Layout layout2 = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+
// Make sure we have an UnchangedRenderResult
- mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+ assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
+
mInstanceUnderTest.detach(mRootContainer);
+ assertThat(findViewsWithText(mRootContainer, TEXT1)).isEmpty();
shadowOf(Looper.getMainLooper()).idle();
- mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
- assertThat(shadowOf(Looper.getMainLooper()).isIdle()).isFalse();
+ assertThat(result.isDone()).isTrue();
+ assertNoException(result);
+ assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
}
@Test
- public void layoutViewIsCachedWhenDetached() {
+ public void fullInflationResultCanBeReused() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
Layout layout = layout(text(TEXT1));
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
+
+ assertNoException(result);
+
ListenableFuture<?> renderFuture = mInstanceUnderTest.mRenderFuture;
mInstanceUnderTest.detach(mRootContainer);
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result);
assertThat(mInstanceUnderTest.mRenderFuture).isSameInstanceAs(renderFuture);
}
@@ -230,14 +342,52 @@
throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result);
+ List<View> textViews1 = findViewsWithText(mRootContainer, TEXT1);
+ assertThat(textViews1).hasSize(1);
+
+ mInstanceUnderTest.close();
+ result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+
+ assertThat(shadowOf(Looper.getMainLooper()).isIdle()).isFalse();
+ shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result);
+ List<View> textViews2 = findViewsWithText(mRootContainer, TEXT1);
+ assertThat(textViews2).hasSize(1);
+ assertThat(textViews1.get(0)).isNotSameInstanceAs(textViews2.get(0));
+ }
+
+ @Test
+ public void detach_clearsHostView() throws Exception {
+ Layout layout = layout(text(TEXT1));
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result);
+ assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
+
+ mInstanceUnderTest.detach(mRootContainer);
+
+ assertThat(mRootContainer.getChildCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void close_clearsHostView() throws Exception {
+ Layout layout = layout(text(TEXT1));
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+ ListenableFuture<Void> result =
+ mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertNoException(result);
+ assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
mInstanceUnderTest.close();
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
-
- assertThat(shadowOf(Looper.getMainLooper()).isIdle()).isFalse();
+ assertThat(mRootContainer.getChildCount()).isEqualTo(0);
}
private void setupInstance(boolean adaptiveUpdateRatesEnabled) {
@@ -248,13 +398,12 @@
ProtoLayoutViewInstance.Config config =
new Config.Builder(
- mApplicationContext,
- listeningExecutorService,
- listeningExecutorService,
- /* clickableIdExtra= */ "CLICKABLE_ID_EXTRA")
+ mApplicationContext,
+ listeningExecutorService,
+ listeningExecutorService,
+ /* clickableIdExtra= */ "CLICKABLE_ID_EXTRA")
.setStateStore(new StateStore(ImmutableMap.of()))
- .setLoadActionListener(nextState -> {
- })
+ .setLoadActionListener(nextState -> {})
.setAnimationEnabled(true)
.setRunningAnimationsLimit(Integer.MAX_VALUE)
.setUpdatesEnabled(true)
@@ -270,6 +419,11 @@
return views;
}
+ private static void assertNoException(ListenableFuture<Void> result) throws Exception {
+ // Assert that result hasn't thrown exception.
+ result.get();
+ }
+
static class FakeExecutorService extends AbstractExecutorService {
private final Handler mHandler;
@@ -279,8 +433,7 @@
}
@Override
- public void shutdown() {
- }
+ public void shutdown() {}
@Override
public List<Runnable> shutdownNow() {
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 5a20188..64ea708 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -21,6 +21,7 @@
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;
@@ -215,8 +216,7 @@
private static final int SCREEN_WIDTH = 400;
private static final int SCREEN_HEIGHT = 400;
- @Rule
- public final Expect expect = Expect.create();
+ @Rule public final Expect expect = Expect.create();
private final StateStore mStateStore = new StateStore(ImmutableMap.of());
private ProtoLayoutDynamicDataPipeline mDataPipeline;
@@ -279,8 +279,8 @@
public void inflate_textView_withObsoleteSemanticsContentDescription() {
String textContents = "Hello World";
String textDescription = "Hello World Text Element";
- Semantics.Builder semantics =
- Semantics.newBuilder().setObsoleteContentDescription(textDescription);
+ Semantics semantics =
+ Semantics.newBuilder().setObsoleteContentDescription(textDescription).build();
LayoutElement root =
LayoutElement.newBuilder()
.setText(
@@ -318,10 +318,11 @@
String staticDescription = "StaticDescription";
StringProp descriptionProp = string(staticDescription).build();
- Semantics.Builder semantics =
+ Semantics semantics =
Semantics.newBuilder()
.setObsoleteContentDescription("ObsoleteContentDescription")
- .setContentDescription(descriptionProp);
+ .setContentDescription(descriptionProp)
+ .build();
LayoutElement root =
LayoutElement.newBuilder()
.setText(
@@ -381,10 +382,11 @@
.build())
.build();
- Semantics.Builder semantics =
+ Semantics semantics =
Semantics.newBuilder()
.setStateDescription(stateDescriptionProp)
- .setContentDescription(contentDescriptionProp);
+ .setContentDescription(contentDescriptionProp)
+ .build();
LayoutElement root =
LayoutElement.newBuilder()
.setText(
@@ -453,8 +455,10 @@
@Test
public void inflate_box_withIllegalSize() {
- LayoutElement.Builder textElement =
- LayoutElement.newBuilder().setText(Text.newBuilder().setText(string("foo")));
+ LayoutElement textElement =
+ LayoutElement.newBuilder()
+ .setText(Text.newBuilder().setText(string("foo")))
+ .build();
LayoutElement root =
LayoutElement.newBuilder()
.setBox(
@@ -466,10 +470,10 @@
.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.
+ // "expand"
+ // element in a "wrap" element is an
+ // undefined state, so the outer box
+ // should not be displayed.
Box.newBuilder()
.setWidth(expand())
.addContents(textElement))))
@@ -484,10 +488,11 @@
@Test
public void inflate_box_withSemanticsModifier() {
String textDescription = "this is a button";
- Semantics.Builder semantics =
+ Semantics semantics =
Semantics.newBuilder()
.setContentDescription(string(textDescription))
- .setRole(SemanticsRole.SEMANTICS_ROLE_BUTTON);
+ .setRole(SemanticsRole.SEMANTICS_ROLE_BUTTON)
+ .build();
String text = "some button";
LayoutElement root =
LayoutElement.newBuilder()
@@ -518,11 +523,12 @@
public void inflate_box_withSemanticsStateDescription() {
String textDescription = "this is a switch";
String offState = "off";
- Semantics.Builder semantics =
+ Semantics semantics =
Semantics.newBuilder()
.setContentDescription(string(textDescription))
.setStateDescription(string(offState))
- .setRole(SemanticsRole.SEMANTICS_ROLE_SWITCH);
+ .setRole(SemanticsRole.SEMANTICS_ROLE_SWITCH)
+ .build();
String text = "a switch";
LayoutElement root =
LayoutElement.newBuilder()
@@ -759,7 +765,7 @@
TextView tv = (TextView) rootLayout.getChildAt(0);
// The clickable view must have the same tag as the corresponding layout clickable.
- expect.that(tv.getTag(R.id.clickable_id_tag)).isEqualTo("foo");
+ expect.that(tv.getTag(clickable_id_tag)).isEqualTo("foo");
// Ensure that the text still went through properly.
expect.that(tv.getText().toString()).isEqualTo(textContents);
@@ -908,9 +914,9 @@
State.Builder receivedState = State.newBuilder();
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(
- fingerprintedLayout(root), resourceResolvers())
- .setLoadActionListener(receivedState::mergeFrom))
+ newRendererConfigBuilder(
+ fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(receivedState::mergeFrom))
.inflate();
// Should be just a text view as the root.
@@ -920,7 +926,7 @@
TextView tv = (TextView) rootLayout.getChildAt(0);
// The clickable view must have the same tag as the corresponding layout clickable.
- expect.that(tv.getTag(R.id.clickable_id_tag)).isEqualTo("foo");
+ expect.that(tv.getTag(clickable_id_tag)).isEqualTo("foo");
// Ensure that the text still went through properly.
expect.that(tv.getText().toString()).isEqualTo(textContents);
@@ -1092,11 +1098,9 @@
Arc.newBuilder()
.setAnchorAngle(degrees(0).build())
.addContents(
- ArcLayoutElement.newBuilder()
- .setSpacer(
- ArcSpacer.newBuilder()
- .setLength(degrees(90))
- .setThickness(dp(20)))))
+ arcLayoutElement(
+ ArcSpacer.newBuilder()
+ .setLength(degrees(90)))))
.build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
@@ -1125,12 +1129,9 @@
.setAnchorAngle(degrees(0).build())
.setMaxAngle(DegreesProp.newBuilder().setValue(90f).build())
.addContents(
- ArcLayoutElement.newBuilder()
- .setSpacer(
- ArcSpacer.newBuilder()
- .setAngularLength(
- spacerLength)
- .setThickness(dp(20))))
+ arcLayoutElement(
+ ArcSpacer.newBuilder()
+ .setAngularLength(spacerLength)))
.addContents(
ArcLayoutElement.newBuilder()
.setLine(
@@ -1155,6 +1156,11 @@
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";
@@ -1720,7 +1726,7 @@
@Test
public void inflate_spannable_onClickCanFire() {
- StringProp.Builder text = string("Hello" + " World");
+ StringProp.Builder text = string("Hello World");
LayoutElement root =
LayoutElement.newBuilder()
.setSpannable(
@@ -1737,11 +1743,11 @@
List<Boolean> hasFiredList = new ArrayList<>();
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(
- fingerprintedLayout(root), resourceResolvers())
- .setLoadActionListener(p -> hasFiredList.add(true))
- .setProtoLayoutTheme(
- loadTheme(R.style.MyProtoLayoutSansSerifTheme)))
+ newRendererConfigBuilder(
+ fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(p -> hasFiredList.add(true))
+ .setProtoLayoutTheme(
+ loadTheme(R.style.MyProtoLayoutSansSerifTheme)))
.inflate();
TextView tv = (TextView) rootLayout.getChildAt(0);
@@ -1951,9 +1957,11 @@
.setImage(
Image.newBuilder()
.setWidth(
- linImageDim(dp(24)))
+ linImageDim(
+ dp(24f)))
.setHeight(
- linImageDim(dp(24)))
+ linImageDim(
+ dp(24f)))
.setResourceId(
string("android"))))
.addContents(LayoutElement.newBuilder().setImage(image)))
@@ -1961,9 +1969,9 @@
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(fingerprintedLayout(root))
- .setProtoLayoutTheme(
- loadTheme(R.style.MyProtoLayoutSansSerifTheme)))
+ newRendererConfigBuilder(fingerprintedLayout(root))
+ .setProtoLayoutTheme(
+ loadTheme(R.style.MyProtoLayoutSansSerifTheme)))
.inflate();
// Outer box should be 24dp
@@ -1991,76 +1999,6 @@
expect.that(image2.getHeight()).isEqualTo(24);
}
- @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(24)))
- .setHeight(
- linImageDim(dp(24)))
- .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);
- }
-
@NonNull
private static ImageDimension.Builder linImageDim(DpProp.Builder builderForValue) {
return ImageDimension.newBuilder().setLinearDimension(builderForValue);
@@ -2073,6 +2011,78 @@
}
@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();
@@ -2203,6 +2213,19 @@
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_noLegacyMode_usesArcLength() {
DynamicFloat arcLength =
@@ -2228,8 +2251,8 @@
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(fingerprintedLayout(root))
- .setAllowLayoutChangingBindsWithoutDefault(true))
+ newRendererConfigBuilder(fingerprintedLayout(root))
+ .setAllowLayoutChangingBindsWithoutDefault(true))
.inflate();
shadowOf(Looper.getMainLooper()).idle();
@@ -2240,19 +2263,6 @@
assertThat(line.getLineSweepAngleDegrees()).isEqualTo(45f);
}
- @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_text_dynamicColor_updatesColor() {
mStateStore.setStateEntryValuesProto(
@@ -2341,10 +2351,10 @@
public void inflateThenMutate_withChangeToText_causesUpdate() {
Layout layout1 =
layout(
- column(// 1
+ column( // 1
text("Hello"), // 1.1
text("World") // 1.2
- ));
+ ));
// Check that we have the initial layout correctly rendered
Renderer renderer = renderer(layout1);
@@ -2359,10 +2369,10 @@
// Produce a new layout with only one Text element changed.
Layout layout2 =
layout(
- column(// 1
+ column( // 1
text("Hello"), // 1.1
text("Mars") // 1.2
- ));
+ ));
// Compute the mutation
ViewGroupMutation mutation =
@@ -2390,17 +2400,17 @@
public void inflateThenMutate_withChangeToImageAndText_causesUpdate() {
Layout layout1 =
layout(
- column(// 1
+ column( // 1
text("Hello"), // 1.1
- row(// 1.2
- image(// 1.2.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);
@@ -2424,17 +2434,17 @@
// Produce a new layout with one Text element and one Image changed.
Layout layout2 =
layout(
- column(// 1
+ column( // 1
text("Hello"), // 1.1
- row(// 1.2
- image(// 1.2.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 =
@@ -2473,11 +2483,11 @@
public void inflateThenMutate_withChangeToProps_causesUpdate() {
Layout layout1 =
layout(
- column(// 1
+ 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);
@@ -2493,11 +2503,11 @@
// Produce a new layout with only the props of the container changed.
Layout layout2 =
layout(
- column(// 1
+ column( // 1
props -> props.widthDp = 123,
text("Hello"), // 1.1
text("World") // 1.2
- ));
+ ));
// Compute the mutation
ViewGroupMutation mutation =
@@ -2525,11 +2535,11 @@
public void inflateThenMutate_withChangeToPropsAndOneChild_doesntUpdateAllChildren() {
Layout layout1 =
layout(
- column(// 1
+ 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);
@@ -2545,11 +2555,11 @@
// Produce a new layout with the props of the container and one child changed.
Layout layout2 =
layout(
- column(// 1
+ column( // 1
props -> props.widthDp = 123,
text("Hello"), // 1.1
text("MARS") // 1.2
- ));
+ ));
// Compute the mutation
ViewGroupMutation mutation =
@@ -2576,10 +2586,10 @@
public void inflateThenMutate_withNoChange_producesNoOpMutation() {
Layout layout =
layout(
- column(// 1
+ column( // 1
text("Hello"), // 1.1
text("World") // 1.2
- ));
+ ));
// Check that we have the initial layout correctly rendered
Renderer renderer = renderer(layout);
@@ -2617,10 +2627,10 @@
public void inflateThenMutate_withDifferentNumberOfChildren_causesUpdate() {
Layout layout1 =
layout(
- column(// 1
+ column( // 1
text("Hello"), // 1.1
text("World") // 1.2
- ));
+ ));
// Check that we have the initial layout correctly rendered
Renderer renderer = renderer(layout1);
@@ -2635,12 +2645,12 @@
Layout layout2 =
layout(
- column(// 1
+ column( // 1
text("Hello"), // 1.1
text("World"), // 1.2
text("and"), // 1.3
text("Mars") // 1.4
- ));
+ ));
// Compute the mutation
ViewGroupMutation mutation =
@@ -2668,10 +2678,10 @@
public void inflateThenMutate_withDynamicText_dataPipelineIsUpdated() {
Layout layout1 =
layout(
- column(// 1
+ column( // 1
dynamicFixedText("Hello"), // 1.1
dynamicFixedText("World") // 1.2
- ));
+ ));
// Check that we have the initial layout correctly rendered
Renderer renderer = renderer(layout1);
@@ -2691,10 +2701,10 @@
Layout layout2 =
layout(
- column(// 1
+ column( // 1
dynamicFixedText("Hello"), // 1.1
dynamicFixedText("Mars") // 1.2
- ));
+ ));
// Compute the mutation
ViewGroupMutation mutation =
@@ -2726,11 +2736,11 @@
public void inflateThenMutate_withSelfMutation_dataPipelineIsPreserved() {
Layout layout1 =
layout(
- column(// 1
+ 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);
@@ -2749,11 +2759,11 @@
// Produce a new layout with the column width changed.
Layout layout2 =
layout(
- column(// 1
+ column( // 1
props -> props.widthDp = 20,
dynamicFixedText("Hello"), // 1.1
dynamicFixedText("World") // 1.2
- ));
+ ));
// Compute the mutation
ViewGroupMutation mutation =
@@ -2786,10 +2796,10 @@
public void reInflate_dataPipelineIsReset() {
Layout layout =
layout(
- column(// 1
+ column( // 1
dynamicFixedText("Hello"), // 1.1
dynamicFixedText("World") // 1.2
- ));
+ ));
// Check that we have the initial layout correctly rendered
Renderer renderer = renderer(layout);
@@ -2828,10 +2838,10 @@
public void inflateArcThenMutate_withChangeToText_causesUpdate() {
Layout layout1 =
layout(
- arc(// 1
+ arc( // 1
arcText("Hello"), // 1.1
arcText("World") // 1.2
- ));
+ ));
// Check that we have the initial layout correctly rendered
Renderer renderer = renderer(layout1);
@@ -2847,10 +2857,10 @@
// Produce a new layout with only one Text element changed.
Layout layout2 =
layout(
- arc(// 1
+ arc( // 1
arcText("Hello"), // 1.1
arcText("Mars") // 1.2
- ));
+ ));
// Compute the mutation
ViewGroupMutation mutation =
@@ -2880,10 +2890,10 @@
public void inflateArcThenMutate_withChangeToProps_causesUpdate() throws Exception {
Layout layout1 =
layout(
- arc(// 1
+ arc( // 1
arcText("Hello"), // 1.1
arcText("World") // 1.2
- ));
+ ));
// Check the premutation layout
Renderer renderer = renderer(layout1);
@@ -2899,11 +2909,11 @@
Layout layout2 =
layout(
- arc(// 1
+ arc( // 1
props -> props.anchorAngleDegrees = 35,
arcText("Hello"), // 1.1
arcText("World") // 1.2
- ));
+ ));
// Compute the mutation
ViewGroupMutation mutation =
@@ -2932,22 +2942,22 @@
public void viewChangesWhileComputingMutation_applyMutationFails() throws Exception {
Layout layout1 =
layout(
- arc(// 1
+ arc( // 1
arcText("Hello"), // 1.1
arcText("World") // 1.2
- ));
+ ));
Layout layout2 =
layout(
- arc(// 1
+ arc( // 1
props -> props.anchorAngleDegrees = 35,
arcText("Hello"), // 1.1
arcText("World") // 1.2
- ));
+ ));
Layout layout3 =
layout(
- arc(// 1
+ arc( // 1
arcText("Hello") // 1.1
- ));
+ ));
// Check the premutation layout
Renderer renderer = renderer(layout1);
ViewGroup inflatedViewParent1 = renderer.inflate();
@@ -2969,10 +2979,10 @@
public void inflateArcThenMutate_withDifferentNumberOfChildren_causesUpdate() {
Layout layout1 =
layout(
- arc(// 1
+ arc( // 1
arcText("Hello"), // 1.1
arcText("World") // 1.2
- ));
+ ));
// Check the premutation layout
Renderer renderer = renderer(layout1);
@@ -2987,12 +2997,12 @@
Layout layout2 =
layout(
- arc(// 1
+ arc( // 1
arcText("Hello"), // 1.1
arcText("World"), // 1.2
arcText("and"), // 1.3
arcText("Mars") // 1.4
- ));
+ ));
// Compute the mutation
ViewGroupMutation mutation =
@@ -3021,10 +3031,10 @@
public void inflateAndMutateTwice_causesTwoUpdates() throws Exception {
Layout layout1 =
layout(
- column(// 1
+ column( // 1
text("Hello"), // 1.1
text("World") // 1.2
- ));
+ ));
// Do the initial inflation.
Renderer renderer = renderer(layout1);
@@ -3037,10 +3047,10 @@
Layout layout2 =
layout(
- column(// 1
+ column( // 1
text("Goodbye"), // 1.1
text("World") // 1.2
- ));
+ ));
// Apply first mutation
ViewGroupMutation mutation1 =
@@ -3058,10 +3068,10 @@
Layout layout3 =
layout(
- column(// 1
+ column( // 1
text("Hello"), // 1.1
text("Mars") // 1.2
- ));
+ ));
// Apply second mutation
ViewGroupMutation mutation2 =
@@ -3082,10 +3092,10 @@
public void inflateArcThenMutate_withNoChange_producesNoOpMutation() {
Layout layout =
layout(
- arc(// 1
+ arc( // 1
arcText("Hello"), // 1.1
arcText("World") // 1.2
- ));
+ ));
// Check that we have the initial layout correctly rendered
Renderer renderer = renderer(layout);
@@ -3114,7 +3124,7 @@
public void boxWithChild_childChanges_appliesGravityToUpdatedChild() throws Exception {
Layout layout1 =
layout(
- box(// 1
+ box( // 1
boxProps -> {
boxProps.horizontalAlignment =
HorizontalAlignment.HORIZONTAL_ALIGN_CENTER;
@@ -3122,12 +3132,12 @@
VerticalAlignment.VERTICAL_ALIGN_CENTER;
},
text("Hello") // 1.1
- ));
+ ));
Renderer renderer = renderer(layout1);
ViewGroup inflatedViewParent = renderer.inflate();
Layout layout2 =
layout(
- box(// 1
+ box( // 1
boxProps -> {
boxProps.horizontalAlignment =
HorizontalAlignment.HORIZONTAL_ALIGN_CENTER;
@@ -3135,7 +3145,7 @@
VerticalAlignment.VERTICAL_ALIGN_CENTER;
},
text("World") // 1.1
- ));
+ ));
ViewGroupMutation mutation =
renderer.mRenderer.computeMutation(
@@ -3156,7 +3166,7 @@
public void boxWithChild_boxChanges_appliesNewGravityToChild() throws Exception {
Layout layout1 =
layout(
- box(// 1
+ box( // 1
boxProps -> {
boxProps.horizontalAlignment =
HorizontalAlignment.HORIZONTAL_ALIGN_CENTER;
@@ -3164,12 +3174,12 @@
VerticalAlignment.VERTICAL_ALIGN_CENTER;
},
text("Hello") // 1.1
- ));
+ ));
Renderer renderer = renderer(layout1);
ViewGroup inflatedViewParent = renderer.inflate();
Layout layout2 =
layout(
- box(// 1
+ box( // 1
boxProps -> {
// A different set of alignments.
boxProps.horizontalAlignment =
@@ -3178,7 +3188,7 @@
VerticalAlignment.VERTICAL_ALIGN_BOTTOM;
},
text("Hello") // 1.1
- ));
+ ));
ViewGroupMutation mutation =
renderer.mRenderer.computeMutation(
@@ -3199,7 +3209,7 @@
public void boxWithChild_bothChange_appliesNewGravityToUpdatedChild() throws Exception {
Layout layout1 =
layout(
- box(// 1
+ box( // 1
boxProps -> {
boxProps.horizontalAlignment =
HorizontalAlignment.HORIZONTAL_ALIGN_CENTER;
@@ -3207,13 +3217,13 @@
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
+ box( // 1
boxProps -> {
// A different set of alignments.
boxProps.horizontalAlignment =
@@ -3222,7 +3232,7 @@
VerticalAlignment.VERTICAL_ALIGN_BOTTOM;
},
text("World") // 1.1
- ));
+ ));
ViewGroupMutation mutation =
renderer.mRenderer.computeMutation(
@@ -3268,10 +3278,9 @@
ProtoLayoutInflater.Config.Builder newRendererConfigBuilder(
Layout layout, ResourceResolvers.Builder resourceResolvers) {
return new ProtoLayoutInflater.Config.Builder(
- getApplicationContext(), layout, resourceResolvers.build())
+ getApplicationContext(), layout, resourceResolvers.build())
.setClickableIdExtra(EXTRA_CLICKABLE_ID)
- .setLoadActionListener(p -> {
- })
+ .setLoadActionListener(p -> {})
.setLoadActionExecutor(ContextCompat.getMainExecutor(getApplicationContext()));
}
@@ -3412,6 +3421,7 @@
.build();
}
+ @NonNull
private static Trigger onVisibleTrigger() {
return Trigger.newBuilder()
.setOnVisibleTrigger(OnVisibleTrigger.getDefaultInstance())
@@ -3461,6 +3471,16 @@
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";
@@ -3529,15 +3549,6 @@
expect.that(linearLayoutParams.weight).isEqualTo(10.0f);
}
- private static ContainerDimension expandWeight() {
- return ContainerDimension.newBuilder()
- .setExpandedDimension(
- ExpandedDimensionProp.newBuilder()
- .setLayoutWeight(FloatProp.newBuilder().setValue(10.0f).build())
- .build())
- .build();
- }
-
@Test
public void enterTransition_noQuota_notPlayed() throws Exception {
Renderer renderer =
@@ -3881,9 +3892,9 @@
Renderer renderer =
renderer(
newRendererConfigBuilder(
- fingerprintedLayout(
- getTextElementWithExitAnimation(
- "Hello", /* iterations= */ 1)))
+ fingerprintedLayout(
+ getTextElementWithExitAnimation(
+ "Hello", /* iterations= */ 1)))
.setAnimationEnabled(false));
mDataPipeline.setFullyVisible(true);
FrameLayout inflatedViewParent = renderer.inflate();
@@ -4124,8 +4135,8 @@
.setEnterTransition(
EnterTransition.newBuilder()
.setFadeIn(
- FadeInTransition.newBuilder()
- .build())
+ FadeInTransition
+ .getDefaultInstance())
.setSlideIn(
SlideInTransition.newBuilder()
.build())),
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java
index 3b6ef5b1..4f8ca09 100644
--- a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java
@@ -43,6 +43,7 @@
import androidx.wear.tiles.renderer.TileRenderer;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
public class GoldenTestActivity extends Activity {
private static final String ICON_ID = "tile_icon";
@@ -76,11 +77,16 @@
Resources resources = generateResources();
TileRenderer renderer = new TileRenderer(appContext, mainExecutor, i -> {});
- View firstChild = renderer.inflate(layout, resources, root);
+ try {
+ View firstChild =
+ renderer.inflateAsync(layout, resources, root).get(30, TimeUnit.MILLISECONDS);
- // Simulate what the thing outside the renderer should do. Center the contents.
- LayoutParams layoutParams = (LayoutParams) firstChild.getLayoutParams();
- layoutParams.gravity = Gravity.CENTER;
+ // Simulate what the thing outside the renderer should do. Center the contents.
+ LayoutParams layoutParams = (LayoutParams) firstChild.getLayoutParams();
+ layoutParams.gravity = Gravity.CENTER;
+ } catch (Exception e) {
+ throw new IllegalStateException("Rendering of layout hasn't finished in time.", e);
+ }
// Set the activity to be full screen so when we crop the Bitmap we don't get time bar etc.
requestWindowFeature(Window.FEATURE_NO_TITLE);
diff --git a/wear/tiles/tiles-renderer/api/current.txt b/wear/tiles/tiles-renderer/api/current.txt
index 670ef3a..431fc0c 100644
--- a/wear/tiles/tiles-renderer/api/current.txt
+++ b/wear/tiles/tiles-renderer/api/current.txt
@@ -46,7 +46,7 @@
ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
ctor public TileRenderer(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
method @Deprecated public android.view.View? inflate(android.view.ViewGroup);
- method public android.view.View? inflate(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
+ method public com.google.common.util.concurrent.ListenableFuture<android.view.View!> inflateAsync(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
}
@Deprecated public static interface TileRenderer.LoadActionListener {
diff --git a/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt b/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt
index 670ef3a..431fc0c 100644
--- a/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt
+++ b/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt
@@ -46,7 +46,7 @@
ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
ctor public TileRenderer(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
method @Deprecated public android.view.View? inflate(android.view.ViewGroup);
- method public android.view.View? inflate(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
+ method public com.google.common.util.concurrent.ListenableFuture<android.view.View!> inflateAsync(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
}
@Deprecated public static interface TileRenderer.LoadActionListener {
diff --git a/wear/tiles/tiles-renderer/api/restricted_current.txt b/wear/tiles/tiles-renderer/api/restricted_current.txt
index 670ef3a..431fc0c 100644
--- a/wear/tiles/tiles-renderer/api/restricted_current.txt
+++ b/wear/tiles/tiles-renderer/api/restricted_current.txt
@@ -46,7 +46,7 @@
ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
ctor public TileRenderer(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
method @Deprecated public android.view.View? inflate(android.view.ViewGroup);
- method public android.view.View? inflate(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
+ method public com.google.common.util.concurrent.ListenableFuture<android.view.View!> inflateAsync(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
}
@Deprecated public static interface TileRenderer.LoadActionListener {
diff --git a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
index 8d16cea..a80d71a 100644
--- a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
+++ b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
@@ -53,6 +53,7 @@
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collection;
+import java.util.concurrent.TimeUnit;
@RunWith(Parameterized.class)
@LargeTest
@@ -239,9 +240,10 @@
ContextCompat.getMainExecutor(getApplicationContext()),
i -> {});
- View firstChild = renderer.inflate(LayoutElementBuilders.Layout.fromProto(
+ View firstChild = renderer.inflateAsync(LayoutElementBuilders.Layout.fromProto(
Layout.newBuilder().setRoot(rootElement).build()),
- ResourceBuilders.Resources.fromProto(generateResources()), mainFrame);
+ ResourceBuilders.Resources.fromProto(generateResources()), mainFrame)
+ .get(30, TimeUnit.MILLISECONDS);
if (firstChild == null) {
throw new RuntimeException("Failed to inflate " + expectedKey);
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileUiClient.kt b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileUiClient.kt
index 525dfcf6..5681e15 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileUiClient.kt
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileUiClient.kt
@@ -30,16 +30,17 @@
import androidx.annotation.MainThread
import androidx.concurrent.futures.await
import androidx.core.content.ContextCompat
-import androidx.wear.tiles.DeviceParametersBuilders
import androidx.wear.protolayout.LayoutElementBuilders
import androidx.wear.protolayout.ResourceBuilders
import androidx.wear.protolayout.StateBuilders
+import androidx.wear.tiles.DeviceParametersBuilders
import androidx.wear.tiles.RequestBuilders
import androidx.wear.tiles.TimelineBuilders
import androidx.wear.tiles.checkers.TimelineChecker
import androidx.wear.tiles.connection.DefaultTileClient
import androidx.wear.tiles.renderer.TileRenderer
import androidx.wear.tiles.timeline.TilesTimelineManager
+import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -48,7 +49,6 @@
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import java.util.concurrent.Executors
/**
* UI client for a single tile. This handles binding to a Tile Service, and inflating the given
@@ -199,13 +199,15 @@
ContextCompat.getMainExecutor(context),
{ state -> coroutineScope.launch { requestTile(state) } }
)
- renderer.inflate(
+ val result = renderer.inflateAsync(
LayoutElementBuilders.Layout.fromProto(layout.toProto()),
tileResources!!,
parentView
- )?.apply {
- (layoutParams as FrameLayout.LayoutParams).gravity = Gravity.CENTER
- }
+ )
+ result.addListener(
+ { (result.get().layoutParams as FrameLayout.LayoutParams).gravity = Gravity.CENTER },
+ ContextCompat.getMainExecutor(context)
+ )
}
private fun registerBroadcastReceiver() {
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index e3a8ce1..1a8c76c 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -35,11 +35,16 @@
import androidx.wear.tiles.TileService;
import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
/**
@@ -80,8 +85,8 @@
* @param loadActionExecutor Executor for {@code loadActionListener}.
* @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
* @deprecated Use {@link #TileRenderer(Context, Executor, Consumer)} which accepts Layout and
- * Resources in {@link #inflate(LayoutElementBuilders.Layout, ResourceBuilders.Resources,
- * ViewGroup)} method.
+ * Resources in {@link #inflateAsync(LayoutElementBuilders.Layout,
+ * ResourceBuilders.Resources, ViewGroup)} method.
*/
@Deprecated
public TileRenderer(
@@ -109,8 +114,8 @@
* @param loadActionExecutor Executor for {@code loadActionListener}.
* @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
* @deprecated Use {@link #TileRenderer(Context, Executor, Consumer)} which accepts Layout and
- * Resources in {@link #inflate(LayoutElementBuilders.Layout, ResourceBuilders.Resources,
- * ViewGroup)} method.
+ * Resources in {@link #inflateAsync(LayoutElementBuilders.Layout,
+ * ResourceBuilders.Resources, ViewGroup)} method.
*/
@Deprecated
public TileRenderer(
@@ -187,9 +192,9 @@
* @return The first child that was inflated. This may be null if the Layout is empty or the
* top-level LayoutElement has no inner set, or the top-level LayoutElement contains an
* unsupported inner type.
- * @deprecated Use {@link #inflate(LayoutElementBuilders.Layout, ResourceBuilders.Resources,
- * ViewGroup)} instead. Note: This method only works with the deprecated constructors that
- * accept Layout and Resources.
+ * @deprecated Use {@link #inflateAsync(LayoutElementBuilders.Layout,
+ * ResourceBuilders.Resources, ViewGroup)} instead. Note: This method only works with the
+ * deprecated constructors that accept Layout and Resources.
*/
@Deprecated
@Nullable
@@ -197,44 +202,45 @@
String errorMessage =
"This method only works with the deprecated constructors that accept Layout and"
+ " Resources.";
- return inflateLayout(
- checkNotNull(mLayout, errorMessage),
- checkNotNull(mResources, errorMessage),
- parent);
+ try {
+ // Waiting for the result from future for backwards compatibility.
+ return inflateLayout(
+ checkNotNull(mLayout, errorMessage),
+ checkNotNull(mResources, errorMessage),
+ parent).get(10, TimeUnit.SECONDS);
+ } catch (ExecutionException | InterruptedException | CancellationException |
+ TimeoutException e) {
+ // Wrap checked exceptions to avoid changing the method signature.
+ throw new RuntimeException("Rendering tile has not successfully finished.", e);
+ }
}
/**
* Inflates a Tile into {@code parent}.
*
- * @param layout The portion of the Tile to render.
+ * @param layout The portion of the Tile to render.
* @param resources The resources for the Tile.
- * @param parent The view to attach the tile into.
- * @return The first child that was inflated. This may be null if the Layout is empty or the
- * top-level LayoutElement has no inner set, or the top-level LayoutElement contains an
- * unsupported inner type.
+ * @param parent The view to attach the tile into.
+ * @return The future with the first child that was inflated. This may be null if the Layout is
+ * empty or the top-level LayoutElement has no inner set, or the top-level LayoutElement
+ * contains an
+ * unsupported inner type.
*/
- @Nullable
- public View inflate(
+ @NonNull
+ public ListenableFuture<View> inflateAsync(
@NonNull LayoutElementBuilders.Layout layout,
@NonNull ResourceBuilders.Resources resources,
@NonNull ViewGroup parent) {
return inflateLayout(layout.toProto(), resources.toProto(), parent);
}
- @Nullable
- private View inflateLayout(
+ @NonNull
+ private ListenableFuture<View> inflateLayout(
@NonNull LayoutElementProto.Layout layout,
@NonNull ResourceProto.Resources resources,
@NonNull ViewGroup parent) {
- mInstance.renderAndAttach(layout, resources, parent);
- boolean finished;
- try {
- mUiExecutor.shutdown();
- finished = mUiExecutor.awaitTermination(30, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- throw new RuntimeException("Rendering tile has not successfully finished.");
- }
- // TODO(b/271076323): Update when renderAndAttach returns result.
- return finished ? parent.getChildAt(0) : null;
+ ListenableFuture<Void> result = mInstance.renderAndAttach(layout, resources, parent);
+ return FluentFuture.from(result)
+ .transform(ignored -> parent.getChildAt(0), mUiExecutor);
}
}
diff --git a/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
index 20202d9..ccc7b55 100644
--- a/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
+++ b/wear/tiles/tiles-tooling/src/main/java/androidx/wear/tiles/tooling/TileServiceViewAdapter.kt
@@ -51,12 +51,13 @@
while (currentClass != null) {
try {
return currentClass.getDeclaredMethod(name, *parameterTypes)
- } catch (_: NoSuchMethodException) { }
+ } catch (_: NoSuchMethodException) {}
currentClass = currentClass.superclass
}
val methodSignature = "$name(${parameterTypes.joinToString { ", " }})"
throw NoSuchMethodException(
- "Could not find method $methodSignature neither in $this nor in its superclasses.")
+ "Could not find method $methodSignature neither in $this nor in its superclasses."
+ )
}
/**
@@ -85,17 +86,17 @@
// tileService.attachBaseContext(context)
val attachBaseContextMethod =
- tileServiceClass
- .findMethod("attachBaseContext", Context::class.java)
- .apply { isAccessible = true }
+ tileServiceClass.findMethod("attachBaseContext", Context::class.java).apply {
+ isAccessible = true
+ }
attachBaseContextMethod.invoke(tileService, context)
val deviceParams = context.buildDeviceParameters()
- val tileRequest = RequestBuilders.TileRequest
- .Builder()
- .setState(StateBuilders.State.Builder().build())
- .setDeviceParameters(deviceParams)
- .build()
+ val tileRequest =
+ RequestBuilders.TileRequest.Builder()
+ .setState(StateBuilders.State.Builder().build())
+ .setDeviceParameters(deviceParams)
+ .build()
// val tile = tileService.onTileRequest(tileRequest)
val onTileRequestMethod =
@@ -103,14 +104,15 @@
.findMethod("onTileRequest", RequestBuilders.TileRequest::class.java)
.apply { isAccessible = true }
val tile =
- (onTileRequestMethod.invoke(tileService, tileRequest) as
- ListenableFuture<TileBuilders.Tile>).get(1, TimeUnit.SECONDS)
+ (onTileRequestMethod.invoke(tileService, tileRequest)
+ as ListenableFuture<TileBuilders.Tile>)
+ .get(1, TimeUnit.SECONDS)
- val resourceRequest = RequestBuilders.ResourcesRequest
- .Builder()
- .setVersion(tile.resourcesVersion)
- .setDeviceParameters(deviceParams)
- .build()
+ val resourceRequest =
+ RequestBuilders.ResourcesRequest.Builder()
+ .setVersion(tile.resourcesVersion)
+ .setDeviceParameters(deviceParams)
+ .build()
// val resources = tileService.onResourcesRequest(resourceRequest).get(1, TimeUnit.SECONDS)
val onResourcesRequestMethod =
@@ -119,20 +121,22 @@
.apply { isAccessible = true }
val resources =
ResourceBuilders.Resources.fromProto(
- (onResourcesRequestMethod.invoke(tileService, resourceRequest) as
- ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources>)
- .get(1, TimeUnit.SECONDS).toProto()
+ (onResourcesRequestMethod.invoke(tileService, resourceRequest)
+ as ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources>)
+ .get(1, TimeUnit.SECONDS)
+ .toProto()
)
val layout = tile.timeline?.getCurrentLayout()
if (layout != null) {
- val renderer = TileRenderer(
- context,
+ val renderer = TileRenderer(context, ContextCompat.getMainExecutor(context)) {}
+ val result = renderer.inflateAsync(layout, resources, this)
+ result.addListener(
+ {
+ (result.get().layoutParams as FrameLayout.LayoutParams).gravity = Gravity.CENTER
+ },
ContextCompat.getMainExecutor(context)
- ) { }
- renderer.inflate(layout, resources, this)?.apply {
- (layoutParams as FrameLayout.LayoutParams).gravity = Gravity.CENTER
- }
+ )
}
}
}
@@ -140,24 +144,20 @@
internal fun TimelineBuilders.Timeline?.getCurrentLayout(): LayoutElementBuilders.Layout? {
val now = System.currentTimeMillis()
return this?.let {
- val cache = TilesTimelineCache(it)
- cache.findTimelineEntryForTime(now) ?: cache.findClosestTimelineEntry(now)
- }?.layout?.let {
- LayoutElementBuilders.Layout.fromProto(it.toProto())
- }
+ val cache = TilesTimelineCache(it)
+ cache.findTimelineEntryForTime(now) ?: cache.findClosestTimelineEntry(now)
+ }
+ ?.layout
+ ?.let { LayoutElementBuilders.Layout.fromProto(it.toProto()) }
}
-/**
- * Creates an instance of [DeviceParametersBuilders.DeviceParameters] from the [Context].
- */
+/** Creates an instance of [DeviceParametersBuilders.DeviceParameters] from the [Context]. */
internal fun Context.buildDeviceParameters(): DeviceParametersBuilders.DeviceParameters {
val displayMetrics = resources.displayMetrics
val isScreenRound = resources.configuration.isScreenRound
return DeviceParametersBuilders.DeviceParameters.Builder()
- .setScreenWidthDp(
- (displayMetrics.widthPixels / displayMetrics.density).roundToInt())
- .setScreenHeightDp(
- (displayMetrics.heightPixels / displayMetrics.density).roundToInt())
+ .setScreenWidthDp((displayMetrics.widthPixels / displayMetrics.density).roundToInt())
+ .setScreenHeightDp((displayMetrics.heightPixels / displayMetrics.density).roundToInt())
.setScreenDensity(displayMetrics.density)
.setScreenShape(
if (isScreenRound) DeviceParametersBuilders.SCREEN_SHAPE_ROUND
@@ -165,4 +165,4 @@
)
.setDevicePlatform(DeviceParametersBuilders.DEVICE_PLATFORM_WEAR_OS)
.build()
-}
\ No newline at end of file
+}