TouchResponse.java

/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.constraintlayout.motion.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import androidx.constraintlayout.widget.R;
import androidx.core.widget.NestedScrollView;

import org.xmlpull.v1.XmlPullParser;

import java.util.Arrays;

/**
 * This class is used to manage Touch behaviour
 *
 *
 */

class TouchResponse {
    private static final String TAG = "TouchResponse";
    private static final boolean DEBUG = false;
    private int mTouchAnchorSide = 0;
    private int mTouchSide = 0;
    private int mOnTouchUp = 0;
    private int mTouchAnchorId = MotionScene.UNSET;
    private int mTouchRegionId = MotionScene.UNSET;
    private int mLimitBoundsTo = MotionScene.UNSET;
    private float mTouchAnchorY = 0.5f;
    private float mTouchAnchorX = 0.5f;
    float mRotateCenterX = 0.5f;
    float mRotateCenterY = 0.5f;
    private int mRotationCenterId = MotionScene.UNSET;
    boolean mIsRotateMode = false;
    private float mTouchDirectionX = 0;
    private float mTouchDirectionY = 1;
    private boolean mDragStarted = false;
    private float[] mAnchorDpDt = new float[2];
    private int[] mTempLoc = new int[2];
    private float mLastTouchX, mLastTouchY;
    private final MotionLayout mMotionLayout;
    private static final int SEC_TO_MILLISECONDS = 1000;
    private static final float EPSILON = 0.0000001f;

    private static final float[][] TOUCH_SIDES = {
            {0.5f, 0.0f}, // top
            {0.0f, 0.5f}, // left
            {1.0f, 0.5f}, // right
            {0.5f, 1.0f}, // bottom
            {0.5f, 0.5f}, // middle
            {0.0f, 0.5f}, // start (dynamically updated)
            {1.0f, 0.5f}, // end  (dynamically updated)
    };
    private static final float[][] TOUCH_DIRECTION = {
            {0.0f, -1.0f}, // up
            {0.0f, 1.0f}, // down
            {-1.0f, 0.0f}, // left
            {1.0f, 0.0f}, // right
            {-1.0f, 0.0f}, // start (dynamically updated)
            {1.0f, 0.0f}, // end  (dynamically updated)
    };
    @SuppressWarnings("unused")
    private static final int TOUCH_UP = 0;
    @SuppressWarnings("unused")
    private static final int TOUCH_DOWN = 1;
    private static final int TOUCH_LEFT = 2;
    private static final int TOUCH_RIGHT = 3;
    private static final int TOUCH_START = 4;
    private static final int TOUCH_END = 5;

    @SuppressWarnings("unused")
    private static final int SIDE_TOP = 0;
    private static final int SIDE_LEFT = 1;
    private static final int SIDE_RIGHT = 2;
    @SuppressWarnings("unused")
    private static final int SIDE_BOTTOM = 3;
    @SuppressWarnings("unused")
    private static final int SIDE_MIDDLE = 4;
    private static final int SIDE_START = 5;
    private static final int SIDE_END = 6;

    private float mMaxVelocity = 4;
    private float mMaxAcceleration = 1.2f;
    private boolean mMoveWhenScrollAtTop = true;
    private float mDragScale = 1f;
    private int mFlags = 0;
    static final int FLAG_DISABLE_POST_SCROLL = 1;
    static final int FLAG_DISABLE_SCROLL = 2;
    static final int FLAG_SUPPORT_SCROLL_UP = 4;

    private float mDragThreshold = 10;
    private float mSpringDamping = 10;
    private float mSpringMass = 1;
    private float mSpringStiffness = Float.NaN;
    private float mSpringStopThreshold = Float.NaN;
    private int mSpringBoundary = 0;
    private int mAutoCompleteMode = COMPLETE_MODE_CONTINUOUS_VELOCITY;
    public static final int COMPLETE_MODE_CONTINUOUS_VELOCITY = 0;
    public static final int COMPLETE_MODE_SPRING = 1;

    TouchResponse(Context context, MotionLayout layout, XmlPullParser parser) {
        mMotionLayout = layout;
        fillFromAttributeList(context, Xml.asAttributeSet(parser));
    }

