1 /* 2 * Copyright (C) 2023 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; 17 18 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT; 19 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT_ARRAY; 20 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT; 21 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.SHORT; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 26 import com.android.internal.widget.remotecompose.core.Operation; 27 import com.android.internal.widget.remotecompose.core.Operations; 28 import com.android.internal.widget.remotecompose.core.RemoteContext; 29 import com.android.internal.widget.remotecompose.core.TouchListener; 30 import com.android.internal.widget.remotecompose.core.VariableSupport; 31 import com.android.internal.widget.remotecompose.core.WireBuffer; 32 import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; 33 import com.android.internal.widget.remotecompose.core.operations.layout.Component; 34 import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent; 35 import com.android.internal.widget.remotecompose.core.operations.utilities.AnimatedFloatExpression; 36 import com.android.internal.widget.remotecompose.core.operations.utilities.NanMap; 37 import com.android.internal.widget.remotecompose.core.operations.utilities.touch.VelocityEasing; 38 import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; 39 import com.android.internal.widget.remotecompose.core.serialize.Serializable; 40 41 import java.util.Arrays; 42 import java.util.List; 43 44 /** 45 * Operation to deal with Touch handling (typically on canvas) This support handling of many typical 46 * touch behaviours. Including animating to Notched, positions. and tweaking the dynamics of the 47 * animation. 48 */ 49 public class TouchExpression extends Operation 50 implements ComponentData, VariableSupport, TouchListener, Serializable { 51 private static final int OP_CODE = Operations.TOUCH_EXPRESSION; 52 private static final String CLASS_NAME = "TouchExpression"; 53 private float mDefValue; 54 private float mOutDefValue; 55 private int mId; 56 public float[] mSrcExp; 57 int mMode = 1; // 0 = delta, 1 = absolute 58 float mMax = 1; 59 float mMin = 1; 60 float mOutMax = 1; 61 float mOutMin = 1; 62 float mValue = 0; 63 boolean mUnmodified = true; 64 private float[] mPreCalcValue; 65 private float mLastChange = Float.NaN; 66 private float mLastCalculatedValue = Float.NaN; 67 AnimatedFloatExpression mExp = new AnimatedFloatExpression(); 68 69 /** The maximum number of floats in the expression */ 70 public static final int MAX_EXPRESSION_SIZE = 32; 71 72 private VelocityEasing mEasyTouch = new VelocityEasing(); 73 private boolean mEasingToStop = false; 74 private float mTouchUpTime = 0; 75 private float mCurrentValue = Float.NaN; 76 private boolean mTouchDown = false; 77 float mMaxTime = 1; 78 float mMaxAcceleration = 5; 79 float mMaxVelocity = 7; 80 int mStopMode = 0; 81 boolean mWrapMode = false; 82 float[] mNotches; 83 float[] mStopSpec; 84 float[] mOutStopSpec; 85 int mTouchEffects; 86 float mVelocityId; 87 88 /** Stop with some deceleration */ 89 public static final int STOP_GENTLY = 0; 90 91 /** Stop only at the start or end */ 92 public static final int STOP_ENDS = 2; 93 94 /** Stop on touch up */ 95 public static final int STOP_INSTANTLY = 1; 96 97 /** Stop at evenly spaced notches */ 98 public static final int STOP_NOTCHES_EVEN = 3; 99 100 /** Stop at a collection points described in percents of the range */ 101 public static final int STOP_NOTCHES_PERCENTS = 4; 102 103 /** Stop at a collection of point described in absolute cordnates */ 104 public static final int STOP_NOTCHES_ABSOLUTE = 5; 105 106 /** Jump to the absolute poition of the point */ 107 public static final int STOP_ABSOLUTE_POS = 6; 108 109 /** 110 * create a touch expression 111 * 112 * @param id The float id the value is output to 113 * @param exp the expression (containing TOUCH_* ) 114 * @param defValue the default value 115 * @param min the minimum value 116 * @param max the maximum value 117 * @param touchEffects the type of touch mode 118 * @param velocityId the velocity (not used) 119 * @param stopMode the behaviour on touch oup 120 * @param stopSpec the parameters that affect the touch up behaviour 121 * @param easingSpec the easing parameters for coming to a stop 122 */ TouchExpression( int id, float[] exp, float defValue, float min, float max, int touchEffects, float velocityId, int stopMode, float[] stopSpec, float[] easingSpec)123 public TouchExpression( 124 int id, 125 float[] exp, 126 float defValue, 127 float min, 128 float max, 129 int touchEffects, 130 float velocityId, 131 int stopMode, 132 float[] stopSpec, 133 float[] easingSpec) { 134 this.mId = id; 135 this.mSrcExp = exp; 136 mOutDefValue = mDefValue = defValue; 137 mMode = STOP_ABSOLUTE_POS == stopMode ? 1 : 0; 138 mOutMax = mMax = max; 139 if (stopSpec != null) { 140 mOutStopSpec = Arrays.copyOf(stopSpec, stopSpec.length); 141 } 142 mTouchEffects = touchEffects; 143 mVelocityId = velocityId; 144 if (Float.isNaN(min) && Utils.idFromNan(min) == 0) { 145 mWrapMode = true; 146 } else { 147 mOutMin = mMin = min; 148 } 149 mStopMode = stopMode; 150 mStopSpec = stopSpec; 151 if (easingSpec != null) { 152 if (easingSpec.length >= 4) { 153 if (Float.floatToRawIntBits(easingSpec[0]) == 0) { 154 mMaxTime = easingSpec[1]; 155 mMaxAcceleration = easingSpec[2]; 156 mMaxVelocity = easingSpec[3]; 157 } 158 } 159 } 160 } 161 162 @Override updateVariables(RemoteContext context)163 public void updateVariables(RemoteContext context) { 164 if (mPreCalcValue == null || mPreCalcValue.length != mSrcExp.length) { 165 mPreCalcValue = new float[mSrcExp.length]; 166 } 167 if (mOutStopSpec == null || mOutStopSpec.length != mStopSpec.length) { 168 mOutStopSpec = new float[mStopSpec.length]; 169 } 170 if (Float.isNaN(mMax)) { 171 mOutMax = context.getFloat(Utils.idFromNan(mMax)); 172 } 173 if (Float.isNaN(mMin)) { 174 mOutMin = context.getFloat(Utils.idFromNan(mMin)); 175 } 176 if (Float.isNaN(mDefValue)) { 177 mOutDefValue = context.getFloat(Utils.idFromNan(mDefValue)); 178 } 179 180 boolean value_changed = false; 181 for (int i = 0; i < mSrcExp.length; i++) { 182 float v = mSrcExp[i]; 183 if (Float.isNaN(v) 184 && !AnimatedFloatExpression.isMathOperator(v) 185 && !NanMap.isDataVariable(v)) { 186 float newValue = context.getFloat(Utils.idFromNan(v)); 187 188 mPreCalcValue[i] = newValue; 189 190 } else { 191 mPreCalcValue[i] = mSrcExp[i]; 192 } 193 } 194 for (int i = 0; i < mStopSpec.length; i++) { 195 float v = mStopSpec[i]; 196 if (Float.isNaN(v)) { 197 float newValue = context.getFloat(Utils.idFromNan(v)); 198 mOutStopSpec[i] = newValue; 199 } else { 200 mOutStopSpec[i] = v; 201 } 202 } 203 float v = mLastCalculatedValue; 204 if (value_changed) { // inputs changed check if output changed 205 v = mExp.eval(mPreCalcValue, mPreCalcValue.length); 206 if (v != mLastCalculatedValue) { 207 mLastChange = context.getAnimationTime(); 208 mLastCalculatedValue = v; 209 } else { 210 value_changed = false; 211 } 212 } 213 } 214 215 @Override registerListening(RemoteContext context)216 public void registerListening(RemoteContext context) { 217 if (Float.isNaN(mMax)) { 218 context.listensTo(Utils.idFromNan(mMax), this); 219 } 220 if (Float.isNaN(mMin)) { 221 context.listensTo(Utils.idFromNan(mMin), this); 222 } 223 if (Float.isNaN(mDefValue)) { 224 context.listensTo(Utils.idFromNan(mDefValue), this); 225 } 226 if (mComponent == null) { 227 context.addTouchListener(this); 228 } 229 for (float v : mSrcExp) { 230 if (Float.isNaN(v) 231 && !AnimatedFloatExpression.isMathOperator(v) 232 && !NanMap.isDataVariable(v)) { 233 context.listensTo(Utils.idFromNan(v), this); 234 } 235 } 236 for (float v : mStopSpec) { 237 if (Float.isNaN(v)) { 238 context.listensTo(Utils.idFromNan(v), this); 239 } 240 } 241 } 242 wrap(float pos)243 private float wrap(float pos) { 244 if (!mWrapMode) { 245 return pos; 246 } 247 pos = pos % mOutMax; 248 if (pos < 0) { 249 pos += mOutMax; 250 } 251 return pos; 252 } 253 getStopPosition(float pos, float slope)254 private float getStopPosition(float pos, float slope) { 255 float target = pos + slope / 2f; 256 if (mWrapMode) { 257 pos = wrap(pos); 258 target = pos += +slope / 2f; 259 } else { 260 target = Math.max(Math.min(target, mOutMax), mOutMin); 261 } 262 float[] positions = new float[mStopSpec.length]; 263 float min = (mWrapMode) ? 0 : mOutMin; 264 265 switch (mStopMode) { 266 case STOP_ENDS: 267 return ((pos + slope) > (mOutMax + min) / 2) ? mOutMax : min; 268 case STOP_INSTANTLY: 269 return pos; 270 case STOP_NOTCHES_EVEN: 271 int evenSpacing = (int) mOutStopSpec[0]; 272 float notchMax = (mOutStopSpec.length > 1) ? mOutStopSpec[1] : mOutMax; 273 float step = (notchMax - min) / evenSpacing; 274 float notch = min + step * (int) (0.5f + (target - mOutMin) / step); 275 if (!mWrapMode) { 276 notch = Math.max(Math.min(notch, mOutMax), min); 277 } 278 return notch; 279 case STOP_NOTCHES_PERCENTS: 280 positions = new float[mStopSpec.length]; 281 float minPos = min; 282 float minPosDist = Math.abs(mOutMin - target); 283 for (int i = 0; i < positions.length; i++) { 284 float p = mOutMin + mStopSpec[i] * (mOutMax - mOutMin); 285 float dist = Math.abs(p - target); 286 if (minPosDist > dist) { 287 minPosDist = dist; 288 minPos = p; 289 } 290 } 291 return minPos; 292 case STOP_NOTCHES_ABSOLUTE: 293 positions = mStopSpec; 294 minPos = mOutMin; 295 minPosDist = Math.abs(mOutMin - target); 296 for (int i = 0; i < positions.length; i++) { 297 float dist = Math.abs(positions[i] - target); 298 if (minPosDist > dist) { 299 minPosDist = dist; 300 minPos = positions[i]; 301 } 302 } 303 return minPos; 304 case STOP_GENTLY: 305 default: 306 return target; 307 } 308 } 309 haptic(RemoteContext context)310 void haptic(RemoteContext context) { 311 int touch = ((mTouchEffects) & 0xFF); 312 if ((mTouchEffects & (1 << 15)) != 0) { 313 touch = context.getInteger(mTouchEffects & 0x7FFF); 314 } 315 316 context.hapticEffect(touch); 317 } 318 319 float mLastValue = 0; 320 crossNotchCheck(RemoteContext context)321 void crossNotchCheck(RemoteContext context) { 322 float prev = mLastValue; 323 float next = mCurrentValue; 324 mLastValue = next; 325 326 float min = (mWrapMode) ? 0 : mOutMin; 327 float max = mOutMax; 328 329 switch (mStopMode) { 330 case STOP_ENDS: 331 if (((min - prev) * (max - prev) < 0) ^ ((min - next) * (max - next)) < 0) { 332 haptic(context); 333 } 334 break; 335 case STOP_INSTANTLY: 336 haptic(context); 337 break; 338 case STOP_NOTCHES_EVEN: 339 int evenSpacing = (int) mStopSpec[0]; 340 float step = (max - min) / evenSpacing; 341 if ((int) ((prev - min) / step) != (int) ((next - min) / step)) { 342 haptic(context); 343 } 344 break; 345 case STOP_NOTCHES_PERCENTS: 346 for (int i = 0; i < mStopSpec.length; i++) { 347 float p = mOutMin + mStopSpec[i] * (mOutMax - mOutMin); 348 if ((prev - p) * (next - p) < 0) { 349 haptic(context); 350 } 351 } 352 break; 353 case STOP_NOTCHES_ABSOLUTE: 354 for (int i = 0; i < mStopSpec.length; i++) { 355 float p = mStopSpec[i]; 356 if ((prev - p) * (next - p) < 0) { 357 haptic(context); 358 } 359 } 360 break; 361 case STOP_GENTLY: 362 } 363 } 364 365 float mScrLeft, mScrRight, mScrTop, mScrBottom; 366 367 @Nullable Component mComponent; 368 369 /** 370 * Set the component the touch expression is in (if any) 371 * 372 * @param component the component, or null if outside 373 */ 374 @Override setComponent(@ullable Component component)375 public void setComponent(@Nullable Component component) { 376 mComponent = component; 377 if (mComponent != null) { 378 try { 379 RootLayoutComponent root = mComponent.getRoot(); 380 root.setHasTouchListeners(true); 381 } catch (Exception e) { 382 } 383 } 384 } 385 updateBounds()386 private void updateBounds() { 387 Component comp = mComponent; 388 if (comp != null) { 389 float x = comp.getX(); 390 float y = comp.getY(); 391 float w = comp.getWidth(); 392 float h = comp.getHeight(); 393 comp = comp.getParent(); 394 while (comp != null) { 395 x += comp.getX(); 396 y += comp.getY(); 397 comp = comp.getParent(); 398 } 399 mScrLeft = x; 400 mScrTop = y; 401 mScrRight = w + x; 402 mScrBottom = h + y; 403 } 404 } 405 406 @Override apply(RemoteContext context)407 public void apply(RemoteContext context) { 408 updateBounds(); 409 if (mUnmodified) { 410 mCurrentValue = mOutDefValue; 411 context.loadFloat(mId, wrap(mCurrentValue)); 412 return; 413 } 414 if (mEasingToStop) { 415 float time = context.getAnimationTime() - mTouchUpTime; 416 float value = mEasyTouch.getPos(time); 417 mCurrentValue = value; 418 if (mWrapMode) { 419 value = wrap(value); 420 } else { 421 value = Math.min(Math.max(value, mOutMin), mOutMax); 422 } 423 context.loadFloat(mId, value); 424 if (mEasyTouch.getDuration() < time) { 425 mEasingToStop = false; 426 } 427 crossNotchCheck(context); 428 context.needsRepaint(); 429 return; 430 } 431 if (mTouchDown) { 432 float value = 433 mExp.eval(context.getCollectionsAccess(), mPreCalcValue, mPreCalcValue.length); 434 if (mMode == 0) { 435 value = mValueAtDown + (value - mDownTouchValue); 436 } 437 if (mWrapMode) { 438 value = wrap(value); 439 } else { 440 value = Math.min(Math.max(value, mOutMin), mOutMax); 441 } 442 mCurrentValue = value; 443 } 444 crossNotchCheck(context); 445 context.loadFloat(mId, wrap(mCurrentValue)); 446 } 447 448 float mValueAtDown; // The currently "displayed" value at down 449 float mDownTouchValue; // The calculated value at down 450 451 @Override touchDown(RemoteContext context, float x, float y)452 public void touchDown(RemoteContext context, float x, float y) { 453 if (!(x >= mScrLeft && x <= mScrRight && y >= mScrTop && y <= mScrBottom)) { 454 Utils.log("NOT IN WINDOW " + x + ", " + y + " " + mScrLeft + ", " + mScrTop); 455 return; 456 } 457 mEasingToStop = false; 458 mTouchDown = true; 459 mUnmodified = false; 460 if (mMode == 0) { 461 mValueAtDown = context.getFloat(mId); 462 mDownTouchValue = 463 mExp.eval(context.getCollectionsAccess(), mPreCalcValue, mPreCalcValue.length); 464 } 465 context.needsRepaint(); 466 } 467 468 @Override touchUp(RemoteContext context, float x, float y, float dx, float dy)469 public void touchUp(RemoteContext context, float x, float y, float dx, float dy) { 470 // calculate the slope (using small changes) 471 if (!mTouchDown) { 472 return; 473 } 474 mTouchDown = false; 475 float dt = 0.0001f; 476 if (mStopMode == STOP_INSTANTLY) { 477 return; 478 } 479 float v = mExp.eval(context.getCollectionsAccess(), mPreCalcValue, mPreCalcValue.length); 480 for (int i = 0; i < mSrcExp.length; i++) { 481 if (Float.isNaN(mSrcExp[i])) { 482 int id = Utils.idFromNan(mSrcExp[i]); 483 if (id == RemoteContext.ID_TOUCH_POS_X) { 484 mPreCalcValue[i] = x + dx * dt; 485 } else if (id == RemoteContext.ID_TOUCH_POS_Y) { 486 mPreCalcValue[i] = y + dy * dt; 487 } 488 } 489 } 490 float vdt = mExp.eval(context.getCollectionsAccess(), mPreCalcValue, mPreCalcValue.length); 491 float slope = (vdt - v) / dt; // the rate of change with respect to the dx,dy movement 492 float value = context.getFloat(mId); 493 494 mTouchUpTime = context.getAnimationTime(); 495 496 float dest = getStopPosition(value, slope); 497 float time = Math.min(2, mMaxTime * Math.abs(dest - value) / (2 * mMaxVelocity)); 498 mEasyTouch.config(value, dest, slope, time, mMaxAcceleration, mMaxVelocity, null); 499 mEasingToStop = true; 500 context.needsRepaint(); 501 } 502 503 @Override touchDrag(RemoteContext context, float x, float y)504 public void touchDrag(RemoteContext context, float x, float y) { 505 if (!mTouchDown) { 506 return; 507 } 508 apply(context); 509 context.needsRepaint(); 510 } 511 512 @Override write(WireBuffer buffer)513 public void write(WireBuffer buffer) { 514 apply( 515 buffer, 516 mId, 517 mValue, 518 mMin, 519 mMax, 520 mVelocityId, 521 mTouchEffects, 522 mSrcExp, 523 mStopMode, 524 mNotches, 525 null); 526 } 527 528 @Override toString()529 public String toString() { 530 String[] labels = new String[mSrcExp.length]; 531 for (int i = 0; i < mSrcExp.length; i++) { 532 if (Float.isNaN(mSrcExp[i])) { 533 labels[i] = "[" + Utils.idStringFromNan(mSrcExp[i]) + "]"; 534 } 535 } 536 if (mPreCalcValue == null) { 537 return CLASS_NAME 538 + "[" 539 + mId 540 + "] = (" 541 + AnimatedFloatExpression.toString(mSrcExp, labels) 542 + ")"; 543 } 544 return CLASS_NAME 545 + "[" 546 + mId 547 + "] = (" 548 + AnimatedFloatExpression.toString(mPreCalcValue, labels) 549 + ")"; 550 } 551 552 // ===================== static ====================== 553 554 /** 555 * The name of the class 556 * 557 * @return the name 558 */ 559 @NonNull name()560 public static String name() { 561 return CLASS_NAME; 562 } 563 564 /** 565 * The OP_CODE for this command 566 * 567 * @return the opcode 568 */ id()569 public static int id() { 570 return OP_CODE; 571 } 572 573 /** 574 * Writes out the operation to the buffer 575 * 576 * @param buffer The buffer to write to 577 * @param id the id of the resulting float 578 * @param value the float expression array 579 * @param min the minimum allowed value 580 * @param max the maximum allowed value 581 * @param velocityId the velocity id 582 * @param touchEffects the type touch effect 583 * @param exp the expression the maps touch drags to movement 584 * @param touchMode the touch mode e.g. notch modes 585 * @param touchSpec the spec of the touch modes 586 * @param easingSpec the spec of when the object comes to an easing 587 */ apply( WireBuffer buffer, int id, float value, float min, float max, float velocityId, int touchEffects, float[] exp, int touchMode, float[] touchSpec, float[] easingSpec)588 public static void apply( 589 WireBuffer buffer, 590 int id, 591 float value, 592 float min, 593 float max, 594 float velocityId, 595 int touchEffects, 596 float[] exp, 597 int touchMode, 598 float[] touchSpec, 599 float[] easingSpec) { 600 buffer.start(OP_CODE); 601 buffer.writeInt(id); 602 buffer.writeFloat(value); 603 buffer.writeFloat(min); 604 buffer.writeFloat(max); 605 buffer.writeFloat(velocityId); 606 buffer.writeInt(touchEffects); 607 buffer.writeInt(exp.length); 608 for (float v : exp) { 609 buffer.writeFloat(v); 610 } 611 int len = 0; 612 if (touchSpec != null) { 613 len = touchSpec.length; 614 } 615 buffer.writeInt((touchMode << 16) | len); 616 for (int i = 0; i < len; i++) { 617 buffer.writeFloat(touchSpec[i]); 618 } 619 620 if (easingSpec != null) { 621 len = easingSpec.length; 622 } else { 623 len = 0; 624 } 625 buffer.writeInt(len); 626 for (int i = 0; i < len; i++) { 627 buffer.writeFloat(easingSpec[i]); 628 } 629 } 630 631 /** 632 * Read this operation and add it to the list of operations 633 * 634 * @param buffer the buffer to read 635 * @param operations the list of operations that will be added to 636 */ read(@onNull WireBuffer buffer, @NonNull List<Operation> operations)637 public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { 638 int id = buffer.readInt(); 639 float startValue = buffer.readFloat(); 640 float min = buffer.readFloat(); 641 float max = buffer.readFloat(); 642 float velocityId = buffer.readFloat(); // TODO future support 643 int touchEffects = buffer.readInt(); 644 int len = buffer.readInt(); 645 int valueLen = len & 0xFFFF; 646 if (valueLen > MAX_EXPRESSION_SIZE) { 647 throw new RuntimeException("Float expression to long"); 648 } 649 float[] exp = new float[valueLen]; 650 for (int i = 0; i < exp.length; i++) { 651 exp[i] = buffer.readFloat(); 652 } 653 int stopLogic = buffer.readInt(); 654 int stopLen = stopLogic & 0xFFFF; 655 int stopMode = stopLogic >> 16; 656 657 float[] stopsData = new float[stopLen]; 658 for (int i = 0; i < stopsData.length; i++) { 659 stopsData[i] = buffer.readFloat(); 660 } 661 int easingLen = buffer.readInt(); 662 663 float[] easingData = new float[easingLen]; 664 for (int i = 0; i < easingData.length; i++) { 665 easingData[i] = buffer.readFloat(); 666 } 667 668 operations.add( 669 new TouchExpression( 670 id, 671 exp, 672 startValue, 673 min, 674 max, 675 touchEffects, 676 velocityId, 677 stopMode, 678 stopsData, 679 easingData)); 680 } 681 682 /** 683 * Populate the documentation with a description of this operation 684 * 685 * @param doc to append the description to. 686 */ documentation(DocumentationBuilder doc)687 public static void documentation(DocumentationBuilder doc) { 688 doc.operation("Expressions Operations", OP_CODE, CLASS_NAME) 689 .description("A Float expression") 690 .field(INT, "id", "The id of the Color") 691 .field(SHORT, "expression_length", "expression length") 692 .field(SHORT, "animation_length", "animation description length") 693 .field( 694 FLOAT_ARRAY, 695 "expression", 696 "expression_length", 697 "Sequence of Floats representing and expression") 698 .field( 699 FLOAT_ARRAY, 700 "AnimationSpec", 701 "animation_length", 702 "Sequence of Floats representing animation curve") 703 .field(FLOAT, "duration", "> time in sec") 704 .field(INT, "bits", "> WRAP|INITALVALUE | TYPE ") 705 .field(FLOAT_ARRAY, "spec", "> [SPEC PARAMETERS] ") 706 .field(FLOAT, "initialValue", "> [Initial value] ") 707 .field(FLOAT, "wrapValue", "> [Wrap value] "); 708 } 709 710 @NonNull 711 @Override deepToString(@onNull String indent)712 public String deepToString(@NonNull String indent) { 713 return indent + toString(); 714 } 715 716 @Override serialize(MapSerializer serializer)717 public void serialize(MapSerializer serializer) { 718 serializer 719 .addType(CLASS_NAME) 720 .add("id", mId) 721 .add("defValue", mDefValue, mOutDefValue) 722 .add("min", mMin, mOutMin) 723 .add("max", mMax, mOutMax) 724 .add("mode", mMode) 725 .addFloatExpressionSrc("srcExp", mSrcExp); 726 } 727 } 728