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