    TouchResponse(MotionLayout layout, OnSwipe onSwipe) {
        mMotionLayout = layout;
        mTouchAnchorId = onSwipe.getTouchAnchorId();
        mTouchAnchorSide = onSwipe.getTouchAnchorSide();
        if (mTouchAnchorSide != -1) {
            mTouchAnchorX = TOUCH_SIDES[mTouchAnchorSide][0];
            mTouchAnchorY = TOUCH_SIDES[mTouchAnchorSide][1];
        }
        mTouchSide = onSwipe.getDragDirection();
        if (mTouchSide < TOUCH_DIRECTION.length) {
            mTouchDirectionX = TOUCH_DIRECTION[mTouchSide][0];
            mTouchDirectionY = TOUCH_DIRECTION[mTouchSide][1];
        } else {
            mTouchDirectionX = mTouchDirectionY = Float.NaN;
            mIsRotateMode = true;
        }
        mMaxVelocity = onSwipe.getMaxVelocity();
        mMaxAcceleration = onSwipe.getMaxAcceleration();
        mMoveWhenScrollAtTop = onSwipe.getMoveWhenScrollAtTop();
        mDragScale = onSwipe.getDragScale();
        mDragThreshold = onSwipe.getDragThreshold();
        mTouchRegionId = onSwipe.getTouchRegionId();
        mOnTouchUp = onSwipe.getOnTouchUp();
        mFlags = onSwipe.getNestedScrollFlags();
        mLimitBoundsTo = onSwipe.getLimitBoundsTo();
        mRotationCenterId = onSwipe.getRotationCenterId();
        mSpringBoundary = onSwipe.getSpringBoundary();
        mSpringDamping = onSwipe.getSpringDamping();
        mSpringMass = onSwipe.getSpringMass();
        mSpringStiffness = onSwipe.getSpringStiffness();
        mSpringStopThreshold = onSwipe.getSpringStopThreshold();
        mAutoCompleteMode = onSwipe.getAutoCompleteMode();
    }

    public void setRTL(boolean rtl) {
        if (rtl) {
            TOUCH_DIRECTION[TOUCH_START] = TOUCH_DIRECTION[TOUCH_RIGHT];
            TOUCH_DIRECTION[TOUCH_END] = TOUCH_DIRECTION[TOUCH_LEFT];
            TOUCH_SIDES[SIDE_START] = TOUCH_SIDES[SIDE_RIGHT];
            TOUCH_SIDES[SIDE_END] = TOUCH_SIDES[SIDE_LEFT];
        } else {
            TOUCH_DIRECTION[TOUCH_START] = TOUCH_DIRECTION[TOUCH_LEFT];
            TOUCH_DIRECTION[TOUCH_END] = TOUCH_DIRECTION[TOUCH_RIGHT];
            TOUCH_SIDES[SIDE_START] = TOUCH_SIDES[SIDE_LEFT];
            TOUCH_SIDES[SIDE_END] = TOUCH_SIDES[SIDE_RIGHT];
        }

        mTouchAnchorX = TOUCH_SIDES[mTouchAnchorSide][0];
        mTouchAnchorY = TOUCH_SIDES[mTouchAnchorSide][1];
        if (mTouchSide >= TOUCH_DIRECTION.length) {
            return;
        }
        mTouchDirectionX = TOUCH_DIRECTION[mTouchSide][0];
        mTouchDirectionY = TOUCH_DIRECTION[mTouchSide][1];
    }

