• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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