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; 17 18 import android.annotation.NonNull; 19 import android.annotation.Nullable; 20 21 import com.android.internal.widget.remotecompose.core.CoreDocument; 22 import com.android.internal.widget.remotecompose.core.Operation; 23 import com.android.internal.widget.remotecompose.core.Operations; 24 import com.android.internal.widget.remotecompose.core.PaintContext; 25 import com.android.internal.widget.remotecompose.core.PaintOperation; 26 import com.android.internal.widget.remotecompose.core.RemoteContext; 27 import com.android.internal.widget.remotecompose.core.WireBuffer; 28 import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; 29 import com.android.internal.widget.remotecompose.core.operations.TextData; 30 import com.android.internal.widget.remotecompose.core.operations.Utils; 31 import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ModifierOperation; 32 import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle; 33 import com.android.internal.widget.remotecompose.core.operations.utilities.ColorUtils; 34 import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; 35 import com.android.internal.widget.remotecompose.core.operations.utilities.easing.Easing; 36 import com.android.internal.widget.remotecompose.core.operations.utilities.easing.FloatAnimation; 37 import com.android.internal.widget.remotecompose.core.semantics.AccessibleComponent; 38 import com.android.internal.widget.remotecompose.core.semantics.CoreSemantics; 39 import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; 40 import com.android.internal.widget.remotecompose.core.serialize.SerializeTags; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 45 /** Represents a click modifier + actions */ 46 public class ClickModifierOperation extends PaintOperation 47 implements Container, 48 ModifierOperation, 49 DecoratorComponent, 50 ClickHandler, 51 AccessibleComponent { 52 private static final int OP_CODE = Operations.MODIFIER_CLICK; 53 54 long mAnimateRippleStart = 0; 55 float mAnimateRippleX = 0f; 56 float mAnimateRippleY = 0f; 57 int mAnimateRippleDuration = 1000; 58 59 float mWidth = 0; 60 float mHeight = 0; 61 62 @NonNull public float[] locationInWindow = new float[2]; 63 64 @NonNull PaintBundle mPaint = new PaintBundle(); 65 66 @Override isClickable()67 public boolean isClickable() { 68 return true; 69 } 70 71 @Nullable 72 @Override getRole()73 public Role getRole() { 74 return Role.BUTTON; 75 } 76 77 @Override getMode()78 public CoreSemantics.Mode getMode() { 79 return CoreSemantics.Mode.MERGE; 80 } 81 82 /** 83 * Animate ripple 84 * 85 * @param x starting position x of the ripple 86 * @param y starting position y of the ripple 87 */ animateRipple(float x, float y)88 public void animateRipple(float x, float y) { 89 mAnimateRippleStart = System.currentTimeMillis(); 90 mAnimateRippleX = x; 91 mAnimateRippleY = y; 92 } 93 94 @NonNull public ArrayList<Operation> mList = new ArrayList<>(); 95 96 @NonNull getList()97 public ArrayList<Operation> getList() { 98 return mList; 99 } 100 101 @Override write(@onNull WireBuffer buffer)102 public void write(@NonNull WireBuffer buffer) { 103 apply(buffer); 104 } 105 106 @NonNull 107 @Override toString()108 public String toString() { 109 return "ClickModifier"; 110 } 111 112 @Override apply(@onNull RemoteContext context)113 public void apply(@NonNull RemoteContext context) { 114 RootLayoutComponent root = context.getDocument().getRootLayoutComponent(); 115 if (root != null) { 116 root.setHasTouchListeners(true); 117 } 118 for (Operation op : mList) { 119 if (op instanceof TextData) { 120 op.apply(context); 121 context.incrementOpCount(); 122 } 123 } 124 } 125 126 @NonNull 127 @Override deepToString(@onNull String indent)128 public String deepToString(@NonNull String indent) { 129 return (indent != null ? indent : "") + toString(); 130 } 131 132 @Override paint(@onNull PaintContext context)133 public void paint(@NonNull PaintContext context) { 134 if (mAnimateRippleStart == 0) { 135 return; 136 } 137 context.needsRepaint(); 138 139 float progress = (System.currentTimeMillis() - mAnimateRippleStart); 140 progress /= (float) mAnimateRippleDuration; 141 if (progress > 1f) { 142 mAnimateRippleStart = 0; 143 } 144 progress = Math.min(1f, progress); 145 context.save(); 146 context.savePaint(); 147 mPaint.reset(); 148 149 FloatAnimation anim1 = 150 new FloatAnimation(Easing.CUBIC_STANDARD, 1f, null, Float.NaN, Float.NaN); 151 anim1.setInitialValue(0f); 152 anim1.setTargetValue(1f); 153 float tween = anim1.get(progress); 154 155 FloatAnimation anim2 = 156 new FloatAnimation(Easing.CUBIC_STANDARD, 0.5f, null, Float.NaN, Float.NaN); 157 anim2.setInitialValue(0f); 158 anim2.setTargetValue(1f); 159 float tweenRadius = anim2.get(progress); 160 161 int startColor = ColorUtils.createColor(250, 250, 250, 180); 162 int endColor = ColorUtils.createColor(200, 200, 200, 0); 163 int paintedColor = Utils.interpolateColor(startColor, endColor, tween); 164 165 float radius = Math.max(mWidth, mHeight) * tweenRadius; 166 mPaint.setColor(paintedColor); 167 context.applyPaint(mPaint); 168 context.clipRect(0f, 0f, mWidth, mHeight); 169 context.drawCircle(mAnimateRippleX, mAnimateRippleY, radius); 170 context.restorePaint(); 171 context.restore(); 172 } 173 174 @Override layout( @onNull RemoteContext context, Component component, float width, float height)175 public void layout( 176 @NonNull RemoteContext context, Component component, float width, float height) { 177 mWidth = width; 178 mHeight = height; 179 } 180 181 @Override serializeToString(int indent, @NonNull StringSerializer serializer)182 public void serializeToString(int indent, @NonNull StringSerializer serializer) { 183 serializer.append(indent, "CLICK_MODIFIER"); 184 for (Operation o : mList) { 185 if (o instanceof ActionOperation) { 186 ((ActionOperation) o).serializeToString(indent + 1, serializer); 187 } 188 } 189 } 190 191 @Override onClick( @onNull RemoteContext context, @NonNull CoreDocument document, @NonNull Component component, float x, float y)192 public void onClick( 193 @NonNull RemoteContext context, 194 @NonNull CoreDocument document, 195 @NonNull Component component, 196 float x, 197 float y) { 198 if (!component.isVisible()) { 199 return; 200 } 201 locationInWindow[0] = 0f; 202 locationInWindow[1] = 0f; 203 component.getLocationInWindow(locationInWindow); 204 animateRipple(x - locationInWindow[0], y - locationInWindow[1]); 205 for (Operation o : mList) { 206 if (o instanceof ActionOperation) { 207 ((ActionOperation) o).runAction(context, document, component, x, y); 208 } 209 } 210 context.hapticEffect(3); 211 } 212 213 /** 214 * The name of the class 215 * 216 * @return the name 217 */ 218 @NonNull name()219 public static String name() { 220 return "ClickModifier"; 221 } 222 223 /** 224 * Write the operation on the buffer 225 * 226 * @param buffer 227 */ apply(@onNull WireBuffer buffer)228 public static void apply(@NonNull WireBuffer buffer) { 229 buffer.start(OP_CODE); 230 } 231 232 /** 233 * Read this operation and add it to the list of operations 234 * 235 * @param buffer the buffer to read 236 * @param operations the list of operations that will be added to 237 */ read(@onNull WireBuffer buffer, @NonNull List<Operation> operations)238 public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { 239 operations.add(new ClickModifierOperation()); 240 } 241 242 /** 243 * Populate the documentation with a description of this operation 244 * 245 * @param doc to append the description to. 246 */ documentation(@onNull DocumentationBuilder doc)247 public static void documentation(@NonNull DocumentationBuilder doc) { 248 doc.operation("Layout Operations", OP_CODE, name()) 249 .description( 250 "Click modifier. This operation contains" 251 + " a list of action executed on click"); 252 } 253 254 @Override serialize(MapSerializer serializer)255 public void serialize(MapSerializer serializer) { 256 serializer.addTags(SerializeTags.MODIFIER).addType("ClickModifierOperation"); 257 } 258 } 259