diff options
22 files changed, 581 insertions, 79 deletions
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/AndroidPlatformSemanticNodeApplier.java b/core/java/com/android/internal/widget/remotecompose/accessibility/AndroidPlatformSemanticNodeApplier.java index a53d6b899898..3fe6873028cd 100644 --- a/core/java/com/android/internal/widget/remotecompose/accessibility/AndroidPlatformSemanticNodeApplier.java +++ b/core/java/com/android/internal/widget/remotecompose/accessibility/AndroidPlatformSemanticNodeApplier.java @@ -132,8 +132,13 @@ public class AndroidPlatformSemanticNodeApplier } } - // TODO correct values - nodeInfo.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(-1, 1, false)); + if (scrollDirection == RootContentBehavior.SCROLL_HORIZONTAL) { + nodeInfo.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(1, -1, false)); + nodeInfo.setClassName("android.widget.HorizontalScrollView"); + } else { + nodeInfo.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(-1, 1, false)); + nodeInfo.setClassName("android.widget.ScrollView"); + } if (scrollDirection == RootContentBehavior.SCROLL_HORIZONTAL) { nodeInfo.setClassName("android.widget.HorizontalScrollView"); diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java index f70f4cbceb70..db2c46046561 100644 --- a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java +++ b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java @@ -33,6 +33,7 @@ import com.android.internal.widget.remotecompose.core.semantics.AccessibilitySem import com.android.internal.widget.remotecompose.core.semantics.AccessibleComponent; import com.android.internal.widget.remotecompose.core.semantics.CoreSemantics; import com.android.internal.widget.remotecompose.core.semantics.ScrollableComponent; +import com.android.internal.widget.remotecompose.core.semantics.ScrollableComponent.ScrollDirection; import java.util.ArrayList; import java.util.Collections; @@ -104,9 +105,9 @@ public class CoreDocumentAccessibility implements RemoteComposeDocumentAccessibi if (isClickAction(action)) { return performClick(component); } else if (isScrollForwardAction(action)) { - return scrollByOffset(mRemoteContext, component, -500) != 0; + return scrollDirection(mRemoteContext, component, ScrollDirection.FORWARD); } else if (isScrollBackwardAction(action)) { - return scrollByOffset(mRemoteContext, component, 500) != 0; + return scrollDirection(mRemoteContext, component, ScrollDirection.BACKWARD); } else if (isShowOnScreenAction(action)) { return showOnScreen(mRemoteContext, component); } else { @@ -141,17 +142,30 @@ public class CoreDocumentAccessibility implements RemoteComposeDocumentAccessibi } private boolean showOnScreen(RemoteContext context, Component component) { - if (component.getParent() instanceof LayoutComponent) { - LayoutComponent parent = (LayoutComponent) component.getParent(); + ScrollableComponent scrollable = findScrollable(component); + + if (scrollable != null) { + return scrollable.showOnScreen(context, component); + } + + return false; + } + + @Nullable + private static ScrollableComponent findScrollable(Component component) { + Component parent = component.getParent(); + + while (parent != null) { ScrollableComponent scrollable = parent.selfOrModifier(ScrollableComponent.class); if (scrollable != null) { - scrollable.showOnScreen(context, component.getComponentId()); - return true; + return scrollable; + } else { + parent = parent.getParent(); } } - return false; + return null; } /** @@ -173,6 +187,25 @@ public class CoreDocumentAccessibility implements RemoteComposeDocumentAccessibi } /** + * scroll content in a given direction + * + * @param context + * @param component + * @param direction + * @return + */ + public boolean scrollDirection( + RemoteContext context, Component component, ScrollDirection direction) { + ScrollableComponent scrollable = component.selfOrModifier(ScrollableComponent.class); + + if (scrollable != null) { + return scrollable.scrollDirection(context, direction); + } + + return false; + } + + /** * Perform a click on the given component * * @param component diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java index c38a44ac30be..da4e8d621602 100644 --- a/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java +++ b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java @@ -39,6 +39,7 @@ public class PlatformRemoteComposeTouchHelper extends ExploreByTouchHelper { private final RemoteComposeDocumentAccessibility mRemoteDocA11y; private final SemanticNodeApplier<AccessibilityNodeInfo> mApplier; + private final View mHost; public PlatformRemoteComposeTouchHelper( View host, @@ -47,6 +48,7 @@ public class PlatformRemoteComposeTouchHelper extends ExploreByTouchHelper { super(host); this.mRemoteDocA11y = remoteDocA11y; this.mApplier = applier; + this.mHost = host; } public static PlatformRemoteComposeTouchHelper forRemoteComposePlayer( @@ -150,6 +152,7 @@ public class PlatformRemoteComposeTouchHelper extends ExploreByTouchHelper { boolean performed = mRemoteDocA11y.performAction(component, action, arguments); if (performed) { + mHost.invalidate(); invalidateRoot(); } diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java index caf19e1ed34a..e5c20eb7ce18 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java +++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java @@ -73,7 +73,9 @@ public class CoreDocument implements Serializable { // We also keep a more fine-grained BUILD number, exposed as // ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD - static final float BUILD = 0.5f; + static final float BUILD = 0.6f; + + private static final boolean UPDATE_VARIABLES_BEFORE_LAYOUT = false; @NonNull ArrayList<Operation> mOperations = new ArrayList<>(); @@ -892,7 +894,10 @@ public class CoreDocument implements Serializable { registerVariables(context, mOperations); context.mMode = RemoteContext.ContextMode.UNSET; - mFirstPaint = true; + + if (UPDATE_VARIABLES_BEFORE_LAYOUT) { + mFirstPaint = true; + } } /////////////////////////////////////////////////////////////////////////////////////////////// @@ -1241,11 +1246,13 @@ public class CoreDocument implements Serializable { context.mRemoteComposeState = mRemoteComposeState; context.mRemoteComposeState.setContext(context); - // Update any dirty variables - if (mFirstPaint) { - mFirstPaint = false; - } else { - updateVariables(context, theme, mOperations); + if (UPDATE_VARIABLES_BEFORE_LAYOUT) { + // Update any dirty variables + if (mFirstPaint) { + mFirstPaint = false; + } else { + updateVariables(context, theme, mOperations); + } } // If we have a content sizing set, we are going to take the original document diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java index ac9f98bd6b15..add9d5bae552 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java +++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java @@ -111,6 +111,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.managers import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BackgroundModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BorderModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ClipRectModifierOperation; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.CollapsiblePriorityModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentVisibilityOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.DrawContentOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.GraphicsLayerModifierOperation; @@ -257,6 +258,7 @@ public class Operations { public static final int MODIFIER_HEIGHT = 67; public static final int MODIFIER_WIDTH_IN = 231; public static final int MODIFIER_HEIGHT_IN = 232; + public static final int MODIFIER_COLLAPSIBLE_PRIORITY = 235; public static final int MODIFIER_BACKGROUND = 55; public static final int MODIFIER_BORDER = 107; public static final int MODIFIER_PADDING = 58; @@ -368,6 +370,7 @@ public class Operations { map.put(MODIFIER_HEIGHT, HeightModifierOperation::read); map.put(MODIFIER_WIDTH_IN, WidthInModifierOperation::read); map.put(MODIFIER_HEIGHT_IN, HeightInModifierOperation::read); + map.put(MODIFIER_COLLAPSIBLE_PRIORITY, CollapsiblePriorityModifierOperation::read); map.put(MODIFIER_PADDING, PaddingModifierOperation::read); map.put(MODIFIER_BACKGROUND, BackgroundModifierOperation::read); map.put(MODIFIER_BORDER, BorderModifierOperation::read); diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java index e37833f33fa5..b297a023d03b 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java +++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java @@ -46,7 +46,7 @@ public abstract class RemoteContext { new CoreDocument(); // todo: is this a valid way to initialize? bbade@ public @NonNull RemoteComposeState mRemoteComposeState = new RemoteComposeState(); // todo, is this a valid use of RemoteComposeState -- bbade@ - + private long mDocLoadTime = System.currentTimeMillis(); @Nullable protected PaintContext mPaintContext = null; protected float mDensity = Float.NaN; @@ -83,6 +83,20 @@ public abstract class RemoteContext { } } + /** + * Get the time the document was loaded + * + * @return time in ms since the document was loaded + */ + public long getDocLoadTime() { + return mDocLoadTime; + } + + /** Set the time the document was loaded */ + public void setDocLoadTime() { + mDocLoadTime = System.currentTimeMillis(); + } + public boolean isAnimationEnabled() { return mAnimate; } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TimeAttribute.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TimeAttribute.java index e9cc26f58c9b..dee79a45de3d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/TimeAttribute.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TimeAttribute.java @@ -85,6 +85,9 @@ public class TimeAttribute extends PaintOperation { /** the year */ public static final short TIME_YEAR = 12; + /** (value - doc_load_time) * 1E-3 */ + public static final short TIME_FROM_LOAD_SEC = 14; + /** * creates a new operation * @@ -226,6 +229,7 @@ public class TimeAttribute extends PaintOperation { int val = mType & 255; int flags = mType >> 8; RemoteContext ctx = context.getContext(); + long load_time = ctx.getDocLoadTime(); LongConstant longConstant = (LongConstant) ctx.getObject(mTimeId); long value = longConstant.getValue(); long delta = 0; @@ -292,6 +296,9 @@ public class TimeAttribute extends PaintOperation { case TIME_YEAR: ctx.loadFloat(mId, time.getYear()); break; + case TIME_FROM_LOAD_SEC: + ctx.loadFloat(mId, (value - load_time) * 1E-3f); + break; } } @@ -334,6 +341,8 @@ public class TimeAttribute extends PaintOperation { return "TIME_DAY_OF_WEEK"; case TIME_YEAR: return "TIME_YEAR"; + case TIME_FROM_LOAD_SEC: + return "TIME_FROM_LOAD_SEC"; default: return "INVALID_TIME_TYPE"; } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java index f1158d91f94b..2a809c6f0a2a 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java @@ -824,15 +824,27 @@ public class Component extends PaintOperation * * @param value a 2 dimension float array that will receive the horizontal and vertical position * of the component. + * @param forSelf whether the location is for this container or a child, relevant for scrollable + * items. */ - public void getLocationInWindow(@NonNull float[] value) { + public void getLocationInWindow(@NonNull float[] value, boolean forSelf) { value[0] += mX; value[1] += mY; if (mParent != null) { - mParent.getLocationInWindow(value); + mParent.getLocationInWindow(value, false); } } + /** + * Returns the location of the component relative to the root component + * + * @param value a 2 dimension float array that will receive the horizontal and vertical position + * of the component. + */ + public void getLocationInWindow(@NonNull float[] value) { + getLocationInWindow(value, true); + } + @NonNull @Override public String toString() { diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java index 6163d8099b8c..bc099e3a3b9d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java @@ -289,11 +289,11 @@ public class LayoutComponent extends Component { } @Override - public void getLocationInWindow(@NonNull float[] value) { + public void getLocationInWindow(@NonNull float[] value, boolean forSelf) { value[0] += mX + mPaddingLeft; value[1] += mY + mPaddingTop; if (mParent != null) { - mParent.getLocationInWindow(value); + mParent.getLocationInWindow(value, false); } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsibleColumnLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsibleColumnLayout.java index b0089525af5a..00ec60533087 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsibleColumnLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsibleColumnLayout.java @@ -24,10 +24,13 @@ import com.android.internal.widget.remotecompose.core.PaintContext; import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.operations.layout.Component; +import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent; import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure; import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass; import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.CollapsiblePriorityModifierOperation; +import java.util.ArrayList; import java.util.List; public class CollapsibleColumnLayout extends ColumnLayout { @@ -153,7 +156,7 @@ public class CollapsibleColumnLayout extends ColumnLayout { } @Override - protected boolean hasVerticalIntrinsicDimension() { + public boolean hasVerticalIntrinsicDimension() { return true; } @@ -166,25 +169,72 @@ public class CollapsibleColumnLayout extends ColumnLayout { boolean verticalWrap, @NonNull MeasurePass measure, @NonNull Size size) { + computeVisibleChildren( + context, maxWidth, maxHeight, horizontalWrap, verticalWrap, measure, size); + } + + @Override + public void computeSize( + @NonNull PaintContext context, + float minWidth, + float maxWidth, + float minHeight, + float maxHeight, + @NonNull MeasurePass measure) { + computeVisibleChildren(context, maxWidth, maxHeight, false, false, measure, null); + } + + @Override + public void internalLayoutMeasure(@NonNull PaintContext context, @NonNull MeasurePass measure) { + // if needed, take care of weight calculations + super.internalLayoutMeasure(context, measure); + // Check again for visibility + ComponentMeasure m = measure.get(this); + computeVisibleChildren(context, m.getW(), m.getH(), false, false, measure, null); + } + + private void computeVisibleChildren( + @NonNull PaintContext context, + float maxWidth, + float maxHeight, + boolean horizontalWrap, + boolean verticalWrap, + @NonNull MeasurePass measure, + @Nullable Size size) { int visibleChildren = 0; ComponentMeasure self = measure.get(this); self.addVisibilityOverride(Visibility.OVERRIDE_VISIBLE); float currentMaxHeight = maxHeight; + boolean hasPriorities = false; for (Component c : mChildrenComponents) { - if (c instanceof CollapsibleColumnLayout) { - c.measure(context, 0f, maxWidth, 0f, currentMaxHeight, measure); - } else { - c.measure(context, 0f, maxWidth, 0f, Float.MAX_VALUE, measure); + if (!measure.contains(c.getComponentId())) { + // No need to remeasure here if already done + if (c instanceof CollapsibleColumnLayout) { + c.measure(context, 0f, maxWidth, 0f, currentMaxHeight, measure); + } else { + c.measure(context, 0f, maxWidth, 0f, Float.MAX_VALUE, measure); + } } + ComponentMeasure m = measure.get(c); if (!m.isGone()) { - size.setWidth(Math.max(size.getWidth(), m.getW())); - size.setHeight(size.getHeight() + m.getH()); + if (size != null) { + size.setWidth(Math.max(size.getWidth(), m.getW())); + size.setHeight(size.getHeight() + m.getH()); + } visibleChildren++; currentMaxHeight -= m.getH(); } + if (c instanceof LayoutComponent) { + LayoutComponent lc = (LayoutComponent) c; + CollapsiblePriorityModifierOperation priority = + lc.selfOrModifier(CollapsiblePriorityModifierOperation.class); + if (priority != null) { + hasPriorities = true; + } + } } - if (!mChildrenComponents.isEmpty()) { + if (!mChildrenComponents.isEmpty() && size != null) { size.setHeight(size.getHeight() + (mSpacedBy * (visibleChildren - 1))); } @@ -192,7 +242,14 @@ public class CollapsibleColumnLayout extends ColumnLayout { float childrenHeight = 0f; boolean overflow = false; - for (Component child : mChildrenComponents) { + ArrayList<Component> children = mChildrenComponents; + if (hasPriorities) { + // TODO: We need to cache this. + children = + CollapsiblePriority.sortWithPriorities( + mChildrenComponents, CollapsiblePriority.VERTICAL); + } + for (Component child : children) { ComponentMeasure childMeasure = measure.get(child); if (overflow || childMeasure.isGone()) { childMeasure.addVisibilityOverride(Visibility.OVERRIDE_GONE); @@ -209,10 +266,10 @@ public class CollapsibleColumnLayout extends ColumnLayout { visibleChildren++; } } - if (verticalWrap) { + if (verticalWrap && size != null) { size.setHeight(Math.min(maxHeight, childrenHeight)); } - if (visibleChildren == 0 || size.getHeight() <= 0f) { + if (visibleChildren == 0 || (size != null && size.getHeight() <= 0f)) { self.addVisibilityOverride(Visibility.OVERRIDE_GONE); } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsiblePriority.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsiblePriority.java new file mode 100644 index 000000000000..46cd45ecba8a --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsiblePriority.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.operations.layout.managers; + +import com.android.internal.widget.remotecompose.core.operations.layout.Component; +import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.CollapsiblePriorityModifierOperation; + +import java.util.ArrayList; + +/** Utility class to manage collapsible priorities on components */ +public class CollapsiblePriority { + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + /** + * Returns the priority of a child component + * + * @param c the child component + * @return priority value, or 0f if not found + */ + static float getPriority(Component c, int orientation) { + if (c instanceof LayoutComponent) { + LayoutComponent lc = (LayoutComponent) c; + CollapsiblePriorityModifierOperation priority = + lc.selfOrModifier(CollapsiblePriorityModifierOperation.class); + if (priority != null && priority.getOrientation() == orientation) { + return priority.getPriority(); + } + } + return Float.MAX_VALUE; + } + + /** + * Allocate and return a sorted array of components by their priorities + * + * @param components the children components + * @return list of components sorted by their priority in decreasing order + */ + static ArrayList<Component> sortWithPriorities( + ArrayList<Component> components, int orientation) { + ArrayList<Component> sorted = new ArrayList<>(components); + sorted.sort( + (t1, t2) -> { + float p1 = getPriority(t1, orientation); + float p2 = getPriority(t2, orientation); + return (int) (p2 - p1); + }); + return sorted; + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsibleRowLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsibleRowLayout.java index 05f332960c16..e3632f9888ec 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsibleRowLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CollapsibleRowLayout.java @@ -24,10 +24,13 @@ import com.android.internal.widget.remotecompose.core.PaintContext; import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.operations.layout.Component; +import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent; import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure; import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass; import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.CollapsiblePriorityModifierOperation; +import java.util.ArrayList; import java.util.List; public class CollapsibleRowLayout extends RowLayout { @@ -135,8 +138,12 @@ public class CollapsibleRowLayout extends RowLayout { } @Override - protected boolean hasHorizontalIntrinsicDimension() { - return true; + public float minIntrinsicHeight(@NonNull RemoteContext context) { + float height = computeModifierDefinedHeight(context); + if (!mChildrenComponents.isEmpty()) { + height += mChildrenComponents.get(0).minIntrinsicHeight(context); + } + return height; } @Override @@ -149,12 +156,8 @@ public class CollapsibleRowLayout extends RowLayout { } @Override - public float minIntrinsicHeight(@NonNull RemoteContext context) { - float height = computeModifierDefinedHeight(context); - if (!mChildrenComponents.isEmpty()) { - height += mChildrenComponents.get(0).minIntrinsicHeight(context); - } - return height; + public boolean hasHorizontalIntrinsicDimension() { + return true; } @Override @@ -166,45 +169,107 @@ public class CollapsibleRowLayout extends RowLayout { boolean verticalWrap, @NonNull MeasurePass measure, @NonNull Size size) { - super.computeWrapSize( - context, Float.MAX_VALUE, maxHeight, horizontalWrap, verticalWrap, measure, size); + computeVisibleChildren( + context, maxWidth, maxHeight, horizontalWrap, verticalWrap, measure, size); } @Override - public boolean applyVisibility( - float selfWidth, float selfHeight, @NonNull MeasurePass measure) { - float childrenWidth = 0f; - float childrenHeight = 0f; - boolean changedVisibility = false; + public void computeSize( + @NonNull PaintContext context, + float minWidth, + float maxWidth, + float minHeight, + float maxHeight, + @NonNull MeasurePass measure) { + computeVisibleChildren(context, maxWidth, maxHeight, false, false, measure, null); + } + + @Override + public void internalLayoutMeasure(@NonNull PaintContext context, @NonNull MeasurePass measure) { + // if needed, take care of weight calculations + super.internalLayoutMeasure(context, measure); + // Check again for visibility + ComponentMeasure m = measure.get(this); + computeVisibleChildren(context, m.getW(), m.getH(), false, false, measure, null); + } + + private void computeVisibleChildren( + @NonNull PaintContext context, + float maxWidth, + float maxHeight, + boolean horizontalWrap, + boolean verticalWrap, + @NonNull MeasurePass measure, + @Nullable Size size) { int visibleChildren = 0; ComponentMeasure self = measure.get(this); - self.clearVisibilityOverride(); - if (selfWidth <= 0 || selfHeight <= 0) { - self.addVisibilityOverride(Visibility.OVERRIDE_GONE); - return true; + self.addVisibilityOverride(Visibility.OVERRIDE_VISIBLE); + float currentMaxWidth = maxWidth; + boolean hasPriorities = false; + for (Component c : mChildrenComponents) { + if (!measure.contains(c.getComponentId())) { + // No need to remeasure here if already done + if (c instanceof CollapsibleRowLayout) { + c.measure(context, 0f, currentMaxWidth, 0f, maxHeight, measure); + } else { + c.measure(context, 0f, Float.MAX_VALUE, 0f, maxHeight, measure); + } + } + ComponentMeasure m = measure.get(c); + if (!m.isGone()) { + if (size != null) { + size.setHeight(Math.max(size.getHeight(), m.getH())); + size.setWidth(size.getWidth() + m.getW()); + } + visibleChildren++; + currentMaxWidth -= m.getW(); + } + if (c instanceof LayoutComponent) { + LayoutComponent lc = (LayoutComponent) c; + CollapsiblePriorityModifierOperation priority = + lc.selfOrModifier(CollapsiblePriorityModifierOperation.class); + if (priority != null) { + hasPriorities = true; + } + } + } + if (!mChildrenComponents.isEmpty() && size != null) { + size.setWidth(size.getWidth() + (mSpacedBy * (visibleChildren - 1))); } - for (Component child : mChildrenComponents) { + + float childrenWidth = 0f; + float childrenHeight = 0f; + + boolean overflow = false; + ArrayList<Component> children = mChildrenComponents; + if (hasPriorities) { + // TODO: We need to cache this. + children = + CollapsiblePriority.sortWithPriorities( + mChildrenComponents, CollapsiblePriority.HORIZONTAL); + } + for (Component child : children) { ComponentMeasure childMeasure = measure.get(child); - int visibility = childMeasure.getVisibility(); - childMeasure.clearVisibilityOverride(); - if (!childMeasure.isVisible()) { + if (overflow || childMeasure.isGone()) { + childMeasure.addVisibilityOverride(Visibility.OVERRIDE_GONE); continue; } - if (childrenWidth + childMeasure.getW() > selfWidth - && childrenHeight + childMeasure.getH() > selfHeight) { + float childWidth = childMeasure.getW(); + boolean childDoesNotFits = childrenWidth + childWidth > maxWidth; + if (childDoesNotFits) { childMeasure.addVisibilityOverride(Visibility.OVERRIDE_GONE); - if (visibility != childMeasure.getVisibility()) { - changedVisibility = true; - } + overflow = true; } else { - childrenWidth += childMeasure.getW(); + childrenWidth += childWidth; childrenHeight = Math.max(childrenHeight, childMeasure.getH()); visibleChildren++; } } - if (visibleChildren == 0) { + if (horizontalWrap && size != null) { + size.setWidth(Math.min(maxWidth, childrenWidth)); + } + if (visibleChildren == 0 || (size != null && size.getWidth() <= 0f)) { self.addVisibilityOverride(Visibility.OVERRIDE_GONE); } - return changedVisibility; } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java index cda90c2d3b0b..9566242ccbc5 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java @@ -33,6 +33,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.measure. import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass; import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightInModifierOperation; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ScrollModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.utils.DebugLog; import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; @@ -372,6 +373,17 @@ public class ColumnLayout extends LayoutManager { DebugLog.e(); } + @Override + public void getLocationInWindow(@NonNull float[] value, boolean forSelf) { + super.getLocationInWindow(value, forSelf); + + if (!forSelf && mVerticalScrollDelegate instanceof ScrollModifierOperation) { + ScrollModifierOperation smo = (ScrollModifierOperation) mVerticalScrollDelegate; + + value[1] += smo.getScrollY(); + } + } + /** * The name of the class * diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java index 5b66b95cf1dd..eb10ead34781 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/LayoutManager.java @@ -226,9 +226,17 @@ public abstract class LayoutManager extends LayoutComponent implements Measurabl measure, mCachedWrapSize); float w = mCachedWrapSize.getWidth(); - computeSize(context, 0f, w, 0, measuredHeight, measure); if (hasHorizontalScroll()) { + computeSize(context, 0f, w, 0, measuredHeight, measure); mComponentModifiers.setHorizontalScrollDimension(measuredWidth, w); + } else { + computeSize( + context, + 0f, + Math.min(measuredWidth, insetMaxWidth), + 0, + Math.min(measuredHeight, insetMaxHeight), + measure); } } else if (hasVerticalIntrinsicDimension()) { mCachedWrapSize.setWidth(0f); @@ -236,9 +244,17 @@ public abstract class LayoutManager extends LayoutComponent implements Measurabl computeWrapSize( context, maxWidth, Float.MAX_VALUE, false, false, measure, mCachedWrapSize); float h = mCachedWrapSize.getHeight(); - computeSize(context, 0f, measuredWidth, 0, h, measure); if (hasVerticalScroll()) { + computeSize(context, 0f, measuredWidth, 0, h, measure); mComponentModifiers.setVerticalScrollDimension(measuredHeight, h); + } else { + computeSize( + context, + 0f, + Math.min(measuredWidth, insetMaxWidth), + 0, + Math.min(measuredHeight, insetMaxHeight), + measure); } } else { float maxChildWidth = measuredWidth - mPaddingLeft - mPaddingRight; diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java index d5d2e03c3f2a..15b54a3ce994 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java @@ -32,6 +32,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.LayoutCo import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure; import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass; import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ScrollModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.WidthInModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.utils.DebugLog; import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; @@ -386,6 +387,17 @@ public class RowLayout extends LayoutManager { DebugLog.e(); } + @Override + public void getLocationInWindow(@NonNull float[] value, boolean forSelf) { + super.getLocationInWindow(value, forSelf); + + if (!forSelf && mHorizontalScrollDelegate instanceof ScrollModifierOperation) { + ScrollModifierOperation smo = (ScrollModifierOperation) mHorizontalScrollDelegate; + + value[0] += smo.getScrollX(); + } + } + /** * The name of the class * diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java index d383ee9e4fc9..120c740eccda 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java @@ -77,6 +77,7 @@ public class TextLayout extends LayoutManager implements VariableSupport, Access private final Size mCachedSize = new Size(0f, 0f); @Nullable private String mCachedString = ""; + @Nullable private String mNewString; Platform.ComputedTextLayout mComputedTextLayout; @@ -99,7 +100,7 @@ public class TextLayout extends LayoutManager implements VariableSupport, Access if (cachedString != null && cachedString.equalsIgnoreCase(mCachedString)) { return; } - mCachedString = cachedString; + mNewString = cachedString; if (mType == -1) { if (mFontFamilyId != -1) { String fontFamily = context.getText(mFontFamilyId); @@ -119,8 +120,6 @@ public class TextLayout extends LayoutManager implements VariableSupport, Access mType = 0; } } - mTextW = -1; - mTextH = -1; if (mHorizontalScrollDelegate != null) { mHorizontalScrollDelegate.reset(); @@ -351,6 +350,9 @@ public class TextLayout extends LayoutManager implements VariableSupport, Access mPaint.setColor(mColor); context.replacePaint(mPaint); float[] bounds = new float[4]; + if (mNewString != null && !mNewString.equals(mCachedString)) { + mCachedString = mNewString; + } if (mCachedString == null) { return; } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/CollapsiblePriorityModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/CollapsiblePriorityModifierOperation.java new file mode 100644 index 000000000000..b1f2d2d35b93 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/CollapsiblePriorityModifierOperation.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.operations.layout.modifiers; + +import android.annotation.NonNull; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; +import com.android.internal.widget.remotecompose.core.serialize.SerializeTags; + +import java.util.List; + +/** Set an optional priority on a component within a collapsible layout */ +public class CollapsiblePriorityModifierOperation extends Operation + implements ModifierOperation, Serializable { + private static final int OP_CODE = Operations.MODIFIER_COLLAPSIBLE_PRIORITY; + public static final String CLASS_NAME = "CollapsiblePriorityModifierOperation"; + + private float mPriority; + private int mOrientation; + + public CollapsiblePriorityModifierOperation(int orientation, float priority) { + mOrientation = orientation; + mPriority = priority; + } + + public float getPriority() { + return mPriority; + } + + public int getOrientation() { + return mOrientation; + } + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer, mOrientation, mPriority); + } + + @Override + public void apply(@NonNull RemoteContext context) { + // nothing + } + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return ""; + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + int orientation = buffer.readInt(); + float priority = buffer.readFloat(); + operations.add(new CollapsiblePriorityModifierOperation(orientation, priority)); + } + + /** + * The OP_CODE for this command + * + * @return the opcode + */ + public static int id() { + return OP_CODE; + } + + /** + * The name of the class + * + * @return the name + */ + @NonNull + public static String name() { + return CLASS_NAME; + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Layout Operations", OP_CODE, "CollapsiblePriorityModifier") + .description("Add additional priority to children of Collapsible layouts") + .field(DocumentedOperation.INT, "orientation", "Horizontal(0) or Vertical (1)") + .field(DocumentedOperation.FLOAT, "priority", "The associated priority"); + } + + /** + * Writes out the CollapsiblePriorityModifier to the buffer + * + * @param buffer buffer to write to + * @param priority priority value + * @param orientation orientation (HORIZONTAL or VERTICAL) + */ + public static void apply(@NonNull WireBuffer buffer, int orientation, float priority) { + buffer.start(OP_CODE); + buffer.writeInt(orientation); + buffer.writeFloat(priority); + } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .addTags(SerializeTags.MODIFIER) + .addType(name()) + .add("orientation", mOrientation) + .add("priority", mPriority); + } + + @Override + public void serializeToString(int indent, @NonNull StringSerializer serializer) { + serializer.append(indent, "PRIORITY = [" + getPriority() + "] (" + mOrientation + ")"); + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java index 3e1f32de66e4..42692f95fcda 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java @@ -430,9 +430,35 @@ public class ScrollModifierOperation extends ListActionsOperation } @Override - public boolean showOnScreen(RemoteContext context, int childId) { - // TODO correct this when we trust the bounds in parent - return scrollByOffset(context, -1000) != 0; + public boolean scrollDirection(RemoteContext context, ScrollDirection direction) { + float offset = mHostDimension * 0.7f; + + if (direction == ScrollDirection.FORWARD + || direction == ScrollDirection.DOWN + || direction == ScrollDirection.RIGHT) { + offset *= -1; + } + + return scrollByOffset(context, (int) offset) != 0; + } + + @Override + public boolean showOnScreen(RemoteContext context, Component child) { + float[] locationInWindow = new float[2]; + child.getLocationInWindow(locationInWindow); + + int offset = 0; + if (handlesVerticalScroll()) { + offset = (int) -locationInWindow[1]; + } else { + offset = (int) -locationInWindow[0]; + } + + if (offset == 0) { + return true; + } else { + return scrollByOffset(context, offset) != 0; + } } @Nullable diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringUtils.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringUtils.java index a95a175d0edd..120c7ac9efbf 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringUtils.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/StringUtils.java @@ -35,7 +35,10 @@ public class StringUtils { @NonNull public static String floatToString( float value, int beforeDecimalPoint, int afterDecimalPoint, char pre, char post) { - + boolean isNeg = value < 0; + if (isNeg) { + value = -value; + } int integerPart = (int) value; float fractionalPart = value % 1; @@ -54,14 +57,13 @@ public class StringUtils { integerPartString = integerPartString.substring(iLen - beforeDecimalPoint); } if (afterDecimalPoint == 0) { - return integerPartString; + return ((isNeg) ? "-" : "") + integerPartString; } // Convert fractional part to string and pad with zeros for (int i = 0; i < afterDecimalPoint; i++) { fractionalPart *= 10; } - fractionalPart = Math.round(fractionalPart); for (int i = 0; i < afterDecimalPoint; i++) { @@ -87,6 +89,6 @@ public class StringUtils { fact = fact + new String(c); } - return integerPartString + "." + fact; + return ((isNeg) ? "-" : "") + integerPartString + "." + fact; } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/semantics/ScrollableComponent.java b/core/java/com/android/internal/widget/remotecompose/core/semantics/ScrollableComponent.java index 3d1bd12357c9..1610e6332c1c 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/semantics/ScrollableComponent.java +++ b/core/java/com/android/internal/widget/remotecompose/core/semantics/ScrollableComponent.java @@ -18,6 +18,7 @@ package com.android.internal.widget.remotecompose.core.semantics; import android.annotation.Nullable; import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.operations.layout.Component; /** * Interface for components that support scrolling. @@ -48,13 +49,23 @@ public interface ScrollableComponent extends AccessibilitySemantics { } /** + * Scrolls the content in the specified direction. + * + * @param direction the direction to scroll + * @return whether a scroll was possible + */ + default boolean scrollDirection(RemoteContext context, ScrollDirection direction) { + return false; + } + + /** * Show a child with the given ID on the screen, typically scrolling so it's fully on screen. * - * @param childId The ID of the child to check for visibility. + * @param child The child (including nested) to check for visibility. * @return {@code true} if the child with the given ID could be shown on screen; {@code false} * otherwise. */ - default boolean showOnScreen(RemoteContext context, int childId) { + default boolean showOnScreen(RemoteContext context, Component child) { return false; } @@ -108,4 +119,13 @@ public interface ScrollableComponent extends AccessibilitySemantics { return mCanScrollBackwards; } } + + enum ScrollDirection { + FORWARD, + BACKWARD, + UP, + DOWN, + LEFT, + RIGHT, + } } diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java index e1f2924021a4..575a6b2ee518 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java +++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java @@ -22,7 +22,6 @@ import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.TouchListener; import com.android.internal.widget.remotecompose.core.VariableSupport; @@ -43,7 +42,6 @@ import java.util.HashMap; * * <p>This is used to play the RemoteCompose operations on Android. */ -@VisibleForTesting public class AndroidRemoteContext extends RemoteContext { public void useCanvas(Canvas canvas) { diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java index 0bc99abc17bc..17f4fc82af5f 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java +++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java @@ -102,6 +102,7 @@ public class RemoteComposeCanvas extends FrameLayout implements View.OnAttachSta mDocument = value; mDocument.initializeContext(mARContext); mDisable = false; + mARContext.setDocLoadTime(); mARContext.setAnimationEnabled(true); mARContext.setDensity(mDensity); mARContext.setUseChoreographer(true); |