1 /*
2  * Copyright (C) 2018 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.graphics.RectF;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.util.SparseIntArray;
25 import android.util.TypedValue;
26 import android.view.View;
27 import android.view.ViewGroup;
28 
29 import androidx.constraintlayout.motion.utils.ViewSpline;
30 import androidx.constraintlayout.widget.ConstraintAttribute;
31 import androidx.constraintlayout.widget.R;
32 
33 import java.lang.reflect.Method;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.Locale;
37 
38 /**
39  * Defines container for a key frame of for storing KeyAttributes.
40  * KeyAttributes change post layout values of a view.
41  *
42  *
43  */
44 
45 public class KeyTrigger extends Key {
46     public static final String VIEW_TRANSITION_ON_CROSS = "viewTransitionOnCross";
47     public static final String VIEW_TRANSITION_ON_POSITIVE_CROSS = "viewTransitionOnPositiveCross";
48     public static final String VIEW_TRANSITION_ON_NEGATIVE_CROSS = "viewTransitionOnNegativeCross";
49     public static final String POST_LAYOUT = "postLayout";
50     public static final String TRIGGER_SLACK = "triggerSlack";
51     public static final String TRIGGER_COLLISION_VIEW = "triggerCollisionView";
52     public static final String TRIGGER_COLLISION_ID = "triggerCollisionId";
53     public static final String TRIGGER_ID = "triggerID";
54     public static final String POSITIVE_CROSS = "positiveCross";
55     public static final String NEGATIVE_CROSS = "negativeCross";
56     public static final String TRIGGER_RECEIVER = "triggerReceiver";
57     public static final String CROSS = "CROSS";
58     public static final int KEY_TYPE = 5;
59     static final String NAME = "KeyTrigger";
60     private static final String TAG = "KeyTrigger";
61     float mTriggerSlack = .1f;
62     int mViewTransitionOnNegativeCross = UNSET;
63     int mViewTransitionOnPositiveCross = UNSET;
64     int mViewTransitionOnCross = UNSET;
65     RectF mCollisionRect = new RectF();
66     RectF mTargetRect = new RectF();
67     HashMap<String, Method> mMethodHashMap = new HashMap<>();
68     private int mCurveFit = -1;
69     private String mCross = null;
70     private int mTriggerReceiver = UNSET;
71     private String mNegativeCross = null;
72     private String mPositiveCross = null;
73     private int mTriggerID = UNSET;
74     private int mTriggerCollisionId = UNSET;
75     private View mTriggerCollisionView = null;
76     private boolean mFireCrossReset = true;
77     private boolean mFireNegativeReset = true;
78     private boolean mFirePositiveReset = true;
79     private float mFireThreshold = Float.NaN;
80     private float mFireLastPos;
81     private boolean mPostLayout = false;
82 
83     {
84         mType = KEY_TYPE;
85         mCustomConstraints = new HashMap<>();
86     }
87 
88     @Override
load(Context context, AttributeSet attrs)89     public void load(Context context, AttributeSet attrs) {
90         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyTrigger);
91         Loader.read(this, a, context);
92     }
93 
94     /**
95      * Gets the curve fit type this drives the interpolation
96      *
97      * @return
98      */
getCurveFit()99     int getCurveFit() {
100         return mCurveFit;
101     }
102 
103     @Override
getAttributeNames(HashSet<String> attributes)104     public void getAttributeNames(HashSet<String> attributes) {
105     }
106 
107     @Override
addValues(HashMap<String, ViewSpline> splines)108     public void addValues(HashMap<String, ViewSpline> splines) {
109     }
110 
111     @Override
setValue(String tag, Object value)112     public void setValue(String tag, Object value) {
113         switch (tag) {
114             case CROSS:
115                 mCross = value.toString();
116                 break;
117             case TRIGGER_RECEIVER:
118                 mTriggerReceiver = toInt(value);
119                 break;
120             case NEGATIVE_CROSS:
121                 mNegativeCross = value.toString();
122                 break;
123             case POSITIVE_CROSS:
124                 mPositiveCross = value.toString();
125                 break;
126             case TRIGGER_ID:
127                 mTriggerID = toInt(value);
128                 break;
129             case TRIGGER_COLLISION_ID:
130                 mTriggerCollisionId = toInt(value);
131                 break;
132             case TRIGGER_COLLISION_VIEW:
133                 mTriggerCollisionView = (View) value;
134                 break;
135             case TRIGGER_SLACK:
136                 mTriggerSlack = toFloat(value);
137                 break;
138             case POST_LAYOUT:
139                 mPostLayout = toBoolean(value);
140                 break;
141             case VIEW_TRANSITION_ON_NEGATIVE_CROSS:
142                 mViewTransitionOnNegativeCross = toInt(value);
143                 break;
144             case VIEW_TRANSITION_ON_POSITIVE_CROSS:
145                 mViewTransitionOnPositiveCross = toInt(value);
146                 break;
147             case VIEW_TRANSITION_ON_CROSS:
148                 mViewTransitionOnCross = toInt(value);
149                 break;
150 
151         }
152     }
153 
setUpRect(RectF rect, View child, boolean postLayout)154     private void setUpRect(RectF rect, View child, boolean postLayout) {
155         rect.top = child.getTop();
156         rect.bottom = child.getBottom();
157         rect.left = child.getLeft();
158         rect.right = child.getRight();
159         if (postLayout) {
160             child.getMatrix().mapRect(rect);
161         }
162     }
163 
164     /**
165      * This fires the keyTriggers associated with this view at that position
166      *
167      * @param pos   the progress
168      * @param child the view
169      */
conditionallyFire(float pos, View child)170     public void conditionallyFire(float pos, View child) {
171         boolean fireCross = false;
172         boolean fireNegative = false;
173         boolean firePositive = false;
174 
175         if (mTriggerCollisionId != UNSET) {
176             if (mTriggerCollisionView == null) {
177                 mTriggerCollisionView =
178                         ((ViewGroup) child.getParent()).findViewById(mTriggerCollisionId);
179             }
180 
181             setUpRect(mCollisionRect, mTriggerCollisionView, mPostLayout);
182             setUpRect(mTargetRect, child, mPostLayout);
183             boolean in = mCollisionRect.intersect(mTargetRect);
184             // TODO scale by mTriggerSlack
185             if (in) {
186                 if (mFireCrossReset) {
187                     fireCross = true;
188                     mFireCrossReset = false;
189                 }
190                 if (mFirePositiveReset) {
191                     firePositive = true;
192                     mFirePositiveReset = false;
193                 }
194                 mFireNegativeReset = true;
195             } else {
196                 if (!mFireCrossReset) {
197                     fireCross = true;
198                     mFireCrossReset = true;
199                 }
200                 if (mFireNegativeReset) {
201                     fireNegative = true;
202                     mFireNegativeReset = false;
203                 }
204                 mFirePositiveReset = true;
205             }
206 
207         } else {
208 
209             // Check for crossing
210             if (mFireCrossReset) {
211 
212                 float offset = pos - mFireThreshold;
213                 float lastOffset = mFireLastPos - mFireThreshold;
214 
215                 if (offset * lastOffset < 0) { // just crossed the threshold
216                     fireCross = true;
217                     mFireCrossReset = false;
218                 }
219             } else {
220                 if (Math.abs(pos - mFireThreshold) > mTriggerSlack) {
221                     mFireCrossReset = true;
222                 }
223             }
224 
225             // Check for negative crossing
226             if (mFireNegativeReset) {
227                 float offset = pos - mFireThreshold;
228                 float lastOffset = mFireLastPos - mFireThreshold;
229                 if (offset * lastOffset < 0 && offset < 0) { // just crossed the threshold
230                     fireNegative = true;
231                     mFireNegativeReset = false;
232                 }
233             } else {
234                 if (Math.abs(pos - mFireThreshold) > mTriggerSlack) {
235                     mFireNegativeReset = true;
236                 }
237             }
238             // Check for positive crossing
239             if (mFirePositiveReset) {
240                 float offset = pos - mFireThreshold;
241                 float lastOffset = mFireLastPos - mFireThreshold;
242                 if (offset * lastOffset < 0 && offset > 0) { // just crossed the threshold
243                     firePositive = true;
244                     mFirePositiveReset = false;
245                 }
246             } else {
247                 if (Math.abs(pos - mFireThreshold) > mTriggerSlack) {
248                     mFirePositiveReset = true;
249                 }
250             }
251         }
252         mFireLastPos = pos;
253 
254         if (fireNegative || fireCross || firePositive) {
255             ((MotionLayout) child.getParent()).fireTrigger(mTriggerID, firePositive, pos);
256         }
257         View call = (mTriggerReceiver == UNSET) ? child :
258                 ((MotionLayout) child.getParent()).findViewById(mTriggerReceiver);
259 
260         if (fireNegative) {
261             if (mNegativeCross != null) {
262                 fire(mNegativeCross, call);
263             }
264             if (mViewTransitionOnNegativeCross != UNSET) {
265                 ((MotionLayout) child.getParent()).viewTransition(mViewTransitionOnNegativeCross,
266                         call);
267             }
268         }
269         if (firePositive) {
270             if (mPositiveCross != null) {
271                 fire(mPositiveCross, call);
272             }
273             if (mViewTransitionOnPositiveCross != UNSET) {
274                 ((MotionLayout) child.getParent()).viewTransition(mViewTransitionOnPositiveCross,
275                         call);
276             }
277         }
278         if (fireCross) {
279             if (mCross != null) {
280                 fire(mCross, call);
281             }
282             if (mViewTransitionOnCross != UNSET) {
283                 ((MotionLayout) child.getParent()).viewTransition(mViewTransitionOnCross, call);
284             }
285         }
286 
287     }
288 
fire(String str, View call)289     private void fire(String str, View call) {
290         if (str == null) {
291             return;
292         }
293         if (str.startsWith(".")) {
294             fireCustom(str, call);
295             return;
296         }
297         Method method = null;
298         if (mMethodHashMap.containsKey(str)) {
299             method = mMethodHashMap.get(str);
300             if (method == null) { // we looked up and did not find
301                 return;
302             }
303         }
304         if (method == null) {
305             try {
306                 method = call.getClass().getMethod(str);
307                 mMethodHashMap.put(str, method);
308             } catch (NoSuchMethodException e) {
309                 mMethodHashMap.put(str, null); // record that we could not get this method
310                 Log.e(TAG, "Could not find method \"" + str + "\"" + "on class "
311                         + call.getClass().getSimpleName() + " " + Debug.getName(call));
312                 return;
313             }
314         }
315         try {
316             method.invoke(call);
317         } catch (Exception e) {
318             Log.e(TAG, "Exception in call \"" + mCross + "\"" + "on class "
319                     + call.getClass().getSimpleName() + " " + Debug.getName(call));
320         }
321     }
322 
fireCustom(String str, View view)323     private void fireCustom(String str, View view) {
324         boolean callAll = str.length() == 1;
325         if (!callAll) {
326             str = str.substring(1).toLowerCase(Locale.ROOT);
327         }
328         for (String name : mCustomConstraints.keySet()) {
329             String lowerCase = name.toLowerCase(Locale.ROOT);
330             if (callAll || lowerCase.matches(str)) {
331                 ConstraintAttribute custom = mCustomConstraints.get(name);
332                 if (custom != null) {
333                     custom.applyCustom(view);
334                 }
335             }
336         }
337     }
338 
339     /**
340      * Copy the key
341      *
342      * @param src to be copied
343      * @return self
344      */
345     @Override
copy(Key src)346     public Key copy(Key src) {
347         super.copy(src);
348         KeyTrigger k = (KeyTrigger) src;
349         mCurveFit = k.mCurveFit;
350         mCross = k.mCross;
351         mTriggerReceiver = k.mTriggerReceiver;
352         mNegativeCross = k.mNegativeCross;
353         mPositiveCross = k.mPositiveCross;
354         mTriggerID = k.mTriggerID;
355         mTriggerCollisionId = k.mTriggerCollisionId;
356         mTriggerCollisionView = k.mTriggerCollisionView;
357         mTriggerSlack = k.mTriggerSlack;
358         mFireCrossReset = k.mFireCrossReset;
359         mFireNegativeReset = k.mFireNegativeReset;
360         mFirePositiveReset = k.mFirePositiveReset;
361         mFireThreshold = k.mFireThreshold;
362         mFireLastPos = k.mFireLastPos;
363         mPostLayout = k.mPostLayout;
364         mCollisionRect = k.mCollisionRect;
365         mTargetRect = k.mTargetRect;
366         mMethodHashMap = k.mMethodHashMap;
367         return this;
368     }
369 
370     /**
371      * Clone this KeyAttributes
372      *
373      * @return
374      */
375     @Override
clone()376     public Key clone() {
377         return new KeyTrigger().copy(this);
378     }
379 
380     private static class Loader {
381         private static final int NEGATIVE_CROSS = 1;
382         private static final int POSITIVE_CROSS = 2;
383         private static final int CROSS = 4;
384         private static final int TRIGGER_SLACK = 5;
385         private static final int TRIGGER_ID = 6;
386         private static final int TARGET_ID = 7;
387         private static final int FRAME_POS = 8;
388         private static final int COLLISION = 9;
389         private static final int POST_LAYOUT = 10;
390         private static final int TRIGGER_RECEIVER = 11;
391         private static final int VT_CROSS = 12;
392         private static final int VT_NEGATIVE_CROSS = 13;
393         private static final int VT_POSITIVE_CROSS = 14;
394 
395         private static SparseIntArray sAttrMap = new SparseIntArray();
396 
397         static {
sAttrMap.append(R.styleable.KeyTrigger_framePosition, FRAME_POS)398             sAttrMap.append(R.styleable.KeyTrigger_framePosition, FRAME_POS);
sAttrMap.append(R.styleable.KeyTrigger_onCross, CROSS)399             sAttrMap.append(R.styleable.KeyTrigger_onCross, CROSS);
sAttrMap.append(R.styleable.KeyTrigger_onNegativeCross, NEGATIVE_CROSS)400             sAttrMap.append(R.styleable.KeyTrigger_onNegativeCross, NEGATIVE_CROSS);
sAttrMap.append(R.styleable.KeyTrigger_onPositiveCross, POSITIVE_CROSS)401             sAttrMap.append(R.styleable.KeyTrigger_onPositiveCross, POSITIVE_CROSS);
sAttrMap.append(R.styleable.KeyTrigger_motionTarget, TARGET_ID)402             sAttrMap.append(R.styleable.KeyTrigger_motionTarget, TARGET_ID);
sAttrMap.append(R.styleable.KeyTrigger_triggerId, TRIGGER_ID)403             sAttrMap.append(R.styleable.KeyTrigger_triggerId, TRIGGER_ID);
sAttrMap.append(R.styleable.KeyTrigger_triggerSlack, TRIGGER_SLACK)404             sAttrMap.append(R.styleable.KeyTrigger_triggerSlack, TRIGGER_SLACK);
sAttrMap.append(R.styleable.KeyTrigger_motion_triggerOnCollision, COLLISION)405             sAttrMap.append(R.styleable.KeyTrigger_motion_triggerOnCollision, COLLISION);
sAttrMap.append(R.styleable.KeyTrigger_motion_postLayoutCollision, POST_LAYOUT)406             sAttrMap.append(R.styleable.KeyTrigger_motion_postLayoutCollision, POST_LAYOUT);
sAttrMap.append(R.styleable.KeyTrigger_triggerReceiver, TRIGGER_RECEIVER)407             sAttrMap.append(R.styleable.KeyTrigger_triggerReceiver, TRIGGER_RECEIVER);
sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnCross, VT_CROSS)408             sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnCross, VT_CROSS);
sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnNegativeCross, VT_NEGATIVE_CROSS)409             sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnNegativeCross,
410                     VT_NEGATIVE_CROSS);
sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnPositiveCross, VT_POSITIVE_CROSS)411             sAttrMap.append(R.styleable.KeyTrigger_viewTransitionOnPositiveCross,
412                     VT_POSITIVE_CROSS);
413         }
414 
read(KeyTrigger c, TypedArray a, @SuppressWarnings("unused") Context context)415         public static void read(KeyTrigger c, TypedArray a,
416                 @SuppressWarnings("unused") Context context) {
417             final int n = a.getIndexCount();
418             for (int i = 0; i < n; i++) {
419                 int attr = a.getIndex(i);
420                 switch (sAttrMap.get(attr)) {
421                     case FRAME_POS:
422                         c.mFramePosition = a.getInteger(attr, c.mFramePosition);
423                         c.mFireThreshold = (c.mFramePosition + .5f) / 100f;
424                         break;
425                     case TARGET_ID:
426                         if (MotionLayout.IS_IN_EDIT_MODE) {
427                             c.mTargetId = a.getResourceId(attr, c.mTargetId);
428                             if (c.mTargetId == -1) {
429                                 c.mTargetString = a.getString(attr);
430                             }
431                         } else {
432                             if (a.peekValue(attr).type == TypedValue.TYPE_STRING) {
433                                 c.mTargetString = a.getString(attr);
434                             } else {
435                                 c.mTargetId = a.getResourceId(attr, c.mTargetId);
436                             }
437                         }
438                         break;
439                     case NEGATIVE_CROSS:
440                         c.mNegativeCross = a.getString(attr);
441                         break;
442                     case POSITIVE_CROSS:
443                         c.mPositiveCross = a.getString(attr);
444                         break;
445                     case CROSS:
446                         c.mCross = a.getString(attr);
447                         break;
448                     case TRIGGER_SLACK:
449                         c.mTriggerSlack = a.getFloat(attr, c.mTriggerSlack);
450                         break;
451                     case TRIGGER_ID:
452                         c.mTriggerID = a.getResourceId(attr, c.mTriggerID);
453                         break;
454                     case COLLISION:
455                         c.mTriggerCollisionId = a.getResourceId(attr, c.mTriggerCollisionId);
456                         break;
457                     case POST_LAYOUT:
458                         c.mPostLayout = a.getBoolean(attr, c.mPostLayout);
459                         break;
460                     case TRIGGER_RECEIVER:
461                         c.mTriggerReceiver = a.getResourceId(attr, c.mTriggerReceiver);
462                         break;
463                     case VT_NEGATIVE_CROSS:
464                         c.mViewTransitionOnNegativeCross = a.getResourceId(attr,
465                                 c.mViewTransitionOnNegativeCross);
466                         break;
467                     case VT_POSITIVE_CROSS:
468                         c.mViewTransitionOnPositiveCross = a.getResourceId(attr,
469                                 c.mViewTransitionOnPositiveCross);
470                         break;
471                     case VT_CROSS:
472                         c.mViewTransitionOnCross = a.getResourceId(attr, c.mViewTransitionOnCross);
473                         break;
474                     default:
475                         Log.e(NAME, "unused attribute 0x" + Integer.toHexString(attr)
476                                 + "   " + sAttrMap.get(attr));
477                         break;
478                 }
479             }
480         }
481     }
482 }
483