1 /*
2  * Copyright (C) 2022 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 
17 package androidx.constraintlayout.core.state;
18 
19 import static androidx.constraintlayout.core.state.ConstraintSetParser.parseColorString;
20 
21 import androidx.annotation.RestrictTo;
22 import androidx.constraintlayout.core.motion.CustomVariable;
23 import androidx.constraintlayout.core.motion.utils.TypedBundle;
24 import androidx.constraintlayout.core.motion.utils.TypedValues;
25 import androidx.constraintlayout.core.parser.CLArray;
26 import androidx.constraintlayout.core.parser.CLContainer;
27 import androidx.constraintlayout.core.parser.CLElement;
28 import androidx.constraintlayout.core.parser.CLKey;
29 import androidx.constraintlayout.core.parser.CLNumber;
30 import androidx.constraintlayout.core.parser.CLObject;
31 import androidx.constraintlayout.core.parser.CLParsingException;
32 
33 import org.jspecify.annotations.NonNull;
34 
35 /**
36  * Contains code for Parsing Transitions
37  */
38 public class TransitionParser {
39     /**
40      * Parse a JSON string of a Transition and insert it into the Transition object
41      *
42      * @deprecated dpToPixel is not necessary, use {@link #parse(CLObject, Transition)} instead.
43      * @param json       Transition Object to parse.
44      * @param transition Transition Object to write transition to
45      */
46     @Deprecated
parse(CLObject json, Transition transition, CorePixelDp dpToPixel)47     public static void parse(CLObject json, Transition transition, CorePixelDp dpToPixel)
48             throws CLParsingException {
49         parse(json, transition);
50     }
51 
52     /**
53      * Parse a JSON string of a Transition and insert it into the Transition object
54      *
55      * @param json       Transition Object to parse.
56      * @param transition Transition Object to write transition to
57      */
58     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
parse(@onNull CLObject json, @NonNull Transition transition)59     public static void parse(@NonNull CLObject json, @NonNull Transition transition)
60             throws CLParsingException {
61         transition.resetProperties();
62         String pathMotionArc = json.getStringOrNull("pathMotionArc");
63         TypedBundle bundle = new TypedBundle();
64         boolean setBundle = false;
65         if (pathMotionArc != null) {
66             setBundle = true;
67             switch (pathMotionArc) {
68                 // TODO use map
69                 case "none":
70                     bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 0);
71                     break;
72                 case "startVertical":
73                     bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 1);
74                     break;
75                 case "startHorizontal":
76                     bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 2);
77                     break;
78                 case "flip":
79                     bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 3);
80                     break;
81                 case "below":
82                     bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 4);
83                     break;
84                 case "above":
85                     bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 5);
86             }
87 
88         }
89         // TODO: Add duration
90         String interpolator = json.getStringOrNull("interpolator");
91         if (interpolator != null) {
92             setBundle = true;
93             bundle.add(TypedValues.TransitionType.TYPE_INTERPOLATOR, interpolator);
94         }
95 
96         float staggered = json.getFloatOrNaN("staggered");
97         if (!Float.isNaN(staggered)) {
98             setBundle = true;
99             bundle.add(TypedValues.TransitionType.TYPE_STAGGERED, staggered);
100         }
101         if (setBundle) {
102             transition.setTransitionProperties(bundle);
103         }
104 
105         CLContainer onSwipe = json.getObjectOrNull("onSwipe");
106 
107         if (onSwipe != null) {
108             parseOnSwipe(onSwipe, transition);
109         }
110         parseKeyFrames(json, transition);
111     }
112 
parseOnSwipe(CLContainer onSwipe, Transition transition)113     private static void parseOnSwipe(CLContainer onSwipe, Transition transition) {
114         String anchor = onSwipe.getStringOrNull("anchor");
115         int side = map(onSwipe.getStringOrNull("side"), Transition.OnSwipe.SIDES);
116         int direction = map(onSwipe.getStringOrNull("direction"),
117                 Transition.OnSwipe.DIRECTIONS);
118         float scale = onSwipe.getFloatOrNaN("scale");
119         float threshold = onSwipe.getFloatOrNaN("threshold");
120         float maxVelocity = onSwipe.getFloatOrNaN("maxVelocity");
121         float maxAccel = onSwipe.getFloatOrNaN("maxAccel");
122         String limitBounds = onSwipe.getStringOrNull("limitBounds");
123         int autoCompleteMode = map(onSwipe.getStringOrNull("mode"), Transition.OnSwipe.MODE);
124         int touchUp = map(onSwipe.getStringOrNull("touchUp"), Transition.OnSwipe.TOUCH_UP);
125         float springMass = onSwipe.getFloatOrNaN("springMass");
126         float springStiffness = onSwipe.getFloatOrNaN("springStiffness");
127         float springDamping = onSwipe.getFloatOrNaN("springDamping");
128         float stopThreshold = onSwipe.getFloatOrNaN("stopThreshold");
129         int springBoundary = map(onSwipe.getStringOrNull("springBoundary"),
130                 Transition.OnSwipe.BOUNDARY);
131         String around = onSwipe.getStringOrNull("around");
132 
133         Transition.OnSwipe swipe = transition.createOnSwipe();
134         swipe.setAnchorId(anchor);
135         swipe.setAnchorSide(side);
136         swipe.setDragDirection(direction);
137         swipe.setDragScale(scale);
138         swipe.setDragThreshold(threshold);
139         swipe.setMaxVelocity(maxVelocity);
140         swipe.setMaxAcceleration(maxAccel);
141         swipe.setLimitBoundsTo(limitBounds);
142         swipe.setAutoCompleteMode(autoCompleteMode);
143         swipe.setOnTouchUp(touchUp);
144         swipe.setSpringMass(springMass);
145         swipe.setSpringStiffness(springStiffness);
146         swipe.setSpringDamping(springDamping);
147         swipe.setSpringStopThreshold(stopThreshold);
148         swipe.setSpringBoundary(springBoundary);
149         swipe.setRotationCenterId(around);
150     }
151 
152 
map(String val, String... types)153     private static int map(String val, String... types) {
154         for (int i = 0; i < types.length; i++) {
155             if (types[i].equals(val)) {
156                 return i;
157             }
158         }
159         return 0;
160     }
161 
map(TypedBundle bundle, int type, String val, String... types)162     private static void map(TypedBundle bundle, int type, String val, String... types) {
163         for (int i = 0; i < types.length; i++) {
164             if (types[i].equals(val)) {
165                 bundle.add(type, i);
166             }
167         }
168     }
169 
170     /**
171      * Parses {@code KeyFrames} attributes from the {@link CLObject} into {@link  Transition}.
172      *
173      * @param transitionCLObject the CLObject for the root transition json
174      * @param transition         core object that holds the state of the Transition
175      */
parseKeyFrames(CLObject transitionCLObject, Transition transition)176     public static void parseKeyFrames(CLObject transitionCLObject, Transition transition)
177             throws CLParsingException {
178         CLContainer keyframes = transitionCLObject.getObjectOrNull("KeyFrames");
179         if (keyframes == null) return;
180         CLArray keyPositions = keyframes.getArrayOrNull("KeyPositions");
181         if (keyPositions != null) {
182             for (int i = 0; i < keyPositions.size(); i++) {
183                 CLElement keyPosition = keyPositions.get(i);
184                 if (keyPosition instanceof CLObject) {
185                     parseKeyPosition((CLObject) keyPosition, transition);
186                 }
187             }
188         }
189         CLArray keyAttributes = keyframes.getArrayOrNull("KeyAttributes");
190         if (keyAttributes != null) {
191             for (int i = 0; i < keyAttributes.size(); i++) {
192                 CLElement keyAttribute = keyAttributes.get(i);
193                 if (keyAttribute instanceof CLObject) {
194                     parseKeyAttribute((CLObject) keyAttribute, transition);
195                 }
196             }
197         }
198         CLArray keyCycles = keyframes.getArrayOrNull("KeyCycles");
199         if (keyCycles != null) {
200             for (int i = 0; i < keyCycles.size(); i++) {
201                 CLElement keyCycle = keyCycles.get(i);
202                 if (keyCycle instanceof CLObject) {
203                     parseKeyCycle((CLObject) keyCycle, transition);
204                 }
205             }
206         }
207     }
208 
209 
parseKeyPosition(CLObject keyPosition, Transition transition)210     private static void parseKeyPosition(CLObject keyPosition,
211             Transition transition) throws CLParsingException {
212         TypedBundle bundle = new TypedBundle();
213         CLArray targets = keyPosition.getArray("target");
214         CLArray frames = keyPosition.getArray("frames");
215         CLArray percentX = keyPosition.getArrayOrNull("percentX");
216         CLArray percentY = keyPosition.getArrayOrNull("percentY");
217         CLArray percentWidth = keyPosition.getArrayOrNull("percentWidth");
218         CLArray percentHeight = keyPosition.getArrayOrNull("percentHeight");
219         String pathMotionArc = keyPosition.getStringOrNull("pathMotionArc");
220         String transitionEasing = keyPosition.getStringOrNull("transitionEasing");
221         String curveFit = keyPosition.getStringOrNull("curveFit");
222         String type = keyPosition.getStringOrNull("type");
223         if (type == null) {
224             type = "parentRelative";
225         }
226         if (percentX != null && frames.size() != percentX.size()) {
227             return;
228         }
229         if (percentY != null && frames.size() != percentY.size()) {
230             return;
231         }
232         for (int i = 0; i < targets.size(); i++) {
233             String target = targets.getString(i);
234             int pos_type = map(type, "deltaRelative", "pathRelative", "parentRelative");
235             bundle.clear();
236             bundle.add(TypedValues.PositionType.TYPE_POSITION_TYPE, pos_type);
237             if (curveFit != null) {
238                 map(bundle, TypedValues.PositionType.TYPE_CURVE_FIT, curveFit,
239                         "spline", "linear");
240             }
241             bundle.addIfNotNull(TypedValues.PositionType.TYPE_TRANSITION_EASING, transitionEasing);
242 
243             if (pathMotionArc != null) {
244                 map(bundle, TypedValues.PositionType.TYPE_PATH_MOTION_ARC, pathMotionArc,
245                         "none", "startVertical", "startHorizontal", "flip", "below", "above");
246             }
247 
248             for (int j = 0; j < frames.size(); j++) {
249                 int frame = frames.getInt(j);
250                 bundle.add(TypedValues.TYPE_FRAME_POSITION, frame);
251                 set(bundle, TypedValues.PositionType.TYPE_PERCENT_X, percentX, j);
252                 set(bundle, TypedValues.PositionType.TYPE_PERCENT_Y, percentY, j);
253                 set(bundle, TypedValues.PositionType.TYPE_PERCENT_WIDTH, percentWidth, j);
254                 set(bundle, TypedValues.PositionType.TYPE_PERCENT_HEIGHT, percentHeight, j);
255 
256                 transition.addKeyPosition(target, bundle);
257             }
258         }
259     }
260 
set(TypedBundle bundle, int type, CLArray array, int index)261     private static void set(TypedBundle bundle, int type,
262             CLArray array, int index) throws CLParsingException {
263         if (array != null) {
264             bundle.add(type, array.getFloat(index));
265         }
266     }
267 
parseKeyAttribute(CLObject keyAttribute, Transition transition)268     private static void parseKeyAttribute(CLObject keyAttribute,
269             Transition transition) throws CLParsingException {
270         CLArray targets = keyAttribute.getArrayOrNull("target");
271         if (targets == null) {
272             return;
273         }
274         CLArray frames = keyAttribute.getArrayOrNull("frames");
275         if (frames == null) {
276             return;
277         }
278         String transitionEasing = keyAttribute.getStringOrNull("transitionEasing");
279         // These present an ordered list of attributes that might be used in a keyCycle
280         String[] attrNames = {
281                 TypedValues.AttributesType.S_SCALE_X,
282                 TypedValues.AttributesType.S_SCALE_Y,
283                 TypedValues.AttributesType.S_TRANSLATION_X,
284                 TypedValues.AttributesType.S_TRANSLATION_Y,
285                 TypedValues.AttributesType.S_TRANSLATION_Z,
286                 TypedValues.AttributesType.S_ROTATION_X,
287                 TypedValues.AttributesType.S_ROTATION_Y,
288                 TypedValues.AttributesType.S_ROTATION_Z,
289                 TypedValues.AttributesType.S_ALPHA
290         };
291         int[] attrIds = {
292                 TypedValues.AttributesType.TYPE_SCALE_X,
293                 TypedValues.AttributesType.TYPE_SCALE_Y,
294                 TypedValues.AttributesType.TYPE_TRANSLATION_X,
295                 TypedValues.AttributesType.TYPE_TRANSLATION_Y,
296                 TypedValues.AttributesType.TYPE_TRANSLATION_Z,
297                 TypedValues.AttributesType.TYPE_ROTATION_X,
298                 TypedValues.AttributesType.TYPE_ROTATION_Y,
299                 TypedValues.AttributesType.TYPE_ROTATION_Z,
300                 TypedValues.AttributesType.TYPE_ALPHA
301         };
302         // if true scale the values from pixels to dp
303         boolean[] scaleTypes = {
304                 false,
305                 false,
306                 true,
307                 true,
308                 true,
309                 false,
310                 false,
311                 false,
312                 false,
313         };
314         TypedBundle[] bundles = new TypedBundle[frames.size()];
315         CustomVariable[][] customVars  = null;
316 
317         for (int i = 0; i < frames.size(); i++) {
318             bundles[i] = new TypedBundle();
319         }
320 
321         for (int k = 0; k < attrNames.length; k++) {
322 
323             String attrName = attrNames[k];
324             int attrId = attrIds[k];
325             boolean scale = scaleTypes[k];
326             CLArray arrayValues = keyAttribute.getArrayOrNull(attrName);
327             // array must contain one per frame
328             if (arrayValues != null && arrayValues.size() != bundles.length) {
329                 throw new CLParsingException(
330                         "incorrect size for " + attrName + " array, "
331                                 + "not matching targets array!", keyAttribute);
332             }
333             if (arrayValues != null) {
334                 for (int i = 0; i < bundles.length; i++) {
335                     float value = arrayValues.getFloat(i);
336                     if (scale) {
337                         value = transition.mToPixel.toPixels(value);
338                     }
339                     bundles[i].add(attrId, value);
340                 }
341             } else {
342                 float value = keyAttribute.getFloatOrNaN(attrName);
343                 if (!Float.isNaN(value)) {
344                     if (scale) {
345                         value = transition.mToPixel.toPixels(value);
346                     }
347                     for (int i = 0; i < bundles.length; i++) {
348                         bundles[i].add(attrId, value);
349                     }
350                 }
351             }
352         }
353         // Support for custom attributes in KeyAttributes
354         CLElement customElement = keyAttribute.getOrNull("custom");
355         if (customElement != null && customElement instanceof CLObject) {
356             CLObject customObj = ((CLObject) customElement);
357             int n = customObj.size();
358             customVars = new CustomVariable[frames.size()][n];
359             for (int i = 0; i < n; i++) {
360                 CLKey key = (CLKey) customObj.get(i);
361                 String customName = key.content();
362                 if (key.getValue() instanceof CLArray) {
363                     CLArray arrayValues = (CLArray) key.getValue();
364                     int vSize = arrayValues.size();
365                     if (vSize == bundles.length && vSize > 0) {
366                         if (arrayValues.get(0) instanceof CLNumber) {
367                             for (int j = 0; j < bundles.length; j++) {
368                                 customVars[j][i] = new CustomVariable(customName,
369                                         TypedValues.Custom.TYPE_FLOAT,
370                                         arrayValues.get(j).getFloat());
371                             }
372                         } else {  // since it is not a number switching to custom color parsing
373                             for (int j = 0; j < bundles.length; j++) {
374                                 long color = parseColorString(arrayValues.get(j).content());
375                                 if (color != -1) {
376                                     customVars[j][i] = new CustomVariable(customName,
377                                             TypedValues.Custom.TYPE_COLOR,
378                                             (int) color);
379                                 }
380                             }
381                         }
382                     }
383                 } else {
384                     CLElement value = key.getValue();
385                     if (value instanceof CLNumber) {
386                         float fValue = value.getFloat();
387                         for (int j = 0; j < bundles.length; j++) {
388                             customVars[j][i] = new CustomVariable(customName,
389                                     TypedValues.Custom.TYPE_FLOAT,
390                                     fValue);
391                         }
392                     } else {
393                         long cValue = parseColorString(value.content());
394                         if (cValue != -1) {
395                             for (int j = 0; j < bundles.length; j++) {
396                                 customVars[j][i] = new CustomVariable(customName,
397                                         TypedValues.Custom.TYPE_COLOR,
398                                         (int) cValue);
399 
400                             }
401                         }
402                     }
403                 }
404 
405             }
406         }
407         String curveFit = keyAttribute.getStringOrNull("curveFit");
408         for (int i = 0; i < targets.size(); i++) {
409             for (int j = 0; j < bundles.length; j++) {
410                 String target = targets.getString(i);
411                 TypedBundle bundle = bundles[j];
412                 if (curveFit != null) {
413                     bundle.add(TypedValues.PositionType.TYPE_CURVE_FIT,
414                             map(curveFit, "spline", "linear"));
415                 }
416                 bundle.addIfNotNull(TypedValues.PositionType.TYPE_TRANSITION_EASING,
417                         transitionEasing);
418                 int frame = frames.getInt(j);
419                 bundle.add(TypedValues.TYPE_FRAME_POSITION, frame);
420                 transition.addKeyAttribute(target, bundle, (customVars != null) ? customVars[j] :
421                         null);
422             }
423         }
424     }
425 
parseKeyCycle(CLObject keyCycleData, Transition transition)426     private static void parseKeyCycle(CLObject keyCycleData,
427             Transition transition) throws CLParsingException {
428         CLArray targets = keyCycleData.getArray("target");
429         CLArray frames = keyCycleData.getArray("frames");
430         String transitionEasing = keyCycleData.getStringOrNull("transitionEasing");
431         // These present an ordered list of attributes that might be used in a keyCycle
432         String[] attrNames = {
433                 TypedValues.CycleType.S_SCALE_X,
434                 TypedValues.CycleType.S_SCALE_Y,
435                 TypedValues.CycleType.S_TRANSLATION_X,
436                 TypedValues.CycleType.S_TRANSLATION_Y,
437                 TypedValues.CycleType.S_TRANSLATION_Z,
438                 TypedValues.CycleType.S_ROTATION_X,
439                 TypedValues.CycleType.S_ROTATION_Y,
440                 TypedValues.CycleType.S_ROTATION_Z,
441                 TypedValues.CycleType.S_ALPHA,
442                 TypedValues.CycleType.S_WAVE_PERIOD,
443                 TypedValues.CycleType.S_WAVE_OFFSET,
444                 TypedValues.CycleType.S_WAVE_PHASE,
445         };
446         int[] attrIds = {
447                 TypedValues.CycleType.TYPE_SCALE_X,
448                 TypedValues.CycleType.TYPE_SCALE_Y,
449                 TypedValues.CycleType.TYPE_TRANSLATION_X,
450                 TypedValues.CycleType.TYPE_TRANSLATION_Y,
451                 TypedValues.CycleType.TYPE_TRANSLATION_Z,
452                 TypedValues.CycleType.TYPE_ROTATION_X,
453                 TypedValues.CycleType.TYPE_ROTATION_Y,
454                 TypedValues.CycleType.TYPE_ROTATION_Z,
455                 TypedValues.CycleType.TYPE_ALPHA,
456                 TypedValues.CycleType.TYPE_WAVE_PERIOD,
457                 TypedValues.CycleType.TYPE_WAVE_OFFSET,
458                 TypedValues.CycleType.TYPE_WAVE_PHASE,
459         };
460         // type 0 the values are used as.
461         // type 1 the value is scaled from dp to pixels.
462         // type 2 are scaled if the system has another type 1.
463         int[] scaleTypes = {
464                 0,
465                 0,
466                 1,
467                 1,
468                 1,
469                 0,
470                 0,
471                 0,
472                 0,
473                 0,
474                 2,
475                 0,
476         };
477 
478 //  TODO S_WAVE_SHAPE S_CUSTOM_WAVE_SHAPE
479         TypedBundle[] bundles = new TypedBundle[frames.size()];
480         for (int i = 0; i < bundles.length; i++) {
481             bundles[i] = new TypedBundle();
482         }
483         boolean scaleOffset = false;
484         for (int k = 0; k < attrNames.length; k++) {
485             if (keyCycleData.has(attrNames[k]) && scaleTypes[k] == 1) {
486                 scaleOffset = true;
487             }
488         }
489         for (int k = 0; k < attrNames.length; k++) {
490             String attrName = attrNames[k];
491             int attrId = attrIds[k];
492             int scale = scaleTypes[k];
493             CLArray arrayValues = keyCycleData.getArrayOrNull(attrName);
494             // array must contain one per frame
495             if (arrayValues != null && arrayValues.size() != bundles.length) {
496                 throw new CLParsingException(
497                         "incorrect size for $attrName array, "
498                                 + "not matching targets array!", keyCycleData
499                 );
500             }
501             if (arrayValues != null) {
502                 for (int i = 0; i < bundles.length; i++) {
503                     float value = arrayValues.getFloat(i);
504                     if (scale == 1) {
505                         value = transition.mToPixel.toPixels(value);
506                     } else if (scale == 2 && scaleOffset) {
507                         value = transition.mToPixel.toPixels(value);
508                     }
509                     bundles[i].add(attrId, value);
510                 }
511             } else {
512                 float value = keyCycleData.getFloatOrNaN(attrName);
513                 if (!Float.isNaN(value)) {
514                     if (scale == 1) {
515                         value = transition.mToPixel.toPixels(value);
516                     } else if (scale == 2 && scaleOffset) {
517                         value = transition.mToPixel.toPixels(value);
518                     }
519                     for (int i = 0; i < bundles.length; i++) {
520                         bundles[i].add(attrId, value);
521                     }
522                 }
523             }
524         }
525         String curveFit = keyCycleData.getStringOrNull(TypedValues.CycleType.S_CURVE_FIT);
526         String easing = keyCycleData.getStringOrNull(TypedValues.CycleType.S_EASING);
527         String waveShape = keyCycleData.getStringOrNull(TypedValues.CycleType.S_WAVE_SHAPE);
528         String customWave = keyCycleData.getStringOrNull(TypedValues.CycleType.S_CUSTOM_WAVE_SHAPE);
529         for (int i = 0; i < targets.size(); i++) {
530             for (int j = 0; j < bundles.length; j++) {
531                 String target = targets.getString(i);
532                 TypedBundle bundle = bundles[j];
533 
534 
535                 if (curveFit != null) {
536                     switch (curveFit) {
537                         case "spline":
538                             bundle.add(TypedValues.CycleType.TYPE_CURVE_FIT, 0);
539                             break;
540                         case "linear":
541                             bundle.add(TypedValues.CycleType.TYPE_CURVE_FIT, 1);
542                             break;
543                     }
544                 }
545                 bundle.addIfNotNull(TypedValues.PositionType.TYPE_TRANSITION_EASING,
546                         transitionEasing);
547                 if (easing != null) {
548                     bundle.add(TypedValues.CycleType.TYPE_EASING, easing);
549                 }
550                 if (waveShape != null) {
551                     bundle.add(TypedValues.CycleType.TYPE_WAVE_SHAPE, waveShape);
552                 }
553                 if (customWave != null) {
554                     bundle.add(TypedValues.CycleType.TYPE_CUSTOM_WAVE_SHAPE, customWave);
555                 }
556 
557                 int frame = frames.getInt(j);
558                 bundle.add(TypedValues.TYPE_FRAME_POSITION, frame);
559                 transition.addKeyCycle(target, bundle);
560 
561             }
562         }
563     }
564 }
565