• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
17 
18 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
19 
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 
23 import com.android.internal.widget.remotecompose.core.CoreDocument;
24 import com.android.internal.widget.remotecompose.core.Operation;
25 import com.android.internal.widget.remotecompose.core.Operations;
26 import com.android.internal.widget.remotecompose.core.PaintContext;
27 import com.android.internal.widget.remotecompose.core.RemoteContext;
28 import com.android.internal.widget.remotecompose.core.VariableSupport;
29 import com.android.internal.widget.remotecompose.core.WireBuffer;
30 import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
31 import com.android.internal.widget.remotecompose.core.operations.TouchExpression;
32 import com.android.internal.widget.remotecompose.core.operations.Utils;
33 import com.android.internal.widget.remotecompose.core.operations.layout.Component;
34 import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent;
35 import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent;
36 import com.android.internal.widget.remotecompose.core.operations.layout.ListActionsOperation;
37 import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent;
38 import com.android.internal.widget.remotecompose.core.operations.layout.ScrollDelegate;
39 import com.android.internal.widget.remotecompose.core.operations.layout.TouchHandler;
40 import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
41 import com.android.internal.widget.remotecompose.core.semantics.ScrollableComponent;
42 import com.android.internal.widget.remotecompose.core.serialize.MapSerializer;
43 import com.android.internal.widget.remotecompose.core.serialize.SerializeTags;
44 
45 import java.util.List;
46 
47 /** Represents a scroll modifier. */
48 public class ScrollModifierOperation extends ListActionsOperation
49         implements TouchHandler,
50                 DecoratorComponent,
51                 ScrollDelegate,
52                 VariableSupport,
53                 ScrollableComponent {
54     private static final int OP_CODE = Operations.MODIFIER_SCROLL;
55     public static final String CLASS_NAME = "ScrollModifierOperation";
56 
57     private final float mPositionExpression;
58     private final float mMax;
59     private final float mNotchMax;
60 
61     int mDirection;
62 
63     float mTouchDownX;
64     float mTouchDownY;
65 
66     float mInitialScrollX;
67     float mInitialScrollY;
68 
69     float mScrollX;
70     float mScrollY;
71 
72     float mMaxScrollX;
73     float mMaxScrollY;
74 
75     float mHostDimension;
76     float mContentDimension;
77 
78     private TouchExpression mTouchExpression;
79 
ScrollModifierOperation(int direction, float position, float max, float notchMax)80     public ScrollModifierOperation(int direction, float position, float max, float notchMax) {
81         super("SCROLL_MODIFIER");
82         this.mDirection = direction;
83         this.mPositionExpression = position;
84         this.mMax = max;
85         this.mNotchMax = notchMax;
86     }
87 
88     /**
89      * Inflate the operation
90      *
91      * @param component
92      */
inflate(Component component)93     public void inflate(Component component) {
94         for (Operation op : mList) {
95             if (op instanceof TouchExpression) {
96                 mTouchExpression = (TouchExpression) op;
97                 mTouchExpression.setComponent(component);
98             }
99         }
100     }
101 
102     @Override
registerListening(@onNull RemoteContext context)103     public void registerListening(@NonNull RemoteContext context) {
104         if (mTouchExpression != null) {
105             mTouchExpression.registerListening(context);
106         }
107     }
108 
109     @Override
updateVariables(@onNull RemoteContext context)110     public void updateVariables(@NonNull RemoteContext context) {
111         if (mTouchExpression != null) {
112             mTouchExpression.updateVariables(context);
113         }
114     }
115 
isVerticalScroll()116     public boolean isVerticalScroll() {
117         return mDirection == 0;
118     }
119 
isHorizontalScroll()120     public boolean isHorizontalScroll() {
121         return mDirection != 0;
122     }
123 
getScrollX()124     public float getScrollX() {
125         return mScrollX;
126     }
127 
getScrollY()128     public float getScrollY() {
129         return mScrollY;
130     }
131 
132     @Override
apply(RemoteContext context)133     public void apply(RemoteContext context) {
134         RootLayoutComponent root = context.getDocument().getRootLayoutComponent();
135         if (root != null) {
136             root.setHasTouchListeners(true);
137         }
138         super.apply(context);
139     }
140 
141     @Override
write(WireBuffer buffer)142     public void write(WireBuffer buffer) {
143         apply(buffer, mDirection, mPositionExpression, mMax, mNotchMax);
144     }
145 
146     /**
147      * Serialize the string
148      *
149      * @param indent padding to display
150      * @param serializer append the string
151      */
152     // @Override
serializeToString(int indent, StringSerializer serializer)153     public void serializeToString(int indent, StringSerializer serializer) {
154         serializer.append(indent, "SCROLL = [" + mDirection + "]");
155     }
156 
157     @NonNull
158     @Override
deepToString(@onNull String indent)159     public String deepToString(@NonNull String indent) {
160         return (indent != null ? indent : "") + toString();
161     }
162 
163     @Override
paint(PaintContext context)164     public void paint(PaintContext context) {
165         for (Operation op : mList) {
166             op.apply(context.getContext());
167         }
168         if (mTouchExpression == null) {
169             return;
170         }
171         float position =
172                 context.getContext()
173                         .mRemoteComposeState
174                         .getFloat(Utils.idFromNan(mPositionExpression));
175 
176         if (mDirection == 0) {
177             mScrollY = -position;
178         } else {
179             mScrollX = -position;
180         }
181     }
182 
183     @Override
toString()184     public String toString() {
185         return "ScrollModifierOperation(" + mDirection + ")";
186     }
187 
188     /**
189      * The name of the class
190      *
191      * @return the name
192      */
193     @NonNull
name()194     public static String name() {
195         return CLASS_NAME;
196     }
197 
198     /**
199      * The OP_CODE for this command
200      *
201      * @return the opcode
202      */
id()203     public static int id() {
204         return OP_CODE;
205     }
206 
207     /**
208      * Write the operation to the buffer
209      *
210      * @param buffer a WireBuffer
211      * @param direction direction of the scroll (HORIZONTAL, VERTICAL)
212      * @param position the current position
213      * @param max the maximum position
214      * @param notchMax the maximum notch
215      */
apply( WireBuffer buffer, int direction, float position, float max, float notchMax)216     public static void apply(
217             WireBuffer buffer, int direction, float position, float max, float notchMax) {
218         buffer.start(OP_CODE);
219         buffer.writeInt(direction);
220         buffer.writeFloat(position);
221         buffer.writeFloat(max);
222         buffer.writeFloat(notchMax);
223     }
224 
225     /**
226      * Read this operation and add it to the list of operations
227      *
228      * @param buffer the buffer to read
229      * @param operations the list of operations that will be added to
230      */
read(WireBuffer buffer, List<Operation> operations)231     public static void read(WireBuffer buffer, List<Operation> operations) {
232         int direction = buffer.readInt();
233         float position = buffer.readFloat();
234         float max = buffer.readFloat();
235         float notchMax = buffer.readFloat();
236         operations.add(new ScrollModifierOperation(direction, position, max, notchMax));
237     }
238 
239     /**
240      * Populate the documentation with a description of this operation
241      *
242      * @param doc to append the description to.
243      */
documentation(DocumentationBuilder doc)244     public static void documentation(DocumentationBuilder doc) {
245         doc.operation("Modifier Operations", OP_CODE, CLASS_NAME)
246                 .description("define a Scroll Modifier")
247                 .field(INT, "direction", "");
248     }
249 
getMaxScrollPosition(Component component, int direction)250     private float getMaxScrollPosition(Component component, int direction) {
251         if (component instanceof LayoutComponent) {
252             LayoutComponent layoutComponent = (LayoutComponent) component;
253             int numChildren = layoutComponent.getChildrenComponents().size();
254             if (numChildren > 0) {
255                 Component lastChild = layoutComponent.getChildrenComponents().get(numChildren - 1);
256                 if (direction == 0) { // VERTICAL
257                     return lastChild.getY();
258                 } else {
259                     return lastChild.getX();
260                 }
261             }
262         }
263         return 0f;
264     }
265 
266     @Override
layout(RemoteContext context, Component component, float width, float height)267     public void layout(RemoteContext context, Component component, float width, float height) {
268         mWidth = width;
269         mHeight = height;
270         float max = mMaxScrollY;
271         if (mDirection != 0) { // HORIZONTAL
272             max = mMaxScrollX;
273         }
274         if (mTouchExpression != null) {
275             float maxScrollPosition = getMaxScrollPosition(component, mDirection);
276             if (maxScrollPosition > 0) {
277                 max = maxScrollPosition;
278             }
279         }
280         context.loadFloat(Utils.idFromNan(mMax), max);
281         context.loadFloat(Utils.idFromNan(mNotchMax), mContentDimension);
282     }
283 
284     @Override
onTouchDown( RemoteContext context, CoreDocument document, Component component, float x, float y)285     public void onTouchDown(
286             RemoteContext context, CoreDocument document, Component component, float x, float y) {
287         mTouchDownX = x;
288         mTouchDownY = y;
289         mInitialScrollX = mScrollX;
290         mInitialScrollY = mScrollY;
291         if (mTouchExpression != null) {
292             mTouchExpression.updateVariables(context);
293             mTouchExpression.touchDown(context, x + mScrollX, y + mScrollY);
294         }
295         document.appliedTouchOperation(component);
296     }
297 
298     @Override
onTouchUp( RemoteContext context, CoreDocument document, Component component, float x, float y, float dx, float dy)299     public void onTouchUp(
300             RemoteContext context,
301             CoreDocument document,
302             Component component,
303             float x,
304             float y,
305             float dx,
306             float dy) {
307         if (mTouchExpression != null) {
308             mTouchExpression.updateVariables(context);
309             mTouchExpression.touchUp(context, x + mScrollX, y + mScrollY, dx, dy);
310         }
311         // If not using touch expression, should add velocity decay here
312     }
313 
314     @Override
onTouchDrag( RemoteContext context, CoreDocument document, Component component, float x, float y)315     public void onTouchDrag(
316             RemoteContext context, CoreDocument document, Component component, float x, float y) {
317         if (mTouchExpression != null) {
318             mTouchExpression.updateVariables(context);
319             mTouchExpression.touchDrag(context, x + mScrollX, y + mScrollY);
320         }
321         float dx = x - mTouchDownX;
322         float dy = y - mTouchDownY;
323 
324         if (!Utils.isVariable(mPositionExpression)) {
325             if (mDirection == 0) {
326                 mScrollY = Math.max(-mMaxScrollY, Math.min(0, mInitialScrollY + dy));
327             } else {
328                 mScrollX = Math.max(-mMaxScrollX, Math.min(0, mInitialScrollX + dx));
329             }
330         }
331     }
332 
333     @Override
onTouchCancel( RemoteContext context, CoreDocument document, Component component, float x, float y)334     public void onTouchCancel(
335             RemoteContext context, CoreDocument document, Component component, float x, float y) {}
336 
337     /**
338      * Set the horizontal scroll dimension
339      *
340      * @param hostDimension the horizontal host dimension
341      * @param contentDimension the horizontal content dimension
342      */
setHorizontalScrollDimension(float hostDimension, float contentDimension)343     public void setHorizontalScrollDimension(float hostDimension, float contentDimension) {
344         mHostDimension = hostDimension;
345         mContentDimension = contentDimension;
346         mMaxScrollX = contentDimension - hostDimension;
347     }
348 
349     /**
350      * Set the vertical scroll dimension
351      *
352      * @param hostDimension the vertical host dimension
353      * @param contentDimension the vertical content dimension
354      */
setVerticalScrollDimension(float hostDimension, float contentDimension)355     public void setVerticalScrollDimension(float hostDimension, float contentDimension) {
356         mHostDimension = hostDimension;
357         mContentDimension = contentDimension;
358         mMaxScrollY = contentDimension - hostDimension;
359     }
360 
getContentDimension()361     public float getContentDimension() {
362         return mContentDimension;
363     }
364 
365     @Override
getScrollX(float currentValue)366     public float getScrollX(float currentValue) {
367         if (mDirection == 1) {
368             return mScrollX;
369         }
370         return 0f;
371     }
372 
373     @Override
getScrollY(float currentValue)374     public float getScrollY(float currentValue) {
375         if (mDirection == 0) {
376             return mScrollY;
377         }
378         return 0f;
379     }
380 
381     @Override
handlesHorizontalScroll()382     public boolean handlesHorizontalScroll() {
383         return mDirection == 1;
384     }
385 
386     @Override
handlesVerticalScroll()387     public boolean handlesVerticalScroll() {
388         return mDirection == 0;
389     }
390 
391     @Override
reset()392     public void reset() {
393         // nothing here for now
394     }
395 
396     @Override
serialize(MapSerializer serializer)397     public void serialize(MapSerializer serializer) {
398         serializer
399                 .addTags(SerializeTags.MODIFIER)
400                 .addType("ScrollModifierOperation")
401                 .add("direction", mDirection)
402                 .add("max", mMax)
403                 .add("notchMax", mNotchMax)
404                 .add("scrollValue", isVerticalScroll() ? mScrollY : mScrollX)
405                 .add("maxScrollValue", isVerticalScroll() ? mMaxScrollY : mMaxScrollX)
406                 .add("contentDimension", mContentDimension)
407                 .add("hostDimension", mHostDimension);
408     }
409 
410     @Override
scrollDirection()411     public int scrollDirection() {
412         if (handlesVerticalScroll()) {
413             return ScrollableComponent.SCROLL_VERTICAL;
414         } else {
415             return ScrollableComponent.SCROLL_HORIZONTAL;
416         }
417     }
418 
419     @Override
scrollByOffset(RemoteContext context, int offset)420     public int scrollByOffset(RemoteContext context, int offset) {
421         // TODO work out how to avoid disabling this
422         mTouchExpression = null;
423 
424         if (handlesVerticalScroll()) {
425             mScrollY = Math.max(-mMaxScrollY, Math.min(0, mScrollY + offset));
426         } else {
427             mScrollX = Math.max(-mMaxScrollX, Math.min(0, mScrollX + offset));
428         }
429         return offset;
430     }
431 
432     @Override
scrollDirection(RemoteContext context, ScrollDirection direction)433     public boolean scrollDirection(RemoteContext context, ScrollDirection direction) {
434         float offset = mHostDimension * 0.7f;
435 
436         if (direction == ScrollDirection.FORWARD
437                 || direction == ScrollDirection.DOWN
438                 || direction == ScrollDirection.RIGHT) {
439             offset *= -1;
440         }
441 
442         return scrollByOffset(context, (int) offset) != 0;
443     }
444 
445     @Override
showOnScreen(RemoteContext context, Component child)446     public boolean showOnScreen(RemoteContext context, Component child) {
447         float[] locationInWindow = new float[2];
448         child.getLocationInWindow(locationInWindow);
449 
450         int offset = 0;
451         if (handlesVerticalScroll()) {
452             offset = (int) -locationInWindow[1];
453         } else {
454             offset = (int) -locationInWindow[0];
455         }
456 
457         if (offset == 0) {
458             return true;
459         } else {
460             return scrollByOffset(context, offset) != 0;
461         }
462     }
463 
464     @Nullable
465     @Override
getScrollAxisRange()466     public ScrollAxisRange getScrollAxisRange() {
467         if (handlesVerticalScroll()) {
468             return new ScrollAxisRange(mScrollY, mMaxScrollY, true, true);
469         } else {
470             return new ScrollAxisRange(mScrollX, mMaxScrollX, true, true);
471         }
472     }
473 }
474