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.Xml;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.ViewGroup;
28 
29 import androidx.constraintlayout.widget.R;
30 import androidx.core.widget.NestedScrollView;
31 
32 import org.xmlpull.v1.XmlPullParser;
33 
34 import java.util.Arrays;
35 
36 /**
37  * This class is used to manage Touch behaviour
38  *
39  *
40  */
41 
42 class TouchResponse {
43     private static final String TAG = "TouchResponse";
44     private static final boolean DEBUG = false;
45     private int mTouchAnchorSide = 0;
46     private int mTouchSide = 0;
47     private int mOnTouchUp = 0;
48     private int mTouchAnchorId = MotionScene.UNSET;
49     private int mTouchRegionId = MotionScene.UNSET;
50     private int mLimitBoundsTo = MotionScene.UNSET;
51     private float mTouchAnchorY = 0.5f;
52     private float mTouchAnchorX = 0.5f;
53     float mRotateCenterX = 0.5f;
54     float mRotateCenterY = 0.5f;
55     private int mRotationCenterId = MotionScene.UNSET;
56     boolean mIsRotateMode = false;
57     private float mTouchDirectionX = 0;
58     private float mTouchDirectionY = 1;
59     private boolean mDragStarted = false;
60     private float[] mAnchorDpDt = new float[2];
61     private int[] mTempLoc = new int[2];
62     private float mLastTouchX, mLastTouchY;
63     private final MotionLayout mMotionLayout;
64     private static final int SEC_TO_MILLISECONDS = 1000;
65     private static final float EPSILON = 0.0000001f;
66 
67     private static final float[][] TOUCH_SIDES = {
68             {0.5f, 0.0f}, // top
69             {0.0f, 0.5f}, // left
70             {1.0f, 0.5f}, // right
71             {0.5f, 1.0f}, // bottom
72             {0.5f, 0.5f}, // middle
73             {0.0f, 0.5f}, // start (dynamically updated)
74             {1.0f, 0.5f}, // end  (dynamically updated)
75     };
76     private static final float[][] TOUCH_DIRECTION = {
77             {0.0f, -1.0f}, // up
78             {0.0f, 1.0f}, // down
79             {-1.0f, 0.0f}, // left
80             {1.0f, 0.0f}, // right
81             {-1.0f, 0.0f}, // start (dynamically updated)
82             {1.0f, 0.0f}, // end  (dynamically updated)
83     };
84     @SuppressWarnings("unused")
85     private static final int TOUCH_UP = 0;
86     @SuppressWarnings("unused")
87     private static final int TOUCH_DOWN = 1;
88     private static final int TOUCH_LEFT = 2;
89     private static final int TOUCH_RIGHT = 3;
90     private static final int TOUCH_START = 4;
91     private static final int TOUCH_END = 5;
92 
93     @SuppressWarnings("unused")
94     private static final int SIDE_TOP = 0;
95     private static final int SIDE_LEFT = 1;
96     private static final int SIDE_RIGHT = 2;
97     @SuppressWarnings("unused")
98     private static final int SIDE_BOTTOM = 3;
99     @SuppressWarnings("unused")
100     private static final int SIDE_MIDDLE = 4;
101     private static final int SIDE_START = 5;
102     private static final int SIDE_END = 6;
103 
104     private float mMaxVelocity = 4;
105     private float mMaxAcceleration = 1.2f;
106     private boolean mMoveWhenScrollAtTop = true;
107     private float mDragScale = 1f;
108     private int mFlags = 0;
109     static final int FLAG_DISABLE_POST_SCROLL = 1;
110     static final int FLAG_DISABLE_SCROLL = 2;
111     static final int FLAG_SUPPORT_SCROLL_UP = 4;
112 
113     private float mDragThreshold = 10;
114     private float mSpringDamping = 10;
115     private float mSpringMass = 1;
116     private float mSpringStiffness = Float.NaN;
117     private float mSpringStopThreshold = Float.NaN;
118     private int mSpringBoundary = 0;
119     private int mAutoCompleteMode = COMPLETE_MODE_CONTINUOUS_VELOCITY;
120     public static final int COMPLETE_MODE_CONTINUOUS_VELOCITY = 0;
121     public static final int COMPLETE_MODE_SPRING = 1;
122 
TouchResponse(Context context, MotionLayout layout, XmlPullParser parser)123     TouchResponse(Context context, MotionLayout layout, XmlPullParser parser) {
124         mMotionLayout = layout;
125         fillFromAttributeList(context, Xml.asAttributeSet(parser));
126     }
127 
TouchResponse(MotionLayout layout, OnSwipe onSwipe)128     TouchResponse(MotionLayout layout, OnSwipe onSwipe) {
129         mMotionLayout = layout;
130         mTouchAnchorId = onSwipe.getTouchAnchorId();
131         mTouchAnchorSide = onSwipe.getTouchAnchorSide();
132         if (mTouchAnchorSide != -1) {
133             mTouchAnchorX = TOUCH_SIDES[mTouchAnchorSide][0];
134             mTouchAnchorY = TOUCH_SIDES[mTouchAnchorSide][1];
135         }
136         mTouchSide = onSwipe.getDragDirection();
137         if (mTouchSide < TOUCH_DIRECTION.length) {
138             mTouchDirectionX = TOUCH_DIRECTION[mTouchSide][0];
139             mTouchDirectionY = TOUCH_DIRECTION[mTouchSide][1];
140         } else {
141             mTouchDirectionX = mTouchDirectionY = Float.NaN;
142             mIsRotateMode = true;
143         }
144         mMaxVelocity = onSwipe.getMaxVelocity();
145         mMaxAcceleration = onSwipe.getMaxAcceleration();
146         mMoveWhenScrollAtTop = onSwipe.getMoveWhenScrollAtTop();
147         mDragScale = onSwipe.getDragScale();
148         mDragThreshold = onSwipe.getDragThreshold();
149         mTouchRegionId = onSwipe.getTouchRegionId();
150         mOnTouchUp = onSwipe.getOnTouchUp();
151         mFlags = onSwipe.getNestedScrollFlags();
152         mLimitBoundsTo = onSwipe.getLimitBoundsTo();
153         mRotationCenterId = onSwipe.getRotationCenterId();
154         mSpringBoundary = onSwipe.getSpringBoundary();
155         mSpringDamping = onSwipe.getSpringDamping();
156         mSpringMass = onSwipe.getSpringMass();
157         mSpringStiffness = onSwipe.getSpringStiffness();
158         mSpringStopThreshold = onSwipe.getSpringStopThreshold();
159         mAutoCompleteMode = onSwipe.getAutoCompleteMode();
160     }
161 
setRTL(boolean rtl)162     public void setRTL(boolean rtl) {
163         if (rtl) {
164             TOUCH_DIRECTION[TOUCH_START] = TOUCH_DIRECTION[TOUCH_RIGHT];
165             TOUCH_DIRECTION[TOUCH_END] = TOUCH_DIRECTION[TOUCH_LEFT];
166             TOUCH_SIDES[SIDE_START] = TOUCH_SIDES[SIDE_RIGHT];
167             TOUCH_SIDES[SIDE_END] = TOUCH_SIDES[SIDE_LEFT];
168         } else {
169             TOUCH_DIRECTION[TOUCH_START] = TOUCH_DIRECTION[TOUCH_LEFT];
170             TOUCH_DIRECTION[TOUCH_END] = TOUCH_DIRECTION[TOUCH_RIGHT];
171             TOUCH_SIDES[SIDE_START] = TOUCH_SIDES[SIDE_LEFT];
172             TOUCH_SIDES[SIDE_END] = TOUCH_SIDES[SIDE_RIGHT];
173         }
174 
175         mTouchAnchorX = TOUCH_SIDES[mTouchAnchorSide][0];
176         mTouchAnchorY = TOUCH_SIDES[mTouchAnchorSide][1];
177         if (mTouchSide >= TOUCH_DIRECTION.length) {
178             return;
179         }
180         mTouchDirectionX = TOUCH_DIRECTION[mTouchSide][0];
181         mTouchDirectionY = TOUCH_DIRECTION[mTouchSide][1];
182     }
183 
fillFromAttributeList(Context context, AttributeSet attrs)184     private void fillFromAttributeList(Context context, AttributeSet attrs) {
185         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.OnSwipe);
186         fill(a);
187         a.recycle();
188     }
189 
fill(TypedArray a)190     private void fill(TypedArray a) {
191         final int count = a.getIndexCount();
192         for (int i = 0; i < count; i++) {
193             int attr = a.getIndex(i);
194             if (attr == R.styleable.OnSwipe_touchAnchorId) {
195                 mTouchAnchorId = a.getResourceId(attr, mTouchAnchorId);
196             } else if (attr == R.styleable.OnSwipe_touchAnchorSide) {
197                 mTouchAnchorSide = a.getInt(attr, mTouchAnchorSide);
198                 mTouchAnchorX = TOUCH_SIDES[mTouchAnchorSide][0];
199                 mTouchAnchorY = TOUCH_SIDES[mTouchAnchorSide][1];
200             } else if (attr == R.styleable.OnSwipe_dragDirection) {
201                 mTouchSide = a.getInt(attr, mTouchSide);
202                 if (mTouchSide < TOUCH_DIRECTION.length) {
203                     mTouchDirectionX = TOUCH_DIRECTION[mTouchSide][0];
204                     mTouchDirectionY = TOUCH_DIRECTION[mTouchSide][1];
205                 } else {
206                     mTouchDirectionX = mTouchDirectionY = Float.NaN;
207                     mIsRotateMode = true;
208                 }
209             } else if (attr == R.styleable.OnSwipe_maxVelocity) {
210                 mMaxVelocity = a.getFloat(attr, mMaxVelocity);
211             } else if (attr == R.styleable.OnSwipe_maxAcceleration) {
212                 mMaxAcceleration = a.getFloat(attr, mMaxAcceleration);
213             } else if (attr == R.styleable.OnSwipe_moveWhenScrollAtTop) {
214                 mMoveWhenScrollAtTop = a.getBoolean(attr, mMoveWhenScrollAtTop);
215             } else if (attr == R.styleable.OnSwipe_dragScale) {
216                 mDragScale = a.getFloat(attr, mDragScale);
217             } else if (attr == R.styleable.OnSwipe_dragThreshold) {
218                 mDragThreshold = a.getFloat(attr, mDragThreshold);
219             } else if (attr == R.styleable.OnSwipe_touchRegionId) {
220                 mTouchRegionId = a.getResourceId(attr, mTouchRegionId);
221             } else if (attr == R.styleable.OnSwipe_onTouchUp) {
222                 mOnTouchUp = a.getInt(attr, mOnTouchUp);
223             } else if (attr == R.styleable.OnSwipe_nestedScrollFlags) {
224                 mFlags = a.getInteger(attr, 0);
225             } else if (attr == R.styleable.OnSwipe_limitBoundsTo) {
226                 mLimitBoundsTo = a.getResourceId(attr, 0);
227             } else if (attr == R.styleable.OnSwipe_rotationCenterId) {
228                 mRotationCenterId = a.getResourceId(attr, mRotationCenterId);
229             } else if (attr == R.styleable.OnSwipe_springDamping) {
230                 mSpringDamping = a.getFloat(attr, mSpringDamping);
231             } else if (attr == R.styleable.OnSwipe_springMass) {
232                 mSpringMass = a.getFloat(attr, mSpringMass);
233             } else if (attr == R.styleable.OnSwipe_springStiffness) {
234                 mSpringStiffness = a.getFloat(attr, mSpringStiffness);
235             } else if (attr == R.styleable.OnSwipe_springStopThreshold) {
236                 mSpringStopThreshold = a.getFloat(attr, mSpringStopThreshold);
237             } else if (attr == R.styleable.OnSwipe_springBoundary) {
238                 mSpringBoundary = a.getInt(attr, mSpringBoundary);
239             } else if (attr == R.styleable.OnSwipe_autoCompleteMode) {
240                 mAutoCompleteMode = a.getInt(attr, mAutoCompleteMode);
241             }
242 
243         }
244     }
245 
setUpTouchEvent(float lastTouchX, float lastTouchY)246     void setUpTouchEvent(float lastTouchX, float lastTouchY) {
247         mLastTouchX = lastTouchX;
248         mLastTouchY = lastTouchY;
249         mDragStarted = false;
250     }
251 
252     /**
253      * @param event
254      * @param velocityTracker
255      * @param currentState
256      * @param motionScene
257      */
processTouchRotateEvent(MotionEvent event, MotionLayout.MotionTracker velocityTracker, int currentState, MotionScene motionScene)258     void processTouchRotateEvent(MotionEvent event,
259                                  MotionLayout.MotionTracker velocityTracker,
260                                  int currentState,
261                                  MotionScene motionScene) {
262         velocityTracker.addMovement(event);
263         switch (event.getAction()) {
264             case MotionEvent.ACTION_DOWN:
265 
266                 mLastTouchX = event.getRawX();
267                 mLastTouchY = event.getRawY();
268 
269                 mDragStarted = false;
270                 break;
271             case MotionEvent.ACTION_MOVE:
272                 @SuppressWarnings("unused")
273                 float dy = event.getRawY() - mLastTouchY;
274                 @SuppressWarnings("unused")
275                 float dx = event.getRawX() - mLastTouchX;
276 
277                 float drag;
278 
279                 float rcx = mMotionLayout.getWidth() / 2.0f;
280                 float rcy = mMotionLayout.getHeight() / 2.0f;
281                 if (mRotationCenterId != MotionScene.UNSET) {
282                     View v = mMotionLayout.findViewById(mRotationCenterId);
283                     mMotionLayout.getLocationOnScreen(mTempLoc);
284                     rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f;
285                     rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f;
286                 } else if (mTouchAnchorId != MotionScene.UNSET) {
287                     MotionController mc = mMotionLayout.getMotionController(mTouchAnchorId);
288                     View v = mMotionLayout.findViewById(mc.getAnimateRelativeTo());
289                     if (v == null) {
290                         Log.e(TAG, "could not find view to animate to");
291                     } else {
292                         mMotionLayout.getLocationOnScreen(mTempLoc);
293                         rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f;
294                         rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f;
295                     }
296                 }
297                 float relativePosX = event.getRawX() - rcx;
298                 float relativePosY = event.getRawY() - rcy;
299 
300                 double angle1 = Math.atan2(event.getRawY() - rcy, event.getRawX() - rcx);
301                 double angle2 = Math.atan2(mLastTouchY - rcy, mLastTouchX - rcx);
302                 drag = (float) ((angle1 - angle2) * 180.0f / Math.PI);
303                 if (drag > 330) {
304                     drag -= 360;
305                 } else if (drag < -330) {
306                     drag += 360;
307                 }
308 
309                 if (Math.abs(drag) > 0.01 || mDragStarted) {
310                     float pos = mMotionLayout.getProgress();
311                     if (!mDragStarted) {
312                         mDragStarted = true;
313                         mMotionLayout.setProgress(pos);
314                     }
315                     if (mTouchAnchorId != MotionScene.UNSET) {
316                         mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos,
317                                 mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
318                         mAnchorDpDt[1] = (float) Math.toDegrees(mAnchorDpDt[1]);
319                     } else {
320                         mAnchorDpDt[1] = 360;
321                     }
322                     float change = drag * mDragScale / mAnchorDpDt[1];
323 
324                     pos = Math.max(Math.min(pos + change, 1), 0);
325                     float current = mMotionLayout.getProgress();
326 
327                     if (pos != current) {
328                         if (current == 0.0f || current == 1.0f) {
329                             mMotionLayout.endTrigger(current == 0.0f);
330                         }
331                         mMotionLayout.setProgress(pos);
332                         velocityTracker.computeCurrentVelocity(SEC_TO_MILLISECONDS);
333                         float tvx = velocityTracker.getXVelocity();
334                         float tvy = velocityTracker.getYVelocity();
335                         float angularVelocity = // v*sin(angle)/r
336                                 (float) (Math.hypot(tvy, tvx)
337                                         * Math.sin(Math.atan2(tvy, tvx) - angle1)
338                                         / Math.hypot(relativePosX, relativePosY));
339                         mMotionLayout.mLastVelocity = (float) Math.toDegrees(angularVelocity);
340                     } else {
341                         mMotionLayout.mLastVelocity = 0;
342                     }
343                     mLastTouchX = event.getRawX();
344                     mLastTouchY = event.getRawY();
345                 }
346 
347                 break;
348             case MotionEvent.ACTION_UP:
349                 mDragStarted = false;
350                 velocityTracker.computeCurrentVelocity(16);
351 
352                 float tvx = velocityTracker.getXVelocity();
353                 float tvy = velocityTracker.getYVelocity();
354                 float currentPos = mMotionLayout.getProgress();
355                 float pos = currentPos;
356                 rcx = mMotionLayout.getWidth() / 2.0f;
357                 rcy = mMotionLayout.getHeight() / 2.0f;
358                 if (mRotationCenterId != MotionScene.UNSET) {
359                     View v = mMotionLayout.findViewById(mRotationCenterId);
360                     mMotionLayout.getLocationOnScreen(mTempLoc);
361                     rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f;
362                     rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f;
363                 } else if (mTouchAnchorId != MotionScene.UNSET) {
364                     MotionController mc = mMotionLayout.getMotionController(mTouchAnchorId);
365                     View v = mMotionLayout.findViewById(mc.getAnimateRelativeTo());
366                     mMotionLayout.getLocationOnScreen(mTempLoc);
367                     rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f;
368                     rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f;
369                 }
370                 relativePosX = event.getRawX() - rcx;
371                 relativePosY = event.getRawY() - rcy;
372                 angle1 = Math.toDegrees(Math.atan2(relativePosY, relativePosX));
373 
374                 if (mTouchAnchorId != MotionScene.UNSET) {
375                     mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos,
376                             mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
377                     mAnchorDpDt[1] = (float) Math.toDegrees(mAnchorDpDt[1]);
378                 } else {
379                     mAnchorDpDt[1] = 360;
380                 }
381                 angle2 = Math.toDegrees(Math.atan2(tvy + relativePosY, tvx + relativePosX));
382                 drag = (float) (angle2 - angle1);
383                 float velocity_tweek = SEC_TO_MILLISECONDS / 16f;
384                 float angularVelocity = drag * velocity_tweek;
385                 if (!Float.isNaN(angularVelocity)) {
386                     pos += 3 * angularVelocity * mDragScale / mAnchorDpDt[1]; // TODO calibrate vel
387                 }
388                 if (pos != 0.0f && pos != 1.0f && mOnTouchUp != MotionLayout.TOUCH_UP_STOP) {
389                     angularVelocity = (float) angularVelocity * mDragScale / mAnchorDpDt[1];
390                     float target = (pos < 0.5) ? 0.0f : 1.0f;
391 
392                     if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_START) {
393                         if (currentPos + angularVelocity < 0) {
394                             angularVelocity = Math.abs(angularVelocity);
395                         }
396                         target = 1;
397                     }
398                     if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_END) {
399                         if (currentPos + angularVelocity > 1) {
400                             angularVelocity = -Math.abs(angularVelocity);
401                         }
402                         target = 0;
403                     }
404                     mMotionLayout.touchAnimateTo(mOnTouchUp, target ,
405                             3 * angularVelocity);
406                     if (0.0f >= currentPos || 1.0f <= currentPos) {
407                         mMotionLayout.setState(MotionLayout.TransitionState.FINISHED);
408                     }
409                 } else if (0.0f >= pos || 1.0f <= pos) {
410                     mMotionLayout.setState(MotionLayout.TransitionState.FINISHED);
411                 }
412                 break;
413         }
414 
415     }
416 
417     /**
418      * Process touch events
419      *
420      * @param event        The event coming from the touch
421      * @param currentState
422      * @param motionScene  The relevant MotionScene
423      */
processTouchEvent(MotionEvent event, MotionLayout.MotionTracker velocityTracker, int currentState, MotionScene motionScene)424     void processTouchEvent(MotionEvent event,
425                            MotionLayout.MotionTracker velocityTracker,
426                            int currentState,
427                            MotionScene motionScene) {
428         if (DEBUG) {
429             Log.v(TAG, Debug.getLocation() + " best processTouchEvent For ");
430         }
431         if (mIsRotateMode) {
432             processTouchRotateEvent(event, velocityTracker, currentState, motionScene);
433             return;
434         }
435         velocityTracker.addMovement(event);
436         switch (event.getAction()) {
437             case MotionEvent.ACTION_DOWN:
438                 mLastTouchX = event.getRawX();
439                 mLastTouchY = event.getRawY();
440                 mDragStarted = false;
441                 break;
442             case MotionEvent.ACTION_MOVE:
443                 float dy = event.getRawY() - mLastTouchY;
444                 float dx = event.getRawX() - mLastTouchX;
445                 float drag = dx * mTouchDirectionX + dy * mTouchDirectionY;
446                 if (DEBUG) {
447                     Log.v(TAG, "# dx = " + dx + " = " + event.getRawX() + " - " + mLastTouchX);
448                     Log.v(TAG, "# drag = " + drag);
449                 }
450                 if (Math.abs(drag) > mDragThreshold || mDragStarted) {
451                     if (DEBUG) {
452                         Log.v(TAG, "# ACTION_MOVE  mDragStarted  ");
453                     }
454                     float pos = mMotionLayout.getProgress();
455                     if (!mDragStarted) {
456                         mDragStarted = true;
457                         mMotionLayout.setProgress(pos);
458                         if (DEBUG) {
459                             Log.v(TAG, "# ACTION_MOVE  progress <- " + pos);
460                         }
461                     }
462                     if (mTouchAnchorId != MotionScene.UNSET) {
463 
464                         mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, mTouchAnchorX,
465                                 mTouchAnchorY, mAnchorDpDt);
466                         if (DEBUG) {
467                             Log.v(TAG, Debug.getLocation() + " mAnchorDpDt "
468                                     + Arrays.toString(mAnchorDpDt));
469                         }
470                     } else {
471                         if (DEBUG) {
472                             Log.v(TAG, Debug.getLocation() + " NO ANCHOR ");
473                         }
474                         float minSize = Math.min(mMotionLayout.getWidth(),
475                                 mMotionLayout.getHeight());
476                         mAnchorDpDt[1] = minSize * mTouchDirectionY;
477                         mAnchorDpDt[0] = minSize * mTouchDirectionX;
478                     }
479 
480                     float movmentInDir = mTouchDirectionX * mAnchorDpDt[0]
481                             + mTouchDirectionY * mAnchorDpDt[1];
482                     if (DEBUG) {
483                         Log.v(TAG, "# ACTION_MOVE  movmentInDir <- " + movmentInDir + " ");
484 
485                         Log.v(TAG, "# ACTION_MOVE  mAnchorDpDt  = " + mAnchorDpDt[0]
486                                 + " ,  " + mAnchorDpDt[1]);
487                         Log.v(TAG, "# ACTION_MOVE  mTouchDir  = " + mTouchDirectionX
488                                 + " , " + mTouchDirectionY);
489 
490                     }
491                     movmentInDir *= mDragScale;
492 
493                     if (Math.abs(movmentInDir) < 0.01) {
494                         mAnchorDpDt[0] = .01f;
495                         mAnchorDpDt[1] = .01f;
496 
497                     }
498                     float change;
499                     if (mTouchDirectionX != 0) {
500                         change = dx / mAnchorDpDt[0];
501                     } else {
502                         change = dy / mAnchorDpDt[1];
503                     }
504                     if (DEBUG) {
505                         Log.v(TAG, "# ACTION_MOVE      CHANGE  = " + change);
506                     }
507 
508                     pos = Math.max(Math.min(pos + change, 1), 0);
509 
510                     if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_START) {
511                         pos = Math.max(pos, 0.01f);
512                     }
513                     if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_END) {
514                         pos = Math.min(pos, 0.99f);
515                     }
516 
517                     float current = mMotionLayout.getProgress();
518                     if (pos != current) {
519                         if (current == 0.0f || current == 1.0f) {
520                             mMotionLayout.endTrigger(current == 0.0f);
521                         }
522                         mMotionLayout.setProgress(pos);
523                         if (DEBUG) {
524                             Log.v(TAG, "# ACTION_MOVE progress <- " + pos);
525                         }
526                         velocityTracker.computeCurrentVelocity(SEC_TO_MILLISECONDS);
527                         float tvx = velocityTracker.getXVelocity();
528                         float tvy = velocityTracker.getYVelocity();
529                         float velocity = (mTouchDirectionX != 0) ? tvx / mAnchorDpDt[0]
530                                 : tvy / mAnchorDpDt[1];
531                         mMotionLayout.mLastVelocity = velocity;
532                     } else {
533                         mMotionLayout.mLastVelocity = 0;
534                     }
535                     mLastTouchX = event.getRawX();
536                     mLastTouchY = event.getRawY();
537                 }
538                 break;
539             case MotionEvent.ACTION_UP:
540                 mDragStarted = false;
541                 velocityTracker.computeCurrentVelocity(SEC_TO_MILLISECONDS);
542                 float tvx = velocityTracker.getXVelocity();
543                 float tvy = velocityTracker.getYVelocity();
544                 float currentPos = mMotionLayout.getProgress();
545                 float pos = currentPos;
546 
547                 if (DEBUG) {
548                     Log.v(TAG, "# ACTION_UP progress  = " + pos);
549                 }
550                 if (mTouchAnchorId != MotionScene.UNSET) {
551                     mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos,
552                             mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
553                 } else {
554                     float minSize = Math.min(mMotionLayout.getWidth(), mMotionLayout.getHeight());
555                     mAnchorDpDt[1] = minSize * mTouchDirectionY;
556                     mAnchorDpDt[0] = minSize * mTouchDirectionX;
557                 }
558                 @SuppressWarnings("unused")
559                 float movmentInDir = mTouchDirectionX * mAnchorDpDt[0]
560                         + mTouchDirectionY * mAnchorDpDt[1];
561                 float velocity;
562                 if (mTouchDirectionX != 0) {
563                     velocity = tvx / mAnchorDpDt[0];
564                 } else {
565                     velocity = tvy / mAnchorDpDt[1];
566                 }
567                 if (DEBUG) {
568                     Log.v(TAG, "# ACTION_UP               tvy = " + tvy);
569                     Log.v(TAG, "# ACTION_UP mTouchDirectionX  = " + mTouchDirectionX);
570                     Log.v(TAG, "# ACTION_UP         velocity  = " + velocity);
571                 }
572 
573                 if (!Float.isNaN(velocity)) {
574                     pos += velocity / 3; // TODO calibration & animation speed based on velocity
575                 }
576                 if (pos != 0.0f && pos != 1.0f && mOnTouchUp != MotionLayout.TOUCH_UP_STOP) {
577                     float target = (pos < 0.5) ? 0.0f : 1.0f;
578 
579                     if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_START) {
580                         if (currentPos + velocity < 0) {
581                             velocity = Math.abs(velocity);
582                         }
583                         target = 1;
584                     }
585                     if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_END) {
586                         if (currentPos + velocity > 1) {
587                             velocity = -Math.abs(velocity);
588                         }
589                         target = 0;
590                     }
591 
592                     mMotionLayout.touchAnimateTo(mOnTouchUp, target, velocity);
593                     if (0.0f >= currentPos || 1.0f <= currentPos) {
594                         mMotionLayout.setState(MotionLayout.TransitionState.FINISHED);
595                     }
596                 } else if (0.0f >= pos || 1.0f <= pos) {
597                     mMotionLayout.setState(MotionLayout.TransitionState.FINISHED);
598 
599                 }
600                 break;
601         }
602     }
603 
setDown(float lastTouchX, float lastTouchY)604     void setDown(float lastTouchX, float lastTouchY) {
605         mLastTouchX = lastTouchX;
606         mLastTouchY = lastTouchY;
607     }
608 
609     /**
610      * Calculate if a drag in this direction results in an increase or decrease in progress.
611      *
612      * @param dx drag direction in x
613      * @param dy drag direction in y
614      * @return the change in progress given that dx and dy
615      */
getProgressDirection(float dx, float dy)616     float getProgressDirection(float dx, float dy) {
617         float pos = mMotionLayout.getProgress();
618         mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
619         float velocity;
620         if (mTouchDirectionX != 0) {
621             if (mAnchorDpDt[0] == 0) {
622                 mAnchorDpDt[0] = EPSILON;
623             }
624             velocity = dx * mTouchDirectionX / mAnchorDpDt[0];
625         } else {
626             if (mAnchorDpDt[1] == 0) {
627                 mAnchorDpDt[1] = EPSILON;
628             }
629             velocity = dy * mTouchDirectionY / mAnchorDpDt[1];
630         }
631         return velocity;
632     }
633 
scrollUp(float dx, float dy)634     void scrollUp(float dx, float dy) {
635         mDragStarted = false;
636 
637         float pos = mMotionLayout.getProgress();
638         mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
639         @SuppressWarnings("unused")
640         float movmentInDir = mTouchDirectionX * mAnchorDpDt[0] + mTouchDirectionY * mAnchorDpDt[1];
641         float velocity;
642         if (mTouchDirectionX != 0) {
643             velocity = dx * mTouchDirectionX / mAnchorDpDt[0];
644         } else {
645             velocity = dy * mTouchDirectionY / mAnchorDpDt[1];
646         }
647         if (!Float.isNaN(velocity)) {
648             pos += velocity / 3; // TODO calibration & animation speed based on velocity
649         }
650         if (pos != 0.0f && pos != 1.0f && mOnTouchUp != MotionLayout.TOUCH_UP_STOP) {
651             mMotionLayout.touchAnimateTo(mOnTouchUp, (pos < 0.5) ? 0.0f : 1.0f, velocity);
652         }
653     }
654 
scrollMove(float dx, float dy)655     void scrollMove(float dx, float dy) {
656         @SuppressWarnings("unused")
657         float drag = dx * mTouchDirectionX + dy * mTouchDirectionY;
658         if (true) { // Todo evaluate || Math.abs(drag) > 10 || mDragStarted) {
659             float pos = mMotionLayout.getProgress();
660             if (!mDragStarted) {
661                 mDragStarted = true;
662                 mMotionLayout.setProgress(pos);
663             }
664             mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos,
665                     mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
666             float movmentInDir = mTouchDirectionX * mAnchorDpDt[0]
667                     + mTouchDirectionY * mAnchorDpDt[1];
668 
669             if (Math.abs(movmentInDir) < 0.01) {
670                 mAnchorDpDt[0] = .01f;
671                 mAnchorDpDt[1] = .01f;
672 
673             }
674             float change;
675             if (mTouchDirectionX != 0) {
676                 change = dx * mTouchDirectionX / mAnchorDpDt[0];
677             } else {
678                 change = dy * mTouchDirectionY / mAnchorDpDt[1];
679             }
680             pos = Math.max(Math.min(pos + change, 1), 0);
681 
682             if (pos != mMotionLayout.getProgress()) {
683                 mMotionLayout.setProgress(pos);
684                 if (DEBUG) {
685                     Log.v(TAG, "# ACTION_UP        progress <- " + pos);
686                 }
687             }
688 
689         }
690     }
691 
setupTouch()692     void setupTouch() {
693 
694         View view = null;
695         if (mTouchAnchorId != -1) {
696             view = mMotionLayout.findViewById(mTouchAnchorId);
697             if (view == null) {
698                 Log.e(TAG, "cannot find TouchAnchorId @id/"
699                         + Debug.getName(mMotionLayout.getContext(), mTouchAnchorId));
700             }
701         }
702         if (view instanceof NestedScrollView) {
703             final NestedScrollView sv = (NestedScrollView) view;
704             sv.setOnTouchListener(new View.OnTouchListener() {
705                 @Override
706                 public boolean onTouch(View view, MotionEvent motionEvent) {
707                     return false;
708                 }
709             });
710             sv.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
711 
712                 @Override
713                 public void onScrollChange(NestedScrollView v,
714                                            int scrollX,
715                                            int scrollY,
716                                            int oldScrollX,
717                                            int oldScrollY) {
718 
719                 }
720             });
721         }
722     }
723 
724     /**
725      * set the id of the anchor
726      *
727      * @param id
728      */
setAnchorId(int id)729     public void setAnchorId(int id) {
730         mTouchAnchorId = id;
731     }
732 
733     /**
734      * Get the view being used as anchor
735      *
736      * @return
737      */
getAnchorId()738     public int getAnchorId() {
739         return mTouchAnchorId;
740     }
741 
742     /**
743      * Set the location in the view to be the touch anchor
744      *
745      * @param x location in x 0 = left, 1 = right
746      * @param y location in y 0 = top, 1 = bottom
747      */
setTouchAnchorLocation(float x, float y)748     public void setTouchAnchorLocation(float x, float y) {
749         mTouchAnchorX = x;
750         mTouchAnchorY = y;
751     }
752 
753     /**
754      * Sets the maximum velocity allowed on touch up.
755      * Velocity is the rate of change in "progress" per second.
756      *
757      * @param velocity in progress per second 1 = one second to do the entire animation
758      */
setMaxVelocity(float velocity)759     public void setMaxVelocity(float velocity) {
760         mMaxVelocity = velocity;
761     }
762 
763     /**
764      * set the maximum Acceleration allowed for a motion.
765      * Acceleration is the rate of change velocity per second.
766      *
767      * @param acceleration
768      */
setMaxAcceleration(float acceleration)769     public void setMaxAcceleration(float acceleration) {
770         mMaxAcceleration = acceleration;
771     }
772 
getMaxAcceleration()773     float getMaxAcceleration() {
774         return mMaxAcceleration;
775     }
776 
777     /**
778      * Gets the maximum velocity allowed on touch up.
779      * Velocity is the rate of change in "progress" per second.
780      *
781      * @return
782      */
getMaxVelocity()783     public float getMaxVelocity() {
784         return mMaxVelocity;
785     }
786 
getMoveWhenScrollAtTop()787     boolean getMoveWhenScrollAtTop() {
788         return mMoveWhenScrollAtTop;
789     }
790 
791     /**
792      * Get how the drag progress will return to the start or end state on touch up.
793      * Can be ether COMPLETE_MODE_CONTINUOUS_VELOCITY (default) or COMPLETE_MODE_SPRING
794      * @return
795      */
getAutoCompleteMode()796     public int getAutoCompleteMode() {
797         return mAutoCompleteMode;
798     }
799     /**
800      * set how the drag progress will return to the start or end state on touch up.
801      *
802      *
803      * @return
804      */
setAutoCompleteMode(int autoCompleteMode)805     void setAutoCompleteMode(int autoCompleteMode) {
806         mAutoCompleteMode = autoCompleteMode;
807     }
808 
809     /**
810      * This calculates the bounds of the mTouchRegionId view.
811      * This reuses rect for efficiency as this class will be called many times.
812      *
813      * @param layout The layout containing the view (findViewId)
814      * @param rect   the rectangle to fill provided so this function does not have to create memory
815      * @return the rect or null
816      */
getTouchRegion(ViewGroup layout, RectF rect)817     RectF getTouchRegion(ViewGroup layout, RectF rect) {
818         if (mTouchRegionId == MotionScene.UNSET) {
819             return null;
820         }
821         View view = layout.findViewById(mTouchRegionId);
822         if (view == null) {
823             return null;
824         }
825         rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
826         return rect;
827     }
828 
getTouchRegionId()829     int getTouchRegionId() {
830         return mTouchRegionId;
831     }
832 
833     /**
834      * This calculates the bounds of the mTouchRegionId view.
835      * This reuses rect for efficiency as this class will be called many times.
836      *
837      * @param layout The layout containing the view (findViewId)
838      * @param rect   the rectangle to fill provided for memory efficiency
839      * @return the rect or null
840      */
getLimitBoundsTo(ViewGroup layout, RectF rect)841     RectF getLimitBoundsTo(ViewGroup layout, RectF rect) {
842         if (mLimitBoundsTo == MotionScene.UNSET) {
843             return null;
844         }
845         View view = layout.findViewById(mLimitBoundsTo);
846         if (view == null) {
847             return null;
848         }
849         rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
850         return rect;
851     }
852 
getLimitBoundsToId()853     int getLimitBoundsToId() {
854         return mLimitBoundsTo;
855     }
856 
dot(float dx, float dy)857     float dot(float dx, float dy) {
858         return dx * mTouchDirectionX + dy * mTouchDirectionY;
859     }
860 
861     @Override
toString()862     public String toString() {
863         return Float.isNaN(mTouchDirectionX) ? "rotation"
864                 : (mTouchDirectionX + " , " + mTouchDirectionY);
865     }
866 
867     /**
868      * flags to control
869      *
870      * @return
871      */
getFlags()872     public int getFlags() {
873         return mFlags;
874     }
875 
setTouchUpMode(int touchUpMode)876     public void setTouchUpMode(int touchUpMode) {
877         mOnTouchUp = touchUpMode;
878     }
879 
880     /**
881      * the stiffness of the spring if using spring
882      *  K in "a = (-k*x-c*v)/m" equation for the acceleration of a spring
883      * @return NaN if not set
884      */
getSpringStiffness()885     public float getSpringStiffness() {
886         return mSpringStiffness;
887     }
888 
889     /**
890      * the Mass of the spring if using spring
891      *  m in "a = (-k*x-c*v)/m" equation for the acceleration of a spring
892      * @return default is 1
893      */
getSpringMass()894     public float getSpringMass() {
895         return mSpringMass;
896     }
897 
898     /**
899      * the damping of the spring if using spring
900      * c in "a = (-k*x-c*v)/m" equation for the acceleration of a spring
901      * @return NaN if not set
902      */
getSpringDamping()903     public float getSpringDamping() {
904         return mSpringDamping;
905     }
906 
907     /**
908      * The threshold below
909      * @return NaN if not set
910      */
getSpringStopThreshold()911     public float getSpringStopThreshold() {
912         return mSpringStopThreshold;
913     }
914 
915     /**
916      * The spring's behaviour when it hits 0 or 1. It can be made ot overshoot or bounce
917      * overshoot = 0
918      * bounceStart = 1
919      * bounceEnd = 2
920      * bounceBoth = 3
921      * @return Bounce mode
922      */
getSpringBoundary()923     public int getSpringBoundary() {
924         return mSpringBoundary;
925     }
926 
isDragStarted()927     boolean isDragStarted() {
928         return mDragStarted;
929     }
930 
931 }
932