1 /*
2  * Copyright (C) 2021 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 androidx.annotation.RestrictTo;
20 import androidx.constraintlayout.core.motion.CustomVariable;
21 import androidx.constraintlayout.core.motion.Motion;
22 import androidx.constraintlayout.core.motion.MotionWidget;
23 import androidx.constraintlayout.core.motion.key.MotionKeyAttributes;
24 import androidx.constraintlayout.core.motion.key.MotionKeyCycle;
25 import androidx.constraintlayout.core.motion.key.MotionKeyPosition;
26 import androidx.constraintlayout.core.motion.utils.Easing;
27 import androidx.constraintlayout.core.motion.utils.KeyCache;
28 import androidx.constraintlayout.core.motion.utils.SpringStopEngine;
29 import androidx.constraintlayout.core.motion.utils.StopEngine;
30 import androidx.constraintlayout.core.motion.utils.StopLogicEngine;
31 import androidx.constraintlayout.core.motion.utils.TypedBundle;
32 import androidx.constraintlayout.core.motion.utils.TypedValues;
33 import androidx.constraintlayout.core.motion.utils.Utils;
34 import androidx.constraintlayout.core.widgets.ConstraintWidget;
35 import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer;
36 
37 import org.jspecify.annotations.NonNull;
38 
39 import java.util.ArrayList;
40 import java.util.Collection;
41 import java.util.HashMap;
42 
43 public class Transition implements TypedValues {
44     private static final boolean DEBUG = false;
45     public static final int START = 0;
46     public static final int END = 1;
47     public static final int INTERPOLATED = 2;
48     static final int EASE_IN_OUT = 0;
49     static final int EASE_IN = 1;
50     static final int EASE_OUT = 2;
51     static final int LINEAR = 3;
52     static final int BOUNCE = 4;
53     static final int OVERSHOOT = 5;
54     static final int ANTICIPATE = 6;
55     private static final int SPLINE_STRING = -1;
56     @SuppressWarnings("unused")
57     private static final int INTERPOLATOR_REFERENCE_ID = -2;
58     private HashMap<Integer, HashMap<String, KeyPosition>> mKeyPositions = new HashMap<>();
59     private HashMap<String, WidgetState> mState = new HashMap<>();
60     private TypedBundle mBundle = new TypedBundle();
61     // Interpolation
62     private int mDefaultInterpolator = 0;
63     private String mDefaultInterpolatorString = null;
64     private Easing mEasing = null;
65     private int mAutoTransition = 0;
66     private int mDuration = 400;
67     private float mStagger = 0.0f;
68     private OnSwipe mOnSwipe = null;
69     final CorePixelDp mToPixel; // Todo placed here as a temp till the refactor is done
70     int mParentStartWidth, mParentStartHeight;
71     int mParentEndWidth, mParentEndHeight;
72     int mParentInterpolatedWidth, mParentInterpolateHeight;
73     boolean mWrap;
74 
Transition(@onNull CorePixelDp dpToPixel)75     public Transition(@NonNull CorePixelDp dpToPixel) {
76         mToPixel = dpToPixel;
77     }
78 
79     // @TODO: add description
80     @SuppressWarnings("HiddenTypeParameter")
createOnSwipe()81     OnSwipe createOnSwipe() {
82         return mOnSwipe = new OnSwipe();
83     }
84 
85     // @TODO: add description
hasOnSwipe()86     public boolean hasOnSwipe() {
87         return mOnSwipe != null;
88     }
89 
90     static class OnSwipe {
91         String mAnchorId;
92         private int mAnchorSide;
93         private StopEngine mEngine;
94         public static final int ANCHOR_SIDE_TOP = 0;
95         public static final int ANCHOR_SIDE_LEFT = 1;
96         public static final int ANCHOR_SIDE_RIGHT = 2;
97         public static final int ANCHOR_SIDE_BOTTOM = 3;
98         public static final int ANCHOR_SIDE_MIDDLE = 4;
99         public static final int ANCHOR_SIDE_START = 5;
100         public static final int ANCHOR_SIDE_END = 6;
101         public static final String[] SIDES = {"top", "left", "right",
102                 "bottom", "middle", "start", "end"};
103         private static final float[][] TOUCH_SIDES = {
104                 {0.5f, 0.0f}, // top
105                 {0.0f, 0.5f}, // left
106                 {1.0f, 0.5f}, // right
107                 {0.5f, 1.0f}, // bottom
108                 {0.5f, 0.5f}, // middle
109                 {0.0f, 0.5f}, // start TODO (dynamically updated)
110                 {1.0f, 0.5f}, // end  TODO (dynamically updated)
111         };
112 
113         @SuppressWarnings("unused")
114         private String mRotationCenterId;
115         String mLimitBoundsTo;
116         @SuppressWarnings("unused")
117         private boolean mDragVertical = true;
118         private int mDragDirection = 0;
119         public static final int DRAG_UP = 0;
120         public static final int DRAG_DOWN = 1;
121         public static final int DRAG_LEFT = 2;
122         public static final int DRAG_RIGHT = 3;
123         public static final int DRAG_START = 4;
124         public static final int DRAG_END = 5;
125         public static final int DRAG_CLOCKWISE = 6;
126         public static final int DRAG_ANTICLOCKWISE = 7;
127         public static final String[] DIRECTIONS = {"up", "down", "left", "right", "start",
128                 "end", "clockwise", "anticlockwise"};
129 
130         private float mDragScale = 1;
131         @SuppressWarnings("unused")
132         private float mDragThreshold = 10;
133         private int mAutoCompleteMode = 0;
134         public static final int MODE_CONTINUOUS_VELOCITY = 0;
135         public static final int MODE_SPRING = 1;
136         public static final String[] MODE = {"velocity", "spring"};
137         private float mMaxVelocity = 4.f;
138         private float mMaxAcceleration = 1.2f;
139 
140         // On touch up what happens
141         private int mOnTouchUp = 0;
142         public static final int ON_UP_AUTOCOMPLETE = 0;
143         public static final int ON_UP_AUTOCOMPLETE_TO_START = 1;
144         public static final int ON_UP_AUTOCOMPLETE_TO_END = 2;
145         public static final int ON_UP_STOP = 3;
146         public static final int ON_UP_DECELERATE = 4;
147         public static final int ON_UP_DECELERATE_AND_COMPLETE = 5;
148         public static final int ON_UP_NEVER_COMPLETE_TO_START = 6;
149         public static final int ON_UP_NEVER_COMPLETE_TO_END = 7;
150         public static final String[] TOUCH_UP = {"autocomplete", "toStart",
151                 "toEnd", "stop", "decelerate", "decelerateComplete",
152                 "neverCompleteStart", "neverCompleteEnd"};
153 
154         private float mSpringMass = 1;
155         private float mSpringStiffness = 400;
156         private float mSpringDamping = 10;
157         private float mSpringStopThreshold = 0.01f;
158         private float mDestination = 0.0f;
159 
160         // In spring mode what happens at the boundary
161         private int mSpringBoundary = 0;
162         public static final int BOUNDARY_OVERSHOOT = 0;
163         public static final int BOUNDARY_BOUNCE_START = 1;
164         public static final int BOUNDARY_BOUNCE_END = 2;
165         public static final int BOUNDARY_BOUNCE_BOTH = 3;
166         public static final String[] BOUNDARY = {"overshoot", "bounceStart",
167                 "bounceEnd", "bounceBoth"};
168 
169         private static final float[][] TOUCH_DIRECTION = {
170                 {0.0f, -1.0f}, // up
171                 {0.0f, 1.0f}, // down
172                 {-1.0f, 0.0f}, // left
173                 {1.0f, 0.0f}, // right
174                 {-1.0f, 0.0f}, // start (dynamically updated)
175                 {1.0f, 0.0f}, // end  (dynamically updated)
176         };
177         private long mStart;
178 
getScale()179         float getScale() {
180             return mDragScale;
181         }
182 
getDirection()183         float[] getDirection() {
184             return TOUCH_DIRECTION[mDragDirection];
185         }
186 
getSide()187         float[] getSide() {
188             return TOUCH_SIDES[mAnchorSide];
189         }
190 
setAnchorId(String anchorId)191         void setAnchorId(String anchorId) {
192             this.mAnchorId = anchorId;
193         }
194 
setAnchorSide(int anchorSide)195         void setAnchorSide(int anchorSide) {
196             this.mAnchorSide = anchorSide;
197         }
198 
setRotationCenterId(String rotationCenterId)199         void setRotationCenterId(String rotationCenterId) {
200             this.mRotationCenterId = rotationCenterId;
201         }
202 
setLimitBoundsTo(String limitBoundsTo)203         void setLimitBoundsTo(String limitBoundsTo) {
204             this.mLimitBoundsTo = limitBoundsTo;
205         }
206 
setDragDirection(int dragDirection)207         void setDragDirection(int dragDirection) {
208             this.mDragDirection = dragDirection;
209             mDragVertical = (mDragDirection < 2);
210         }
211 
setDragScale(float dragScale)212         void setDragScale(float dragScale) {
213             if (Float.isNaN(dragScale)) {
214                 return;
215             }
216             this.mDragScale = dragScale;
217         }
218 
setDragThreshold(float dragThreshold)219         void setDragThreshold(float dragThreshold) {
220             if (Float.isNaN(dragThreshold)) {
221                 return;
222             }
223             this.mDragThreshold = dragThreshold;
224         }
225 
setAutoCompleteMode(int mAutoCompleteMode)226         void setAutoCompleteMode(int mAutoCompleteMode) {
227             this.mAutoCompleteMode = mAutoCompleteMode;
228         }
229 
setMaxVelocity(float maxVelocity)230         void setMaxVelocity(float maxVelocity) {
231             if (Float.isNaN(maxVelocity)) {
232                 return;
233             }
234             this.mMaxVelocity = maxVelocity;
235         }
236 
setMaxAcceleration(float maxAcceleration)237         void setMaxAcceleration(float maxAcceleration) {
238             if (Float.isNaN(maxAcceleration)) {
239                 return;
240             }
241             this.mMaxAcceleration = maxAcceleration;
242         }
243 
setOnTouchUp(int onTouchUp)244         void setOnTouchUp(int onTouchUp) {
245             this.mOnTouchUp = onTouchUp;
246         }
247 
setSpringMass(float mSpringMass)248         void setSpringMass(float mSpringMass) {
249             if (Float.isNaN(mSpringMass)) {
250                 return;
251             }
252             this.mSpringMass = mSpringMass;
253         }
254 
setSpringStiffness(float mSpringStiffness)255         void setSpringStiffness(float mSpringStiffness) {
256             if (Float.isNaN(mSpringStiffness)) {
257                 return;
258             }
259             this.mSpringStiffness = mSpringStiffness;
260         }
261 
setSpringDamping(float mSpringDamping)262         void setSpringDamping(float mSpringDamping) {
263             if (Float.isNaN(mSpringDamping)) {
264                 return;
265             }
266             this.mSpringDamping = mSpringDamping;
267         }
268 
setSpringStopThreshold(float mSpringStopThreshold)269         void setSpringStopThreshold(float mSpringStopThreshold) {
270             if (Float.isNaN(mSpringStopThreshold)) {
271                 return;
272             }
273             this.mSpringStopThreshold = mSpringStopThreshold;
274         }
275 
setSpringBoundary(int mSpringBoundary)276         void setSpringBoundary(int mSpringBoundary) {
277             this.mSpringBoundary = mSpringBoundary;
278         }
279 
getDestinationPosition(float currentPosition, float velocity, float duration)280         float getDestinationPosition(float currentPosition, float velocity, float duration) {
281             float rest = currentPosition + 0.5f * Math.abs(velocity) * velocity / mMaxAcceleration;
282             switch (mOnTouchUp) {
283                 case ON_UP_AUTOCOMPLETE_TO_START:
284                     if (currentPosition >= 1f) {
285                         return 1;
286                     }
287                     return 0;
288                 case ON_UP_NEVER_COMPLETE_TO_END:
289                     return 0;
290                 case ON_UP_AUTOCOMPLETE_TO_END:
291                     if (currentPosition <= 0f) {
292                         return 0;
293                     }
294                     return 1;
295                 case ON_UP_NEVER_COMPLETE_TO_START:
296                     return 1;
297                 case ON_UP_STOP:
298                     return Float.NaN;
299                 case ON_UP_DECELERATE:
300                     return Math.max(0, Math.min(1, rest));
301                 case ON_UP_DECELERATE_AND_COMPLETE: // complete if within 20% of edge #todo improve
302                     if (rest > 0.2f && rest < 0.8f) {
303                         return rest;
304                     } else {
305                         return rest > .5f ? 1 : 0;
306                     }
307                 case ON_UP_AUTOCOMPLETE:
308             }
309 
310             if (DEBUG) {
311                 Utils.log(" currentPosition = " + currentPosition);
312                 Utils.log("        velocity = " + velocity);
313                 Utils.log("            peek = " + rest);
314                 Utils.log("mMaxAcceleration = " + mMaxAcceleration);
315             }
316             return rest > .5 ? 1 : 0;
317         }
318 
config(float position, float velocity, long start, float duration)319         void config(float position, float velocity, long start, float duration) {
320             mStart = start;
321             if (Math.abs(velocity) > mMaxVelocity) {
322                 velocity = mMaxVelocity * Math.signum(velocity);
323             }
324             mDestination = getDestinationPosition(position, velocity, duration);
325             if (mDestination == position) {
326                 mEngine = null;
327                 return;
328             }
329             if ((mOnTouchUp == ON_UP_DECELERATE)
330                     && (mAutoCompleteMode == MODE_CONTINUOUS_VELOCITY)) {
331                 StopLogicEngine.Decelerate sld;
332                 if (mEngine instanceof StopLogicEngine.Decelerate) {
333                     sld = (StopLogicEngine.Decelerate) mEngine;
334                 } else {
335                     mEngine = sld = new StopLogicEngine.Decelerate();
336                 }
337                 sld.config(position, mDestination, velocity);
338                 return;
339             }
340 
341 
342             if (mAutoCompleteMode == MODE_CONTINUOUS_VELOCITY) {
343                 StopLogicEngine sl;
344                 if (mEngine instanceof StopLogicEngine) {
345                     sl = (StopLogicEngine) mEngine;
346                 } else {
347                     mEngine = sl = new StopLogicEngine();
348                 }
349 
350                 sl.config(position, mDestination, velocity,
351                         duration, mMaxAcceleration,
352                         mMaxVelocity);
353                 return;
354             }
355             SpringStopEngine sl;
356             if (mEngine instanceof SpringStopEngine) {
357                 sl = (SpringStopEngine) mEngine;
358             } else {
359                 mEngine = sl = new SpringStopEngine();
360             }
361 
362             sl.springConfig(position, mDestination, velocity,
363                     mSpringMass,
364                     mSpringStiffness,
365                     mSpringDamping,
366                     mSpringStopThreshold, mSpringBoundary);
367         }
368 
369         /**
370          * @param currentTime time in nanoseconds
371          * @return new values of progress
372          */
getTouchUpProgress(long currentTime)373         public float getTouchUpProgress(long currentTime) {
374             float time = (currentTime - mStart) * 1E-9f;
375             float pos = mEngine.getInterpolation(time);
376             if (mEngine.isStopped()) {
377                 pos = mDestination;
378             }
379             return pos;
380         }
381 
printInfo()382         public void printInfo() {
383             if (mAutoCompleteMode == MODE_CONTINUOUS_VELOCITY) {
384                 System.out.println("velocity = " + mEngine.getVelocity());
385                 System.out.println("mMaxAcceleration = " + mMaxAcceleration);
386                 System.out.println("mMaxVelocity = " + mMaxVelocity);
387             } else {
388                 System.out.println("mSpringMass          = " + mSpringMass);
389                 System.out.println("mSpringStiffness     = " + mSpringStiffness);
390                 System.out.println("mSpringDamping       = " + mSpringDamping);
391                 System.out.println("mSpringStopThreshold = " + mSpringStopThreshold);
392                 System.out.println("mSpringBoundary      = " + mSpringBoundary);
393             }
394         }
395 
isNotDone(float progress)396         public boolean isNotDone(float progress) {
397             if (mOnTouchUp == ON_UP_STOP) {
398                 return false;
399             }
400             return mEngine != null && !mEngine.isStopped();
401         }
402     }
403 
404     /**
405      * For the given position (in the MotionLayout coordinate space) determine whether we accept
406      * the first down for on swipe.
407      * <p>
408      * This is based off {@link OnSwipe#mLimitBoundsTo}. If null, we accept the drag at any
409      * position, otherwise, we only accept it if it's within its bounds.
410      */
411     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
isFirstDownAccepted(float posX, float posY)412     public boolean isFirstDownAccepted(float posX, float posY) {
413         if (mOnSwipe == null) {
414             return false;
415         }
416 
417         if (mOnSwipe.mLimitBoundsTo != null) {
418             WidgetState targetWidget = mState.get(mOnSwipe.mLimitBoundsTo);
419             if (targetWidget == null) {
420                 System.err.println("mLimitBoundsTo target is null");
421                 return false;
422             }
423             // Calculate against the interpolated/current frame
424             WidgetFrame frame = targetWidget.getFrame(2);
425             return posX >= frame.left && posX < frame.right && posY >= frame.top
426                     && posY < frame.bottom;
427         } else {
428             return true;
429         }
430     }
431 
432     /**
433      * Converts from xy drag to progress
434      * This should be used till touch up
435      *
436      * @param currentProgress 0...1 progress in
437      * @param baseW           parent width
438      * @param baseH           parent height
439      * @param dx              change in x
440      * @param dy              change in y
441      * @return the change in progress
442      */
dragToProgress(float currentProgress, int baseW, int baseH, float dx, float dy)443     public float dragToProgress(float currentProgress, int baseW, int baseH, float dx, float dy) {
444         Collection<WidgetState> widgets = mState.values();
445         WidgetState childWidget = null;
446         for (WidgetState widget : widgets) {
447             childWidget = widget;
448             break;
449         }
450         if (mOnSwipe == null || childWidget == null) {
451             if (childWidget != null) {
452                 return -dy / childWidget.mParentHeight;
453             }
454             return 1.0f;
455         }
456         if (mOnSwipe.mAnchorId == null) {
457 
458             float[] dir = mOnSwipe.getDirection();
459             float motionDpDtX = childWidget.mParentHeight;
460             float motionDpDtY = childWidget.mParentHeight;
461 
462             float drag = (dir[0] != 0) ? dx * Math.abs(dir[0]) / motionDpDtX
463                     : dy * Math.abs(dir[1]) / motionDpDtY;
464             return drag * mOnSwipe.getScale();
465         }
466         WidgetState base = mState.get(mOnSwipe.mAnchorId);
467         float[] dir = mOnSwipe.getDirection();
468         float[] side = mOnSwipe.getSide();
469         float[] motionDpDt = new float[2];
470 
471         base.interpolate(baseW, baseH, currentProgress, this);
472         base.mMotionControl.getDpDt(currentProgress, side[0], side[1], motionDpDt);
473         float drag = (dir[0] != 0) ? dx * Math.abs(dir[0]) / motionDpDt[0]
474                 : dy * Math.abs(dir[1]) / motionDpDt[1];
475         if (DEBUG) {
476             Utils.log(" drag " + drag);
477         }
478         return drag * mOnSwipe.getScale();
479     }
480 
481     /**
482      * Set the start of the touch up
483      *
484      * @param currentProgress 0...1 progress in
485      * @param currentTime     time in nanoseconds
486      * @param velocityX       pixels per millisecond
487      * @param velocityY       pixels per millisecond
488      */
setTouchUp(float currentProgress, long currentTime, float velocityX, float velocityY)489     public void setTouchUp(float currentProgress,
490             long currentTime,
491             float velocityX,
492             float velocityY) {
493         if (mOnSwipe != null) {
494             if (DEBUG) {
495                 Utils.log(" >>> velocity x,y = " + velocityX + " , " + velocityY);
496             }
497             WidgetState base = mState.get(mOnSwipe.mAnchorId);
498             float[] motionDpDt = new float[2];
499             float[] dir = mOnSwipe.getDirection();
500             float[] side = mOnSwipe.getSide();
501             base.mMotionControl.getDpDt(currentProgress, side[0], side[1], motionDpDt);
502             float movementInDir = dir[0] * motionDpDt[0] + dir[1] * motionDpDt[1];
503             if (Math.abs(movementInDir) < 0.01) {
504                 if (DEBUG) {
505                     Utils.log(" >>> cap minimum v!! ");
506                 }
507                 motionDpDt[0] = .01f;
508                 motionDpDt[1] = .01f;
509             }
510 
511             float drag = (dir[0] != 0) ? velocityX / motionDpDt[0] : velocityY / motionDpDt[1];
512             drag *= mOnSwipe.getScale();
513             if (DEBUG) {
514                 Utils.log(" >>> velocity        " + drag);
515                 Utils.log(" >>> mDuration       " + mDuration);
516                 Utils.log(" >>> currentProgress " + currentProgress);
517             }
518             mOnSwipe.config(currentProgress, drag, currentTime, mDuration * 1E-3f);
519             if (DEBUG) {
520                 mOnSwipe.printInfo();
521             }
522         }
523     }
524 
525     /**
526      * get the current touch up progress current time in nanoseconds
527      * (ideally coming from an animation clock)
528      *
529      * @param currentTime in nanoseconds
530      * @return progress
531      */
getTouchUpProgress(long currentTime)532     public float getTouchUpProgress(long currentTime) {
533         if (mOnSwipe != null) {
534             return mOnSwipe.getTouchUpProgress(currentTime);
535         }
536         return 0;
537     }
538 
539     /**
540      * Are we still animating
541      *
542      * @param currentProgress motion progress
543      * @return true to continue moving
544      */
isTouchNotDone(float currentProgress)545     public boolean isTouchNotDone(float currentProgress) {
546         return mOnSwipe.isNotDone(currentProgress);
547     }
548 
549     /**
550      * get the interpolater based on a constant or a string
551      */
getInterpolator(int interpolator, String interpolatorString)552     public static Interpolator getInterpolator(int interpolator, String interpolatorString) {
553         switch (interpolator) {
554             case SPLINE_STRING:
555                 return v -> (float) Easing.getInterpolator(interpolatorString).get(v);
556             case EASE_IN_OUT:
557                 return v -> (float) Easing.getInterpolator("standard").get(v);
558             case EASE_IN:
559                 return v -> (float) Easing.getInterpolator("accelerate").get(v);
560             case EASE_OUT:
561                 return v -> (float) Easing.getInterpolator("decelerate").get(v);
562             case LINEAR:
563                 return v -> (float) Easing.getInterpolator("linear").get(v);
564             case ANTICIPATE:
565                 return v -> (float) Easing.getInterpolator("anticipate").get(v);
566             case OVERSHOOT:
567                 return v -> (float) Easing.getInterpolator("overshoot").get(v);
568             case BOUNCE: // TODO make a better bounce
569                 return v -> (float) Easing.getInterpolator("spline(0.0, 0.2, 0.4, 0.6, "
570                         + "0.8 ,1.0, 0.8, 1.0, 0.9, 1.0)").get(v);
571         }
572         return null;
573     }
574 
575     // @TODO: add description
576     @SuppressWarnings("HiddenTypeParameter")
findPreviousPosition(String target, int frameNumber)577     public KeyPosition findPreviousPosition(String target, int frameNumber) {
578         while (frameNumber >= 0) {
579             HashMap<String, KeyPosition> map = mKeyPositions.get(frameNumber);
580             if (map != null) {
581                 KeyPosition keyPosition = map.get(target);
582                 if (keyPosition != null) {
583                     return keyPosition;
584                 }
585             }
586             frameNumber--;
587         }
588         return null;
589     }
590 
591     // @TODO: add description
592     @SuppressWarnings("HiddenTypeParameter")
findNextPosition(String target, int frameNumber)593     public KeyPosition findNextPosition(String target, int frameNumber) {
594         while (frameNumber <= 100) {
595             HashMap<String, KeyPosition> map = mKeyPositions.get(frameNumber);
596             if (map != null) {
597                 KeyPosition keyPosition = map.get(target);
598                 if (keyPosition != null) {
599                     return keyPosition;
600                 }
601             }
602             frameNumber++;
603         }
604         return null;
605     }
606 
607     // @TODO: add description
getNumberKeyPositions(WidgetFrame frame)608     public int getNumberKeyPositions(WidgetFrame frame) {
609         int numKeyPositions = 0;
610         int frameNumber = 0;
611         while (frameNumber <= 100) {
612             HashMap<String, KeyPosition> map = mKeyPositions.get(frameNumber);
613             if (map != null) {
614                 KeyPosition keyPosition = map.get(frame.widget.stringId);
615                 if (keyPosition != null) {
616                     numKeyPositions++;
617                 }
618             }
619             frameNumber++;
620         }
621         return numKeyPositions;
622     }
623 
624     // @TODO: add description
getMotion(String id)625     public Motion getMotion(String id) {
626         return getWidgetState(id, null, 0).mMotionControl;
627     }
628 
629     // @TODO: add description
fillKeyPositions(WidgetFrame frame, float[] x, float[] y, float[] pos)630     public void fillKeyPositions(WidgetFrame frame, float[] x, float[] y, float[] pos) {
631         int numKeyPositions = 0;
632         int frameNumber = 0;
633         while (frameNumber <= 100) {
634             HashMap<String, KeyPosition> map = mKeyPositions.get(frameNumber);
635             if (map != null) {
636                 KeyPosition keyPosition = map.get(frame.widget.stringId);
637                 if (keyPosition != null) {
638                     x[numKeyPositions] = keyPosition.mX;
639                     y[numKeyPositions] = keyPosition.mY;
640                     pos[numKeyPositions] = keyPosition.mFrame;
641                     numKeyPositions++;
642                 }
643             }
644             frameNumber++;
645         }
646     }
647 
648     // @TODO: add description
hasPositionKeyframes()649     public boolean hasPositionKeyframes() {
650         return mKeyPositions.size() > 0;
651     }
652 
653     // @TODO: add description
setTransitionProperties(TypedBundle bundle)654     public void setTransitionProperties(TypedBundle bundle) {
655         bundle.applyDelta(mBundle);
656         bundle.applyDelta(this);
657     }
658 
659     @Override
setValue(int id, int value)660     public boolean setValue(int id, int value) {
661         return false;
662     }
663 
664     @Override
setValue(int id, float value)665     public boolean setValue(int id, float value) {
666         if (id == TypedValues.TransitionType.TYPE_STAGGERED) {
667             mStagger = value;
668         }
669         return false;
670     }
671 
672     @Override
setValue(int id, String value)673     public boolean setValue(int id, String value) {
674         if (id == TransitionType.TYPE_INTERPOLATOR) {
675             mEasing = Easing.getInterpolator(mDefaultInterpolatorString = value);
676         }
677         return false;
678     }
679 
680     @Override
setValue(int id, boolean value)681     public boolean setValue(int id, boolean value) {
682         return false;
683     }
684 
685     @Override
getId(String name)686     public int getId(String name) {
687         return 0;
688     }
689 
isEmpty()690     public boolean isEmpty() {
691         return mState.isEmpty();
692     }
693 
694     // @TODO: add description
clear()695     public void clear() {
696         mState.clear();
697     }
698 
699     /**
700      * Reset animation properties of the Transition.
701      * <p>
702      * This will not affect the internal model of the widgets (a.k.a. {@link #mState}).
703      */
resetProperties()704     void resetProperties() {
705         mOnSwipe = null;
706         mBundle.clear();
707     }
708 
709     // @TODO: add description
contains(String key)710     public boolean contains(String key) {
711         return mState.containsKey(key);
712     }
713 
714     // @TODO: add description
addKeyPosition(String target, TypedBundle bundle)715     public void addKeyPosition(String target, TypedBundle bundle) {
716         getWidgetState(target, null, 0).setKeyPosition(bundle);
717     }
718 
719     // @TODO: add description
addKeyAttribute(String target, TypedBundle bundle)720     public void addKeyAttribute(String target, TypedBundle bundle) {
721         getWidgetState(target, null, 0).setKeyAttribute(bundle);
722     }
723 
724     /**
725      * Add a key attribute and the custom variables into the
726      * @param target the id of the target
727      * @param bundle the key attributes bundle containing position etc.
728      * @param custom the customVariables to add at that position
729      */
addKeyAttribute(String target, TypedBundle bundle, CustomVariable[]custom)730     public void addKeyAttribute(String target, TypedBundle bundle, CustomVariable[]custom) {
731         getWidgetState(target, null, 0).setKeyAttribute(bundle,custom);
732     }
733 
734     // @TODO: add description
addKeyCycle(String target, TypedBundle bundle)735     public void addKeyCycle(String target, TypedBundle bundle) {
736         getWidgetState(target, null, 0).setKeyCycle(bundle);
737     }
738 
739     // @TODO: add description
addKeyPosition(String target, int frame, int type, float x, float y)740     public void addKeyPosition(String target, int frame, int type, float x, float y) {
741         TypedBundle bundle = new TypedBundle();
742         bundle.add(TypedValues.PositionType.TYPE_POSITION_TYPE, 2);
743         bundle.add(TypedValues.TYPE_FRAME_POSITION, frame);
744         bundle.add(TypedValues.PositionType.TYPE_PERCENT_X, x);
745         bundle.add(TypedValues.PositionType.TYPE_PERCENT_Y, y);
746         getWidgetState(target, null, 0).setKeyPosition(bundle);
747 
748         KeyPosition keyPosition = new KeyPosition(target, frame, type, x, y);
749         HashMap<String, KeyPosition> map = mKeyPositions.get(frame);
750         if (map == null) {
751             map = new HashMap<>();
752             mKeyPositions.put(frame, map);
753         }
754         map.put(target, keyPosition);
755     }
756 
757     // @TODO: add description
addCustomFloat(int state, String widgetId, String property, float value)758     public void addCustomFloat(int state, String widgetId, String property, float value) {
759         WidgetState widgetState = getWidgetState(widgetId, null, state);
760         WidgetFrame frame = widgetState.getFrame(state);
761         frame.addCustomFloat(property, value);
762     }
763 
764     // @TODO: add description
addCustomColor(int state, String widgetId, String property, int color)765     public void addCustomColor(int state, String widgetId, String property, int color) {
766         WidgetState widgetState = getWidgetState(widgetId, null, state);
767         WidgetFrame frame = widgetState.getFrame(state);
768         frame.addCustomColor(property, color);
769     }
770 
calculateParentDimensions(float progress)771     private void calculateParentDimensions(float progress) {
772         mParentInterpolatedWidth = (int) (0.5f +
773                 mParentStartWidth + (mParentEndWidth - mParentStartWidth) * progress);
774         mParentInterpolateHeight = (int) (0.5f +
775                 mParentStartHeight + (mParentEndHeight - mParentStartHeight) * progress);
776     }
777 
getInterpolatedWidth()778     public int getInterpolatedWidth() {
779         return mParentInterpolatedWidth;
780     }
781 
getInterpolatedHeight()782     public int getInterpolatedHeight() {
783         return mParentInterpolateHeight;
784     }
785     /**
786      * Update container of parameters for the state
787      *
788      * @param container contains all the widget parameters
789      * @param state     starting or ending
790      */
updateFrom(ConstraintWidgetContainer container, int state)791     public void updateFrom(ConstraintWidgetContainer container, int state) {
792         mWrap = container.mListDimensionBehaviors[0]
793                 == ConstraintWidget.DimensionBehaviour.WRAP_CONTENT;
794         mWrap |= container.mListDimensionBehaviors[1]
795                 == ConstraintWidget.DimensionBehaviour.WRAP_CONTENT;
796         if (state == START) {
797             mParentInterpolatedWidth = mParentStartWidth = container.getWidth();
798             mParentInterpolateHeight = mParentStartHeight = container.getHeight();
799         } else {
800             mParentEndWidth = container.getWidth();
801             mParentEndHeight = container.getHeight();
802         }
803         final ArrayList<ConstraintWidget> children = container.getChildren();
804         final int count = children.size();
805         WidgetState[] states = new WidgetState[count];
806 
807         for (int i = 0; i < count; i++) {
808             ConstraintWidget child = children.get(i);
809             WidgetState widgetState = getWidgetState(child.stringId, null, state);
810             states[i] = widgetState;
811             widgetState.update(child, state);
812             String id = widgetState.getPathRelativeId();
813             if (id != null) {
814                 widgetState.setPathRelative(getWidgetState(id, null, state));
815             }
816         }
817 
818         calcStagger();
819     }
820 
821     // @TODO: add description
interpolate(int parentWidth, int parentHeight, float progress)822     public void interpolate(int parentWidth, int parentHeight, float progress) {
823         if (mWrap) {
824             calculateParentDimensions(progress);
825         }
826 
827         if (mEasing != null) {
828             progress = (float) mEasing.get(progress);
829         }
830         for (String key : mState.keySet()) {
831             WidgetState widget = mState.get(key);
832             widget.interpolate(parentWidth, parentHeight, progress, this);
833         }
834     }
835 
836     // @TODO: add description
getStart(String id)837     public WidgetFrame getStart(String id) {
838         WidgetState widgetState = mState.get(id);
839         if (widgetState == null) {
840             return null;
841         }
842         return widgetState.mStart;
843     }
844 
845     // @TODO: add description
getEnd(String id)846     public WidgetFrame getEnd(String id) {
847         WidgetState widgetState = mState.get(id);
848         if (widgetState == null) {
849             return null;
850         }
851         return widgetState.mEnd;
852     }
853 
854     // @TODO: add description
getInterpolated(String id)855     public WidgetFrame getInterpolated(String id) {
856         WidgetState widgetState = mState.get(id);
857         if (widgetState == null) {
858             return null;
859         }
860         return widgetState.mInterpolated;
861     }
862 
863     // @TODO: add description
getPath(String id)864     public float[] getPath(String id) {
865         WidgetState widgetState = mState.get(id);
866         int duration = 1000;
867         int frames = duration / 16;
868         float[] mPoints = new float[frames * 2];
869         widgetState.mMotionControl.buildPath(mPoints, frames);
870         return mPoints;
871     }
872 
873     // @TODO: add description
getKeyFrames(String id, float[] rectangles, int[] pathMode, int[] position)874     public int getKeyFrames(String id, float[] rectangles, int[] pathMode, int[] position) {
875         WidgetState widgetState = mState.get(id);
876         return widgetState.mMotionControl.buildKeyFrames(rectangles, pathMode, position);
877     }
878 
879     @SuppressWarnings("unused")
getWidgetState(String widgetId)880     private WidgetState getWidgetState(String widgetId) {
881         return this.mState.get(widgetId);
882     }
883 
getWidgetState(String widgetId, ConstraintWidget child, int transitionState)884     public WidgetState getWidgetState(String widgetId,
885             ConstraintWidget child,
886             int transitionState) {
887         WidgetState widgetState = this.mState.get(widgetId);
888         if (widgetState == null) {
889             widgetState = new WidgetState();
890             mBundle.applyDelta(widgetState.mMotionControl);
891             widgetState.mMotionWidgetStart.updateMotion(widgetState.mMotionControl);
892             mState.put(widgetId, widgetState);
893             if (child != null) {
894                 widgetState.update(child, transitionState);
895             }
896         }
897         return widgetState;
898     }
899 
900     /**
901      * Used in debug draw
902      */
getStart(ConstraintWidget child)903     public WidgetFrame getStart(ConstraintWidget child) {
904         return getWidgetState(child.stringId, null, Transition.START).mStart;
905     }
906 
907     /**
908      * Used in debug draw
909      */
getEnd(ConstraintWidget child)910     public WidgetFrame getEnd(ConstraintWidget child) {
911         return getWidgetState(child.stringId, null, Transition.END).mEnd;
912     }
913 
914     /**
915      * Used after the interpolation
916      */
getInterpolated(ConstraintWidget child)917     public WidgetFrame getInterpolated(ConstraintWidget child) {
918         return getWidgetState(child.stringId, null, Transition.INTERPOLATED).mInterpolated;
919     }
920 
921     /**
922      * This gets the interpolator being used
923      */
getInterpolator()924     public Interpolator getInterpolator() {
925         return getInterpolator(mDefaultInterpolator, mDefaultInterpolatorString);
926     }
927 
928     /**
929      * This gets the auto transition mode being used
930      */
getAutoTransition()931     public int getAutoTransition() {
932         return mAutoTransition;
933     }
934 
935     public static class WidgetState {
936         WidgetFrame mStart;
937         WidgetFrame mEnd;
938         WidgetFrame mInterpolated;
939         Motion mMotionControl;
940         boolean mNeedSetup = true;
941         MotionWidget mMotionWidgetStart;
942         MotionWidget mMotionWidgetEnd;
943         MotionWidget mMotionWidgetInterpolated;
944         KeyCache mKeyCache = new KeyCache();
945         int mParentHeight = -1;
946         int mParentWidth = -1;
947 
WidgetState()948         public WidgetState() {
949             mStart = new WidgetFrame();
950             mEnd = new WidgetFrame();
951             mInterpolated = new WidgetFrame();
952             mMotionWidgetStart = new MotionWidget(mStart);
953             mMotionWidgetEnd = new MotionWidget(mEnd);
954             mMotionWidgetInterpolated = new MotionWidget(mInterpolated);
955             mMotionControl = new Motion(mMotionWidgetStart);
956             mMotionControl.setStart(mMotionWidgetStart);
957             mMotionControl.setEnd(mMotionWidgetEnd);
958         }
959 
setKeyPosition(TypedBundle prop)960         public void setKeyPosition(TypedBundle prop) {
961             MotionKeyPosition keyPosition = new MotionKeyPosition();
962             prop.applyDelta(keyPosition);
963             mMotionControl.addKey(keyPosition);
964         }
965 
setKeyAttribute(TypedBundle prop)966         public void setKeyAttribute(TypedBundle prop) {
967             MotionKeyAttributes keyAttributes = new MotionKeyAttributes();
968             prop.applyDelta(keyAttributes);
969             mMotionControl.addKey(keyAttributes);
970         }
971 
972         /**
973          * Set tge keyAttribute bundle and associated custom attributes
974          * @param prop
975          * @param custom
976          */
setKeyAttribute(TypedBundle prop, CustomVariable[] custom)977         public void setKeyAttribute(TypedBundle prop, CustomVariable[] custom) {
978             MotionKeyAttributes keyAttributes = new MotionKeyAttributes();
979             prop.applyDelta(keyAttributes);
980             if (custom != null) {
981                 for (int i = 0; i < custom.length; i++) {
982                     keyAttributes.mCustom.put( custom[i].getName(), custom[i]);
983                 }
984             }
985             mMotionControl.addKey(keyAttributes);
986         }
987 
setKeyCycle(TypedBundle prop)988         public void setKeyCycle(TypedBundle prop) {
989             MotionKeyCycle keyAttributes = new MotionKeyCycle();
990             prop.applyDelta(keyAttributes);
991             mMotionControl.addKey(keyAttributes);
992         }
993 
update(ConstraintWidget child, int state)994         public void update(ConstraintWidget child, int state) {
995             if (state == START) {
996                 mStart.update(child);
997                 mMotionWidgetStart.updateMotion(mMotionWidgetStart);
998                 mMotionControl.setStart(mMotionWidgetStart);
999                 mNeedSetup = true;
1000             } else if (state == END) {
1001                 mEnd.update(child);
1002                 mMotionControl.setEnd(mMotionWidgetEnd);
1003                 mNeedSetup = true;
1004             }
1005             mParentWidth = -1;
1006         }
1007 
1008         /**
1009          * Return the id of the widget to animate relative to
1010          *
1011          * @return id of widget or null
1012          */
getPathRelativeId()1013         String getPathRelativeId() {
1014             return mMotionControl.getAnimateRelativeTo();
1015         }
1016 
getFrame(int type)1017         public WidgetFrame getFrame(int type) {
1018             if (type == START) {
1019                 return mStart;
1020             } else if (type == END) {
1021                 return mEnd;
1022             }
1023             return mInterpolated;
1024         }
1025 
interpolate(int parentWidth, int parentHeight, float progress, Transition transition)1026         public void interpolate(int parentWidth,
1027                 int parentHeight,
1028                 float progress,
1029                 Transition transition) {
1030             // TODO  only update if parentHeight != mParentHeight || parentWidth != mParentWidth) {
1031             mParentHeight = parentHeight;
1032             mParentWidth = parentWidth;
1033             if (mNeedSetup) {
1034                 mMotionControl.setup(parentWidth, parentHeight, 1, System.nanoTime());
1035                 mNeedSetup = false;
1036             }
1037             WidgetFrame.interpolate(parentWidth, parentHeight,
1038                     mInterpolated, mStart, mEnd, transition, progress);
1039             mInterpolated.interpolatedPos = progress;
1040             mMotionControl.interpolate(mMotionWidgetInterpolated,
1041                     progress, System.nanoTime(), mKeyCache);
1042         }
1043 
setPathRelative(WidgetState widgetState)1044         public void setPathRelative(WidgetState widgetState) {
1045             mMotionControl.setupRelative(widgetState.mMotionControl);
1046         }
1047     }
1048 
1049     static class KeyPosition {
1050         int mFrame;
1051         String mTarget;
1052         int mType;
1053         float mX;
1054         float mY;
1055 
KeyPosition(String target, int frame, int type, float x, float y)1056         KeyPosition(String target, int frame, int type, float x, float y) {
1057             this.mTarget = target;
1058             this.mFrame = frame;
1059             this.mType = type;
1060             this.mX = x;
1061             this.mY = y;
1062         }
1063     }
1064 
calcStagger()1065     public void calcStagger() {
1066         if (mStagger == 0.0f) {
1067             return;
1068         }
1069         boolean flip = mStagger < 0.0;
1070 
1071         float stagger = Math.abs(mStagger);
1072         float min = Float.MAX_VALUE, max = -Float.MAX_VALUE;
1073         boolean useMotionStagger = false;
1074 
1075         for (String widgetId : mState.keySet()) {
1076             WidgetState widgetState = mState.get(widgetId);
1077             Motion f = widgetState.mMotionControl;
1078             if (!Float.isNaN(f.getMotionStagger())) {
1079                 useMotionStagger = true;
1080                 break;
1081             }
1082         }
1083         if (useMotionStagger) {
1084             for (String widgetId : mState.keySet()) {
1085                 WidgetState widgetState = mState.get(widgetId);
1086                 Motion f = widgetState.mMotionControl;
1087                 float widgetStagger = f.getMotionStagger();
1088                 if (!Float.isNaN(widgetStagger)) {
1089                     min = Math.min(min, widgetStagger);
1090                     max = Math.max(max, widgetStagger);
1091                 }
1092             }
1093 
1094             for (String widgetId : mState.keySet()) {
1095                 WidgetState widgetState = mState.get(widgetId);
1096                 Motion f = widgetState.mMotionControl;
1097 
1098                 float widgetStagger = f.getMotionStagger();
1099                 if (!Float.isNaN(widgetStagger)) {
1100                     float scale = 1 / (1 - stagger);
1101 
1102                     float offset = stagger - stagger * (widgetStagger - min) / (max - min);
1103                     if (flip) {
1104                         offset = stagger - stagger
1105                                 * ((max - widgetStagger) / (max - min));
1106                     }
1107                     f.setStaggerScale(scale);
1108                     f.setStaggerOffset(offset);
1109                 }
1110             }
1111 
1112         } else {
1113             for (String widgetId : mState.keySet()) {
1114                 WidgetState widgetState = mState.get(widgetId);
1115                 Motion f = widgetState.mMotionControl;
1116                 float x = f.getFinalX();
1117                 float y = f.getFinalY();
1118                 float widgetStagger = x + y;
1119                 min = Math.min(min, widgetStagger);
1120                 max = Math.max(max, widgetStagger);
1121             }
1122 
1123             for (String widgetId : mState.keySet()) {
1124                 WidgetState widgetState = mState.get(widgetId);
1125                 Motion f = widgetState.mMotionControl;
1126                 float x = f.getFinalX();
1127                 float y = f.getFinalY();
1128                 float widgetStagger = x + y;
1129                 float offset = stagger - stagger * (widgetStagger - min) / (max - min);
1130                 if (flip) {
1131                     offset = stagger - stagger
1132                             * ((max - widgetStagger) / (max - min));
1133                 }
1134 
1135                 float scale = 1 / (1 - stagger);
1136                 f.setStaggerScale(scale);
1137                 f.setStaggerOffset(offset);
1138             }
1139         }
1140     }
1141 }
1142