1 /*
2  * Copyright (C) 2020 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.motion.widget;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.content.res.XmlResourceParser;
22 import android.graphics.Rect;
23 import android.util.AttributeSet;
24 import android.util.Log;
25 import android.util.TypedValue;
26 import android.util.Xml;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.animation.AccelerateDecelerateInterpolator;
31 import android.view.animation.AccelerateInterpolator;
32 import android.view.animation.AnimationUtils;
33 import android.view.animation.AnticipateInterpolator;
34 import android.view.animation.BounceInterpolator;
35 import android.view.animation.DecelerateInterpolator;
36 import android.view.animation.Interpolator;
37 import android.view.animation.OvershootInterpolator;
38 
39 import androidx.constraintlayout.core.motion.utils.Easing;
40 import androidx.constraintlayout.core.motion.utils.KeyCache;
41 import androidx.constraintlayout.widget.ConstraintAttribute;
42 import androidx.constraintlayout.widget.ConstraintLayout;
43 import androidx.constraintlayout.widget.ConstraintSet;
44 import androidx.constraintlayout.widget.R;
45 
46 import org.xmlpull.v1.XmlPullParser;
47 import org.xmlpull.v1.XmlPullParserException;
48 
49 import java.io.IOException;
50 import java.util.ArrayList;
51 
52 /**
53  * Provides a support for <ViewTransition> tag
54  * it Parses tag
55  * it implement the transition
56  * it will update ConstraintSet or sets
57  * For asynchronous it will create and drive a MotionController.
58  */
59 public class ViewTransition {
60     private static final String TAG = "ViewTransition";
61     ConstraintSet mSet;
62     public static final String VIEW_TRANSITION_TAG = "ViewTransition";
63     public static final String KEY_FRAME_SET_TAG = "KeyFrameSet";
64     public static final String CONSTRAINT_OVERRIDE = "ConstraintOverride";
65     public static final String CUSTOM_ATTRIBUTE = "CustomAttribute";
66     public static final String CUSTOM_METHOD = "CustomMethod";
67 
68     private static final int UNSET = -1;
69     private int mId;
70     // Transition can be up or down of manually fired
71     public static final int ONSTATE_ACTION_DOWN = 1;
72     public static final int ONSTATE_ACTION_UP = 2;
73     public static final int ONSTATE_ACTION_DOWN_UP = 3;
74     public static final int ONSTATE_SHARED_VALUE_SET = 4;
75     public static final int ONSTATE_SHARED_VALUE_UNSET = 5;
76 
77     private int mOnStateTransition = UNSET;
78     private boolean mDisabled = false;
79     private int mPathMotionArc = 0;
80     int mViewTransitionMode;
81     static final int VIEWTRANSITIONMODE_CURRENTSTATE = 0;
82     static final int VIEWTRANSITIONMODE_ALLSTATES = 1;
83     static final int VIEWTRANSITIONMODE_NOSTATE = 2;
84     KeyFrames mKeyFrames;
85     ConstraintSet.Constraint mConstraintDelta;
86     private int mDuration = UNSET;
87     private int mUpDuration = UNSET;
88 
89     private int mTargetId;
90     private String mTargetString;
91 
92     // interpolator code
93     private static final int SPLINE_STRING = -1;
94     private static final int INTERPOLATOR_REFERENCE_ID = -2;
95     private int mDefaultInterpolator = 0;
96     private String mDefaultInterpolatorString = null;
97     private int mDefaultInterpolatorID = -1;
98     static final int EASE_IN_OUT = 0;
99     static final int EASE_IN = 1;
100     static final int EASE_OUT = 2;
101     static final int LINEAR = 3;
102     static final int BOUNCE = 4;
103     static final int OVERSHOOT = 5;
104     static final int ANTICIPATE = 6;
105 
106     Context mContext;
107     private int mSetsTag = UNSET;
108     private int mClearsTag = UNSET;
109     private int mIfTagSet = UNSET;
110     private int mIfTagNotSet = UNSET;
111 
112     // shared value management. mSharedValueId is the key we are watching,
113     // mSharedValueCurrent the current value for that key, and mSharedValueTarget
114     // is the target we are waiting for to trigger.
115     private int mSharedValueTarget = UNSET;
116     private int mSharedValueID = UNSET;
117     private int mSharedValueCurrent = UNSET;
118 
getSharedValueCurrent()119     public int getSharedValueCurrent() {
120         return mSharedValueCurrent;
121     }
122 
setSharedValueCurrent(int sharedValueCurrent)123     public void setSharedValueCurrent(int sharedValueCurrent) {
124         this.mSharedValueCurrent = sharedValueCurrent;
125     }
126 
127     /**
128      * Gets the type of transition to listen to.
129      *
130      * @return ONSTATE_TRANSITION_*
131      */
getStateTransition()132     public int getStateTransition() {
133         return mOnStateTransition;
134     }
135 
136     /**
137      * Sets the type of transition to listen to.
138      *
139      * @param stateTransition
140      */
setStateTransition(int stateTransition)141     public void setStateTransition(int stateTransition) {
142         this.mOnStateTransition = stateTransition;
143     }
144 
145     /**
146      * Gets the SharedValue it will be listening for.
147      *
148      * @return
149      */
getSharedValue()150     public int getSharedValue() {
151         return mSharedValueTarget;
152     }
153 
154     /**
155      * sets the SharedValue it will be listening for.
156      */
setSharedValue(int sharedValue)157     public void setSharedValue(int sharedValue) {
158         this.mSharedValueTarget = sharedValue;
159     }
160 
161     /**
162      * Gets the ID of the SharedValue it will be listening for.
163      *
164      * @return the id of the shared value
165      */
getSharedValueID()166     public int getSharedValueID() {
167         return mSharedValueID;
168     }
169 
170     /**
171      * sets the ID of the SharedValue it will be listening for.
172      */
setSharedValueID(int sharedValueID)173     public void setSharedValueID(int sharedValueID) {
174         this.mSharedValueID = sharedValueID;
175     }
176 
177     /**
178      * debug string for a ViewTransition
179      * @return
180      */
181     @Override
toString()182     public String toString() {
183         return "ViewTransition(" + Debug.getName(mContext, mId) + ")";
184     }
185 
getInterpolator(Context context)186     Interpolator getInterpolator(Context context) {
187         switch (mDefaultInterpolator) {
188             case SPLINE_STRING:
189                 final Easing easing = Easing.getInterpolator(mDefaultInterpolatorString);
190                 return new Interpolator() {
191                     @Override
192                     public float getInterpolation(float v) {
193                         return (float) easing.get(v);
194                     }
195                 };
196             case INTERPOLATOR_REFERENCE_ID:
197                 return AnimationUtils.loadInterpolator(context,
198                         mDefaultInterpolatorID);
199             case EASE_IN_OUT:
200                 return new AccelerateDecelerateInterpolator();
201             case EASE_IN:
202                 return new AccelerateInterpolator();
203             case EASE_OUT:
204                 return new DecelerateInterpolator();
205             case LINEAR:
206                 return null;
207             case ANTICIPATE:
208                 return new AnticipateInterpolator();
209             case OVERSHOOT:
210                 return new OvershootInterpolator();
211             case BOUNCE:
212                 return new BounceInterpolator();
213         }
214         return null;
215     }
216 
217     ViewTransition(Context context, XmlPullParser parser) {
218         mContext = context;
219         try {
220             for (int eventType = parser.getEventType();
221                     eventType != XmlResourceParser.END_DOCUMENT;
222                     eventType = parser.next()) {
223                 switch (eventType) {
224                     case XmlResourceParser.START_DOCUMENT:
225                     case XmlResourceParser.TEXT:
226                         break;
227                     case XmlResourceParser.START_TAG:
228                         String tagName = parser.getName();
229                         switch (tagName) {
230                             case VIEW_TRANSITION_TAG:
231                                 parseViewTransitionTags(context, parser);
232                                 break;
233                             case KEY_FRAME_SET_TAG:
234                                 mKeyFrames = new KeyFrames(context, parser);
235                                 break;
236                             case CONSTRAINT_OVERRIDE:
237                                 mConstraintDelta = ConstraintSet.buildDelta(context, parser);
238                                 break;
239                             case CUSTOM_ATTRIBUTE:
240                             case CUSTOM_METHOD:
241                                 ConstraintAttribute.parse(context, parser,
242                                         mConstraintDelta.mCustomConstraints);
243                                 break;
244                             default:
245                                 Log.e(TAG, Debug.getLoc() + " unknown tag " + tagName);
246                                 Log.e(TAG, ".xml:" + parser.getLineNumber());
247                         }
248 
249                         break;
250                     case XmlResourceParser.END_TAG:
251                         if (VIEW_TRANSITION_TAG.equals(parser.getName())) {
252                             return;
253                         }
254                         break;
255                 }
256             }
257         } catch (XmlPullParserException e) {
258             Log.e(TAG, "Error parsing XML resource", e);
259         } catch (IOException e) {
260             Log.e(TAG, "Error parsing XML resource", e);
261         }
262     }
263 
264     private void parseViewTransitionTags(Context context, XmlPullParser parser) {
265         AttributeSet attrs = Xml.asAttributeSet(parser);
266         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewTransition);
267         final int count = a.getIndexCount();
268         for (int i = 0; i < count; i++) {
269             int attr = a.getIndex(i);
270             if (attr == R.styleable.ViewTransition_android_id) {
271                 mId = a.getResourceId(attr, mId);
272             } else if (attr == R.styleable.ViewTransition_motionTarget) {
273                 if (MotionLayout.IS_IN_EDIT_MODE) {
274                     mTargetId = a.getResourceId(attr, mTargetId);
275                     if (mTargetId == -1) {
276                         mTargetString = a.getString(attr);
277                     }
278                 } else {
279                     if (a.peekValue(attr).type == TypedValue.TYPE_STRING) {
280                         mTargetString = a.getString(attr);
281                     } else {
282                         mTargetId = a.getResourceId(attr, mTargetId);
283                     }
284                 }
285             } else if (attr == R.styleable.ViewTransition_onStateTransition) {
286                 mOnStateTransition = a.getInt(attr, mOnStateTransition);
287             } else if (attr == R.styleable.ViewTransition_transitionDisable) {
288                 mDisabled = a.getBoolean(attr, mDisabled);
289             } else if (attr == R.styleable.ViewTransition_pathMotionArc) {
290                 mPathMotionArc = a.getInt(attr, mPathMotionArc);
291             } else if (attr == R.styleable.ViewTransition_duration) {
292                 mDuration = a.getInt(attr, mDuration);
293             } else if (attr == R.styleable.ViewTransition_upDuration) {
294                 mUpDuration = a.getInt(attr, mUpDuration);
295             } else if (attr == R.styleable.ViewTransition_viewTransitionMode) {
296                 mViewTransitionMode = a.getInt(attr, mViewTransitionMode);
297             } else if (attr == R.styleable.ViewTransition_motionInterpolator) {
298                 TypedValue type = a.peekValue(attr);
299                 if (type.type == TypedValue.TYPE_REFERENCE) {
300                     mDefaultInterpolatorID = a.getResourceId(attr, -1);
301                     if (mDefaultInterpolatorID != UNSET) {
302                         mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID;
303                     }
304                 } else if (type.type == TypedValue.TYPE_STRING) {
305                     mDefaultInterpolatorString = a.getString(attr);
306                     if (mDefaultInterpolatorString != null
307                             && mDefaultInterpolatorString.indexOf("/") > 0) {
308                         mDefaultInterpolatorID = a.getResourceId(attr, UNSET);
309                         mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID;
310                     } else {
311                         mDefaultInterpolator = SPLINE_STRING;
312                     }
313                 } else {
314                     mDefaultInterpolator = a.getInteger(attr, mDefaultInterpolator);
315                 }
316             } else if (attr == R.styleable.ViewTransition_setsTag) {
317                 mSetsTag = a.getResourceId(attr, mSetsTag);
318             } else if (attr == R.styleable.ViewTransition_clearsTag) {
319                 mClearsTag = a.getResourceId(attr, mClearsTag);
320             } else if (attr == R.styleable.ViewTransition_ifTagSet) {
321                 mIfTagSet = a.getResourceId(attr, mIfTagSet);
322             } else if (attr == R.styleable.ViewTransition_ifTagNotSet) {
323                 mIfTagNotSet = a.getResourceId(attr, mIfTagNotSet);
324             } else if (attr == R.styleable.ViewTransition_SharedValueId) {
325                 mSharedValueID = a.getResourceId(attr, mSharedValueID);
326             } else if (attr == R.styleable.ViewTransition_SharedValue) {
327                 mSharedValueTarget = a.getInteger(attr, mSharedValueTarget);
328             }
329         }
330         a.recycle();
331     }
332 
333     void applyIndependentTransition(ViewTransitionController controller,
334                                     MotionLayout motionLayout,
335                                     View view) {
336         MotionController motionController = new MotionController(view);
337         motionController.setBothStates(view);
338         mKeyFrames.addAllFrames(motionController);
339         motionController.setup(motionLayout.getWidth(), motionLayout.getHeight(),
340                 mDuration, System.nanoTime());
341         new Animate(controller, motionController,
342                 mDuration, mUpDuration, mOnStateTransition,
343                 getInterpolator(motionLayout.getContext()), mSetsTag, mClearsTag);
344     }
345 
346     static class Animate {
347         private final int mSetsTag;
348         private final int mClearsTag;
349         long mStart;
350         MotionController mMC;
351         int mDuration;
352         int mUpDuration;
353         KeyCache mCache = new KeyCache();
354         ViewTransitionController mVtController;
355         Interpolator mInterpolator;
356         boolean mReverse = false;
357         float mPosition;
358         float mDpositionDt;
359         long mLastRender;
360         Rect mTempRec = new Rect();
361         boolean mHoldAt100 = false;
362 
363         Animate(ViewTransitionController controller,
364                 MotionController motionController,
365                 int duration, int upDuration, int mode,
366                 Interpolator interpolator, int setTag, int clearTag) {
367             mVtController = controller;
368             mMC = motionController;
369             mDuration = duration;
370             mUpDuration = upDuration;
371             mStart = System.nanoTime();
372             mLastRender = mStart;
373             mVtController.addAnimation(this);
374             mInterpolator = interpolator;
375             mSetsTag = setTag;
376             mClearsTag = clearTag;
377             if (mode == ONSTATE_ACTION_DOWN_UP) {
378                 mHoldAt100 = true;
379             }
380             mDpositionDt = (duration == 0) ? Float.MAX_VALUE : 1f / duration;
381             mutate();
382         }
383 
384         void reverse(boolean dir) {
385             mReverse = dir;
386             if (mReverse && mUpDuration != UNSET) {
387                 mDpositionDt = (mUpDuration == 0) ? Float.MAX_VALUE : 1f / mUpDuration;
388             }
389             mVtController.invalidate();
390             mLastRender = System.nanoTime();
391         }
392 
393         void mutate() {
394             if (mReverse) {
395                 mutateReverse();
396             } else {
397                 mutateForward();
398             }
399         }
400 
401         void mutateReverse() {
402             long current = System.nanoTime();
403             long elapse = current - mLastRender;
404             mLastRender = current;
405 
406             mPosition -= ((float) (elapse * 1E-6)) * mDpositionDt;
407             if (mPosition < 0.0f) {
408                 mPosition = 0.0f;
409             }
410 
411             float ipos = (mInterpolator == null) ? mPosition
412                     : mInterpolator.getInterpolation(mPosition);
413             boolean repaint = mMC.interpolate(mMC.mView, ipos, current, mCache);
414 
415             if (mPosition <= 0) {
416                 if (mSetsTag != UNSET) {
417                     mMC.getView().setTag(mSetsTag, System.nanoTime());
418                 }
419                 if (mClearsTag != UNSET) {
420                     mMC.getView().setTag(mClearsTag, null);
421                 }
422                 mVtController.removeAnimation(this);
423             }
424             if (mPosition > 0f || repaint) {
425                 mVtController.invalidate();
426             }
427         }
428 
429         void mutateForward() {
430 
431             long current = System.nanoTime();
432             long elapse = current - mLastRender;
433             mLastRender = current;
434 
435             mPosition += ((float) (elapse * 1E-6)) * mDpositionDt;
436             if (mPosition >= 1.0f) {
437                 mPosition = 1.0f;
438             }
439 
440             float ipos = (mInterpolator == null) ? mPosition
441                     : mInterpolator.getInterpolation(mPosition);
442             boolean repaint = mMC.interpolate(mMC.mView, ipos, current, mCache);
443 
444             if (mPosition >= 1) {
445                 if (mSetsTag != UNSET) {
446                     mMC.getView().setTag(mSetsTag, System.nanoTime());
447                 }
448                 if (mClearsTag != UNSET) {
449                     mMC.getView().setTag(mClearsTag, null);
450                 }
451                 if (!mHoldAt100) {
452                     mVtController.removeAnimation(this);
453                 }
454             }
455             if (mPosition < 1f || repaint) {
456                 mVtController.invalidate();
457             }
458         }
459 
460         public void reactTo(int action, float x, float y) {
461             switch (action) {
462                 case MotionEvent.ACTION_UP:
463                     if (!mReverse) {
464                         reverse(true);
465                     }
466                     return;
467                 case MotionEvent.ACTION_MOVE:
468                     View view = mMC.getView();
469                     view.getHitRect(mTempRec);
470                     if (!mTempRec.contains((int) x, (int) y)) {
471                         if (!mReverse) {
472                             reverse(true);
473                         }
474                     }
475             }
476         }
477     }
478 
479     void applyTransition(ViewTransitionController controller,
480                          MotionLayout layout,
481                          int fromId,
482                          ConstraintSet current,
483                          View... views) {
484         if (mDisabled) {
485             return;
486         }
487         if (mViewTransitionMode == VIEWTRANSITIONMODE_NOSTATE) {
488             applyIndependentTransition(controller, layout, views[0]);
489             return;
490         }
491         if (mViewTransitionMode == VIEWTRANSITIONMODE_ALLSTATES) {
492             int[] ids = layout.getConstraintSetIds();
493             for (int i = 0; i < ids.length; i++) {
494                 int id = ids[i];
495                 if (id == fromId) {
496                     continue;
497                 }
498                 ConstraintSet cSet = layout.getConstraintSet(id);
499                 for (View view : views) {
500                     ConstraintSet.Constraint constraint = cSet.getConstraint(view.getId());
501                     if (mConstraintDelta != null) {
502                         mConstraintDelta.applyDelta(constraint);
503                         constraint.mCustomConstraints.putAll(mConstraintDelta.mCustomConstraints);
504                     }
505                 }
506             }
507         }
508 
509         ConstraintSet transformedState = new ConstraintSet();
510         transformedState.clone(current);
511         for (View view : views) {
512             ConstraintSet.Constraint constraint = transformedState.getConstraint(view.getId());
513             if (mConstraintDelta != null) {
514                 mConstraintDelta.applyDelta(constraint);
515                 constraint.mCustomConstraints.putAll(mConstraintDelta.mCustomConstraints);
516             }
517         }
518 
519         layout.updateState(fromId, transformedState);
520         layout.updateState(R.id.view_transition, current);
521         layout.setState(R.id.view_transition, -1, -1);
522         MotionScene.Transition tmpTransition =
523                 new MotionScene.Transition(-1, layout.mScene, R.id.view_transition, fromId);
524         for (View view : views) {
525             updateTransition(tmpTransition, view);
526         }
527         layout.setTransition(tmpTransition);
528         layout.transitionToEnd(() -> {
529             if (mSetsTag != UNSET) {
530                 for (View view : views) {
531                     view.setTag(mSetsTag, System.nanoTime());
532                 }
533             }
534             if (mClearsTag != UNSET) {
535                 for (View view : views) {
536                     view.setTag(mClearsTag, null);
537                 }
538             }
539         });
540     }
541 
542     private void updateTransition(MotionScene.Transition transition, View view) {
543         if (mDuration != -1) {
544             transition.setDuration(mDuration);
545         }
546         transition.setPathMotionArc(mPathMotionArc);
547         transition.setInterpolatorInfo(mDefaultInterpolator,
548                 mDefaultInterpolatorString, mDefaultInterpolatorID);
549         int id = view.getId();
550         if (mKeyFrames != null) {
551             ArrayList<Key> keys = mKeyFrames.getKeyFramesForView(KeyFrames.UNSET);
552             KeyFrames keyFrames = new KeyFrames();
553             for (Key key : keys) {
554                 keyFrames.addKey(key.clone().setViewId(id));
555             }
556 
557             transition.addKeyFrame(keyFrames);
558         }
559     }
560 
561     int getId() {
562         return mId;
563     }
564 
565     void setId(int id) {
566         this.mId = id;
567     }
568 
569     boolean matchesView(View view) {
570         if (view == null) {
571             return false;
572         }
573         if (mTargetId == -1 && mTargetString == null) {
574             return false;
575         }
576         if (!checkTags(view)) {
577             return false;
578         }
579         if (view.getId() == mTargetId) {
580             return true;
581         }
582         if (mTargetString == null) {
583             return false;
584         }
585         ViewGroup.LayoutParams lp = view.getLayoutParams();
586         if (lp instanceof ConstraintLayout.LayoutParams) {
587             String tag = ((ConstraintLayout.LayoutParams) view.getLayoutParams()).constraintTag;
588             if (tag != null && tag.matches(mTargetString)) {
589                 return true;
590             }
591         }
592         return false;
593     }
594 
595     boolean supports(int action) {
596         if (mOnStateTransition == ONSTATE_ACTION_DOWN) {
597             return action == MotionEvent.ACTION_DOWN;
598         }
599         if (mOnStateTransition == ONSTATE_ACTION_UP) {
600             return action == MotionEvent.ACTION_UP;
601         }
602         if (mOnStateTransition == ONSTATE_ACTION_DOWN_UP) {
603             return action == MotionEvent.ACTION_DOWN;
604         }
605         return false;
606     }
607 
608     boolean isEnabled() {
609         return !mDisabled;
610     }
611 
612     void setEnabled(boolean enable) {
613         this.mDisabled = !enable;
614     }
615 
616     boolean checkTags(View view) {
617 
618         boolean set = (mIfTagSet == UNSET) ? true : (null != view.getTag(mIfTagSet));
619         boolean notSet = (mIfTagNotSet == UNSET) ? true : null == view.getTag(mIfTagNotSet);
620         return set && notSet;
621     }
622 }
623