    private void fillFromAttributeList(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.OnSwipe);
        fill(a);
        a.recycle();
    }

    private void fill(TypedArray a) {
        final int count = a.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = a.getIndex(i);
            if (attr == R.styleable.OnSwipe_touchAnchorId) {
                mTouchAnchorId = a.getResourceId(attr, mTouchAnchorId);
            } else if (attr == R.styleable.OnSwipe_touchAnchorSide) {
                mTouchAnchorSide = a.getInt(attr, mTouchAnchorSide);
                mTouchAnchorX = TOUCH_SIDES[mTouchAnchorSide][0];
                mTouchAnchorY = TOUCH_SIDES[mTouchAnchorSide][1];
            } else if (attr == R.styleable.OnSwipe_dragDirection) {
                mTouchSide = a.getInt(attr, mTouchSide);
                if (mTouchSide < TOUCH_DIRECTION.length) {
                    mTouchDirectionX = TOUCH_DIRECTION[mTouchSide][0];
                    mTouchDirectionY = TOUCH_DIRECTION[mTouchSide][1];
                } else {
                    mTouchDirectionX = mTouchDirectionY = Float.NaN;
                    mIsRotateMode = true;
                }
            } else if (attr == R.styleable.OnSwipe_maxVelocity) {
                mMaxVelocity = a.getFloat(attr, mMaxVelocity);
            } else if (attr == R.styleable.OnSwipe_maxAcceleration) {
                mMaxAcceleration = a.getFloat(attr, mMaxAcceleration);
            } else if (attr == R.styleable.OnSwipe_moveWhenScrollAtTop) {
                mMoveWhenScrollAtTop = a.getBoolean(attr, mMoveWhenScrollAtTop);
            } else if (attr == R.styleable.OnSwipe_dragScale) {
                mDragScale = a.getFloat(attr, mDragScale);
            } else if (attr == R.styleable.OnSwipe_dragThreshold) {
                mDragThreshold = a.getFloat(attr, mDragThreshold);
            } else if (attr == R.styleable.OnSwipe_touchRegionId) {
                mTouchRegionId = a.getResourceId(attr, mTouchRegionId);
            } else if (attr == R.styleable.OnSwipe_onTouchUp) {
                mOnTouchUp = a.getInt(attr, mOnTouchUp);
            } else if (attr == R.styleable.OnSwipe_nestedScrollFlags) {
                mFlags = a.getInteger(attr, 0);
            } else if (attr == R.styleable.OnSwipe_limitBoundsTo) {
                mLimitBoundsTo = a.getResourceId(attr, 0);
            } else if (attr == R.styleable.OnSwipe_rotationCenterId) {
                mRotationCenterId = a.getResourceId(attr, mRotationCenterId);
            } else if (attr == R.styleable.OnSwipe_springDamping) {
                mSpringDamping = a.getFloat(attr, mSpringDamping);
            } else if (attr == R.styleable.OnSwipe_springMass) {
                mSpringMass = a.getFloat(attr, mSpringMass);
            } else if (attr == R.styleable.OnSwipe_springStiffness) {
                mSpringStiffness = a.getFloat(attr, mSpringStiffness);
            } else if (attr == R.styleable.OnSwipe_springStopThreshold) {
                mSpringStopThreshold = a.getFloat(attr, mSpringStopThreshold);
            } else if (attr == R.styleable.OnSwipe_springBoundary) {
                mSpringBoundary = a.getInt(attr, mSpringBoundary);
            } else if (attr == R.styleable.OnSwipe_autoCompleteMode) {
                mAutoCompleteMode = a.getInt(attr, mAutoCompleteMode);
            }

        }
    }

    void setUpTouchEvent(float lastTouchX, float lastTouchY) {
        mLastTouchX = lastTouchX;
        mLastTouchY = lastTouchY;
        mDragStarted = false;
    }

    /**
     * @param event
     * @param velocityTracker
     * @param currentState
     * @param motionScene
     */
    void processTouchRotateEvent(MotionEvent event,
                                 MotionLayout.MotionTracker velocityTracker,
                                 int currentState,
                                 MotionScene motionScene) {
        velocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                mLastTouchX = event.getRawX();
                mLastTouchY = event.getRawY();

                mDragStarted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                @SuppressWarnings("unused")
                float dy = event.getRawY() - mLastTouchY;
                @SuppressWarnings("unused")
                float dx = event.getRawX() - mLastTouchX;

                float drag;

                float rcx = mMotionLayout.getWidth() / 2.0f;
                float rcy = mMotionLayout.getHeight() / 2.0f;
                if (mRotationCenterId != MotionScene.UNSET) {
                    View v = mMotionLayout.findViewById(mRotationCenterId);
                    mMotionLayout.getLocationOnScreen(mTempLoc);
                    rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f;
                    rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f;
                } else if (mTouchAnchorId != MotionScene.UNSET) {
                    MotionController mc = mMotionLayout.getMotionController(mTouchAnchorId);
                    View v = mMotionLayout.findViewById(mc.getAnimateRelativeTo());
                    if (v == null) {
                        Log.e(TAG, "could not find view to animate to");
                    } else {
                        mMotionLayout.getLocationOnScreen(mTempLoc);
                        rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f;
                        rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f;
                    }
                }
                float relativePosX = event.getRawX() - rcx;
                float relativePosY = event.getRawY() - rcy;

                double angle1 = Math.atan2(event.getRawY() - rcy, event.getRawX() - rcx);
                double angle2 = Math.atan2(mLastTouchY - rcy, mLastTouchX - rcx);
                drag = (float) ((angle1 - angle2) * 180.0f / Math.PI);
                if (drag > 330) {
                    drag -= 360;
                } else if (drag < -330) {
                    drag += 360;
                }

                if (Math.abs(drag) > 0.01 || mDragStarted) {
                    float pos = mMotionLayout.getProgress();
                    if (!mDragStarted) {
                        mDragStarted = true;
                        mMotionLayout.setProgress(pos);
                    }
                    if (mTouchAnchorId != MotionScene.UNSET) {
                        mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos,
                                mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
                        mAnchorDpDt[1] = (float) Math.toDegrees(mAnchorDpDt[1]);
                    } else {
                        mAnchorDpDt[1] = 360;
                    }
                    float change = drag * mDragScale / mAnchorDpDt[1];

                    pos = Math.max(Math.min(pos + change, 1), 0);
                    float current = mMotionLayout.getProgress();

                    if (pos != current) {
                        if (current == 0.0f || current == 1.0f) {
                            mMotionLayout.endTrigger(current == 0.0f);
                        }
                        mMotionLayout.setProgress(pos);
                        velocityTracker.computeCurrentVelocity(SEC_TO_MILLISECONDS);
                        float tvx = velocityTracker.getXVelocity();
                        float tvy = velocityTracker.getYVelocity();
                        float angularVelocity = // v*sin(angle)/r
                                (float) (Math.hypot(tvy, tvx)
                                        * Math.sin(Math.atan2(tvy, tvx) - angle1)
                                        / Math.hypot(relativePosX, relativePosY));
                        mMotionLayout.mLastVelocity = (float) Math.toDegrees(angularVelocity);
                    } else {
                        mMotionLayout.mLastVelocity = 0;
                    }
                    mLastTouchX = event.getRawX();
                    mLastTouchY = event.getRawY();
                }

                break;
            case MotionEvent.ACTION_UP:
                mDragStarted = false;
                velocityTracker.computeCurrentVelocity(16);

                float tvx = velocityTracker.getXVelocity();
                float tvy = velocityTracker.getYVelocity();
                float currentPos = mMotionLayout.getProgress();
                float pos = currentPos;
                rcx = mMotionLayout.getWidth() / 2.0f;
                rcy = mMotionLayout.getHeight() / 2.0f;
                if (mRotationCenterId != MotionScene.UNSET) {
                    View v = mMotionLayout.findViewById(mRotationCenterId);
                    mMotionLayout.getLocationOnScreen(mTempLoc);
                    rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f;
                    rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f;
                } else if (mTouchAnchorId != MotionScene.UNSET) {
                    MotionController mc = mMotionLayout.getMotionController(mTouchAnchorId);
                    View v = mMotionLayout.findViewById(mc.getAnimateRelativeTo());
                    mMotionLayout.getLocationOnScreen(mTempLoc);
                    rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f;
                    rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f;
                }
                relativePosX = event.getRawX() - rcx;
                relativePosY = event.getRawY() - rcy;
                angle1 = Math.toDegrees(Math.atan2(relativePosY, relativePosX));

                if (mTouchAnchorId != MotionScene.UNSET) {
                    mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos,
                            mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
                    mAnchorDpDt[1] = (float) Math.toDegrees(mAnchorDpDt[1]);
                } else {
                    mAnchorDpDt[1] = 360;
                }
                angle2 = Math.toDegrees(Math.atan2(tvy + relativePosY, tvx + relativePosX));
                drag = (float) (angle2 - angle1);
                float velocity_tweek = SEC_TO_MILLISECONDS / 16f;
                float angularVelocity = drag * velocity_tweek;
                if (!Float.isNaN(angularVelocity)) {
                    pos += 3 * angularVelocity * mDragScale / mAnchorDpDt[1]; // TODO calibrate vel
                }
                if (pos != 0.0f && pos != 1.0f && mOnTouchUp != MotionLayout.TOUCH_UP_STOP) {
                    angularVelocity = (float) angularVelocity * mDragScale / mAnchorDpDt[1];
                    float target = (pos < 0.5) ? 0.0f : 1.0f;

                    if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_START) {
                        if (currentPos + angularVelocity < 0) {
                            angularVelocity = Math.abs(angularVelocity);
                        }
                        target = 1;
                    }
                    if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_END) {
                        if (currentPos + angularVelocity > 1) {
                            angularVelocity = -Math.abs(angularVelocity);
                        }
                        target = 0;
                    }
                    mMotionLayout.touchAnimateTo(mOnTouchUp, target ,
                            3 * angularVelocity);
                    if (0.0f >= currentPos || 1.0f <= currentPos) {
                        mMotionLayout.setState(MotionLayout.TransitionState.FINISHED);
                    }
                } else if (0.0f >= pos || 1.0f <= pos) {
                    mMotionLayout.setState(MotionLayout.TransitionState.FINISHED);
                }
                break;
        }

    }

    /**
     * Process touch events
     *
     * @param event        The event coming from the touch
     * @param currentState
     * @param motionScene  The relevant MotionScene
     */
    void processTouchEvent(MotionEvent event,
                           MotionLayout.MotionTracker velocityTracker,
                           int currentState,
                           MotionScene motionScene) {
        if (DEBUG) {
            Log.v(TAG, Debug.getLocation() + " best processTouchEvent For ");
        }
        if (mIsRotateMode) {
            processTouchRotateEvent(event, velocityTracker, currentState, motionScene);
            return;
        }
        velocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastTouchX = event.getRawX();
                mLastTouchY = event.getRawY();
                mDragStarted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                float dy = event.getRawY() - mLastTouchY;
                float dx = event.getRawX() - mLastTouchX;
                float drag = dx * mTouchDirectionX + dy * mTouchDirectionY;
                if (DEBUG) {
                    Log.v(TAG, "# dx = " + dx + " = " + event.getRawX() + " - " + mLastTouchX);
                    Log.v(TAG, "# drag = " + drag);
                }
                if (Math.abs(drag) > mDragThreshold || mDragStarted) {
                    if (DEBUG) {
                        Log.v(TAG, "# ACTION_MOVE  mDragStarted  ");
                    }
                    float pos = mMotionLayout.getProgress();
                    if (!mDragStarted) {
                        mDragStarted = true;
                        mMotionLayout.setProgress(pos);
                        if (DEBUG) {
                            Log.v(TAG, "# ACTION_MOVE  progress <- " + pos);
                        }
                    }
                    if (mTouchAnchorId != MotionScene.UNSET) {

                        mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, mTouchAnchorX,
                                mTouchAnchorY, mAnchorDpDt);
                        if (DEBUG) {
                            Log.v(TAG, Debug.getLocation() + " mAnchorDpDt "
                                    + Arrays.toString(mAnchorDpDt));
                        }
                    } else {
                        if (DEBUG) {
                            Log.v(TAG, Debug.getLocation() + " NO ANCHOR ");
                        }
                        float minSize = Math.min(mMotionLayout.getWidth(),
                                mMotionLayout.getHeight());
                        mAnchorDpDt[1] = minSize * mTouchDirectionY;
                        mAnchorDpDt[0] = minSize * mTouchDirectionX;
                    }

                    float movmentInDir = mTouchDirectionX * mAnchorDpDt[0]
                            + mTouchDirectionY * mAnchorDpDt[1];
                    if (DEBUG) {
                        Log.v(TAG, "# ACTION_MOVE  movmentInDir <- " + movmentInDir + " ");

                        Log.v(TAG, "# ACTION_MOVE  mAnchorDpDt  = " + mAnchorDpDt[0]
                                + " ,  " + mAnchorDpDt[1]);
                        Log.v(TAG, "# ACTION_MOVE  mTouchDir  = " + mTouchDirectionX
                                + " , " + mTouchDirectionY);

                    }
                    movmentInDir *= mDragScale;

                    if (Math.abs(movmentInDir) < 0.01) {
                        mAnchorDpDt[0] = .01f;
                        mAnchorDpDt[1] = .01f;

                    }
                    float change;
                    if (mTouchDirectionX != 0) {
                        change = dx / mAnchorDpDt[0];
                    } else {
                        change = dy / mAnchorDpDt[1];
                    }
                    if (DEBUG) {
                        Log.v(TAG, "# ACTION_MOVE      CHANGE  = " + change);
                    }

                    pos = Math.max(Math.min(pos + change, 1), 0);

                    if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_START) {
                        pos = Math.max(pos, 0.01f);
                    }
                    if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_END) {
                        pos = Math.min(pos, 0.99f);
                    }

                    float current = mMotionLayout.getProgress();
                    if (pos != current) {
                        if (current == 0.0f || current == 1.0f) {
                            mMotionLayout.endTrigger(current == 0.0f);
                        }
                        mMotionLayout.setProgress(pos);
                        if (DEBUG) {
                            Log.v(TAG, "# ACTION_MOVE progress <- " + pos);
                        }
                        velocityTracker.computeCurrentVelocity(SEC_TO_MILLISECONDS);
                        float tvx = velocityTracker.getXVelocity();
                        float tvy = velocityTracker.getYVelocity();
                        float velocity = (mTouchDirectionX != 0) ? tvx / mAnchorDpDt[0]
                                : tvy / mAnchorDpDt[1];
                        mMotionLayout.mLastVelocity = velocity;
                    } else {
                        mMotionLayout.mLastVelocity = 0;
                    }
                    mLastTouchX = event.getRawX();
                    mLastTouchY = event.getRawY();
                }
                break;
            case MotionEvent.ACTION_UP:
                mDragStarted = false;
                velocityTracker.computeCurrentVelocity(SEC_TO_MILLISECONDS);
                float tvx = velocityTracker.getXVelocity();
                float tvy = velocityTracker.getYVelocity();
                float currentPos = mMotionLayout.getProgress();
                float pos = currentPos;

                if (DEBUG) {
                    Log.v(TAG, "# ACTION_UP progress  = " + pos);
                }
                if (mTouchAnchorId != MotionScene.UNSET) {
                    mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos,
                            mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
                } else {
                    float minSize = Math.min(mMotionLayout.getWidth(), mMotionLayout.getHeight());
                    mAnchorDpDt[1] = minSize * mTouchDirectionY;
                    mAnchorDpDt[0] = minSize * mTouchDirectionX;
                }
                @SuppressWarnings("unused")
                float movmentInDir = mTouchDirectionX * mAnchorDpDt[0]
                        + mTouchDirectionY * mAnchorDpDt[1];
                float velocity;
                if (mTouchDirectionX != 0) {
                    velocity = tvx / mAnchorDpDt[0];
                } else {
                    velocity = tvy / mAnchorDpDt[1];
                }
                if (DEBUG) {
                    Log.v(TAG, "# ACTION_UP               tvy = " + tvy);
                    Log.v(TAG, "# ACTION_UP mTouchDirectionX  = " + mTouchDirectionX);
                    Log.v(TAG, "# ACTION_UP         velocity  = " + velocity);
                }

                if (!Float.isNaN(velocity)) {
                    pos += velocity / 3; // TODO calibration & animation speed based on velocity
                }
                if (pos != 0.0f && pos != 1.0f && mOnTouchUp != MotionLayout.TOUCH_UP_STOP) {
                    float target = (pos < 0.5) ? 0.0f : 1.0f;

                    if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_START) {
                        if (currentPos + velocity < 0) {
                            velocity = Math.abs(velocity);
                        }
                        target = 1;
                    }
                    if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_END) {
                        if (currentPos + velocity > 1) {
                            velocity = -Math.abs(velocity);
                        }
                        target = 0;
                    }

                    mMotionLayout.touchAnimateTo(mOnTouchUp, target, velocity);
                    if (0.0f >= currentPos || 1.0f <= currentPos) {
                        mMotionLayout.setState(MotionLayout.TransitionState.FINISHED);
                    }
                } else if (0.0f >= pos || 1.0f <= pos) {
                    mMotionLayout.setState(MotionLayout.TransitionState.FINISHED);

                }
                break;
        }
    }

    void setDown(float lastTouchX, float lastTouchY) {
        mLastTouchX = lastTouchX;
        mLastTouchY = lastTouchY;
    }

    /**
     * Calculate if a drag in this direction results in an increase or decrease in progress.
     *
     * @param dx drag direction in x
     * @param dy drag direction in y
     * @return the change in progress given that dx and dy
     */
    float getProgressDirection(float dx, float dy) {
        float pos = mMotionLayout.getProgress();
        mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
        float velocity;
        if (mTouchDirectionX != 0) {
            if (mAnchorDpDt[0] == 0) {
                mAnchorDpDt[0] = EPSILON;
            }
            velocity = dx * mTouchDirectionX / mAnchorDpDt[0];
        } else {
            if (mAnchorDpDt[1] == 0) {
                mAnchorDpDt[1] = EPSILON;
            }
            velocity = dy * mTouchDirectionY / mAnchorDpDt[1];
        }
        return velocity;
    }

    void scrollUp(float dx, float dy) {
        mDragStarted = false;

        float pos = mMotionLayout.getProgress();
        mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
        @SuppressWarnings("unused")
        float movmentInDir = mTouchDirectionX * mAnchorDpDt[0] + mTouchDirectionY * mAnchorDpDt[1];
        float velocity;
        if (mTouchDirectionX != 0) {
            velocity = dx * mTouchDirectionX / mAnchorDpDt[0];
        } else {
            velocity = dy * mTouchDirectionY / mAnchorDpDt[1];
        }
        if (!Float.isNaN(velocity)) {
            pos += velocity / 3; // TODO calibration & animation speed based on velocity
        }
        if (pos != 0.0f && pos != 1.0f && mOnTouchUp != MotionLayout.TOUCH_UP_STOP) {
            mMotionLayout.touchAnimateTo(mOnTouchUp, (pos < 0.5) ? 0.0f : 1.0f, velocity);
        }
    }

    void scrollMove(float dx, float dy) {
        @SuppressWarnings("unused")
        float drag = dx * mTouchDirectionX + dy * mTouchDirectionY;
        if (true) { // Todo evaluate || Math.abs(drag) > 10 || mDragStarted) {
            float pos = mMotionLayout.getProgress();
            if (!mDragStarted) {
                mDragStarted = true;
                mMotionLayout.setProgress(pos);
            }
            mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos,
                    mTouchAnchorX, mTouchAnchorY, mAnchorDpDt);
            float movmentInDir = mTouchDirectionX * mAnchorDpDt[0]
                    + mTouchDirectionY * mAnchorDpDt[1];

            if (Math.abs(movmentInDir) < 0.01) {
                mAnchorDpDt[0] = .01f;
                mAnchorDpDt[1] = .01f;

            }
            float change;
            if (mTouchDirectionX != 0) {
                change = dx * mTouchDirectionX / mAnchorDpDt[0];
            } else {
                change = dy * mTouchDirectionY / mAnchorDpDt[1];
            }
            pos = Math.max(Math.min(pos + change, 1), 0);

            if (pos != mMotionLayout.getProgress()) {
                mMotionLayout.setProgress(pos);
                if (DEBUG) {
                    Log.v(TAG, "# ACTION_UP        progress <- " + pos);
                }
            }

        }
    }

    void setupTouch() {

        View view = null;
        if (mTouchAnchorId != -1) {
            view = mMotionLayout.findViewById(mTouchAnchorId);
            if (view == null) {
                Log.e(TAG, "cannot find TouchAnchorId @id/"
                        + Debug.getName(mMotionLayout.getContext(), mTouchAnchorId));
            }
        }
        if (view instanceof NestedScrollView) {
            final NestedScrollView sv = (NestedScrollView) view;
            sv.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View view, MotionEvent motionEvent) {
                    return false;
                }
            });
            sv.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {

                @Override
                public void onScrollChange(NestedScrollView v,
                                           int scrollX,
                                           int scrollY,
                                           int oldScrollX,
                                           int oldScrollY) {

                }
            });
        }
    }

    /**
     * set the id of the anchor
     *
     * @param id
     */
    public void setAnchorId(int id) {
        mTouchAnchorId = id;
    }

    /**
     * Get the view being used as anchor
     *
     * @return
     */
    public int getAnchorId() {
        return mTouchAnchorId;
    }

    /**
     * Set the location in the view to be the touch anchor
     *
     * @param x location in x 0 = left, 1 = right
     * @param y location in y 0 = top, 1 = bottom
     */
    public void setTouchAnchorLocation(float x, float y) {
        mTouchAnchorX = x;
        mTouchAnchorY = y;
    }

    /**
     * Sets the maximum velocity allowed on touch up.
     * Velocity is the rate of change in "progress" per second.
     *
     * @param velocity in progress per second 1 = one second to do the entire animation
     */
    public void setMaxVelocity(float velocity) {
        mMaxVelocity = velocity;
    }

    /**
     * set the maximum Acceleration allowed for a motion.
     * Acceleration is the rate of change velocity per second.
     *
     * @param acceleration
     */
    public void setMaxAcceleration(float acceleration) {
        mMaxAcceleration = acceleration;
    }

    float getMaxAcceleration() {
        return mMaxAcceleration;
    }

    /**
     * Gets the maximum velocity allowed on touch up.
     * Velocity is the rate of change in "progress" per second.
     *
     * @return
     */
    public float getMaxVelocity() {
        return mMaxVelocity;
    }

    boolean getMoveWhenScrollAtTop() {
        return mMoveWhenScrollAtTop;
    }

    /**
     * Get how the drag progress will return to the start or end state on touch up.
     * Can be ether COMPLETE_MODE_CONTINUOUS_VELOCITY (default) or COMPLETE_MODE_SPRING
     * @return
     */
    public int getAutoCompleteMode() {
        return mAutoCompleteMode;
    }
    /**
     * set how the drag progress will return to the start or end state on touch up.
     *
     *
     * @return
     */
    void setAutoCompleteMode(int autoCompleteMode) {
        mAutoCompleteMode = autoCompleteMode;
    }

    /**
     * This calculates the bounds of the mTouchRegionId view.
     * This reuses rect for efficiency as this class will be called many times.
     *
     * @param layout The layout containing the view (findViewId)
     * @param rect   the rectangle to fill provided so this function does not have to create memory
     * @return the rect or null
     */
    RectF getTouchRegion(ViewGroup layout, RectF rect) {
        if (mTouchRegionId == MotionScene.UNSET) {
            return null;
        }
        View view = layout.findViewById(mTouchRegionId);
        if (view == null) {
            return null;
        }
        rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
        return rect;
    }

    int getTouchRegionId() {
        return mTouchRegionId;
    }

    /**
     * This calculates the bounds of the mTouchRegionId view.
     * This reuses rect for efficiency as this class will be called many times.
     *
     * @param layout The layout containing the view (findViewId)
     * @param rect   the rectangle to fill provided for memory efficiency
     * @return the rect or null
     */
    RectF getLimitBoundsTo(ViewGroup layout, RectF rect) {
        if (mLimitBoundsTo == MotionScene.UNSET) {
            return null;
        }
        View view = layout.findViewById(mLimitBoundsTo);
        if (view == null) {
            return null;
        }
        rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
        return rect;
    }

    int getLimitBoundsToId() {
        return mLimitBoundsTo;
    }

    float dot(float dx, float dy) {
        return dx * mTouchDirectionX + dy * mTouchDirectionY;
    }

    @Override
    public String toString() {
        return Float.isNaN(mTouchDirectionX) ? "rotation"
                : (mTouchDirectionX + " , " + mTouchDirectionY);
    }

    /**
     * flags to control
     *
     * @return
     */
    public int getFlags() {
        return mFlags;
    }

    public void setTouchUpMode(int touchUpMode) {
        mOnTouchUp = touchUpMode;
    }

    /**
     * the stiffness of the spring if using spring
     *  K in "a = (-k*x-c*v)/m" equation for the acceleration of a spring
     * @return NaN if not set
     */
    public float getSpringStiffness() {
        return mSpringStiffness;
    }

    /**
     * the Mass of the spring if using spring
     *  m in "a = (-k*x-c*v)/m" equation for the acceleration of a spring
     * @return default is 1
     */
    public float getSpringMass() {
        return mSpringMass;
    }

    /**
     * the damping of the spring if using spring
     * c in "a = (-k*x-c*v)/m" equation for the acceleration of a spring
     * @return NaN if not set
     */
    public float getSpringDamping() {
        return mSpringDamping;
    }

    /**
     * The threshold below
     * @return NaN if not set
     */
    public float getSpringStopThreshold() {
        return mSpringStopThreshold;
    }

    /**
     * The spring's behaviour when it hits 0 or 1. It can be made ot overshoot or bounce
     * overshoot = 0
     * bounceStart = 1
     * bounceEnd = 2
     * bounceBoth = 3
     * @return Bounce mode
     */
    public int getSpringBoundary() {
        return mSpringBoundary;
    }

    boolean isDragStarted() {
        return mDragStarted;
    }

}