/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package com.linkbubble.physics; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.view.animation.LinearInterpolator; import android.view.animation.OvershootInterpolator; import com.linkbubble.MainController; import com.linkbubble.util.CrashTracking; import com.linkbubble.util.Util; public class DraggableHelper { public enum AnimationType { Linear, SmallOvershoot, MediumOvershoot, LargeOvershoot, DistanceProportion } public interface OnTouchActionEventListener { void onActionDown(TouchEvent event); void onActionMove(MoveEvent event); void onActionUp(ReleaseEvent event); } public static class TouchEvent { public int posX; public int posY; public float rawX; public float rawY; } public static class MoveEvent { public int dx; public int dy; public float rawX; public float rawY; } public static class ReleaseEvent { public int posX; public int posY; public float vx; public float vy; public float rawX; public float rawY; } private static class InternalMoveEvent { public InternalMoveEvent(float x, float y, long t) { mX = x; mY = y; mTime = t; } public long mTime; public float mX, mY; } // Reusable events TouchEvent mTouchEvent = new TouchEvent(); MoveEvent mMoveEvent = new MoveEvent(); ReleaseEvent mReleaseEvent = new ReleaseEvent(); private View mView; private WindowManager.LayoutParams mWindowManagerParams; private boolean mAlive; // Move animation state private int mInitialX; private int mInitialY; private int mTargetX; private int mTargetY; private float mAnimPeriod; private float mAnimTime; private AnimationType mAnimType; private LinearInterpolator mLinearInterpolator = new LinearInterpolator(); private OvershootInterpolator mOvershootInterpolatorSmall = new OvershootInterpolator(0.5f); private OvershootInterpolator mOvershootInterpolatorMedium = new OvershootInterpolator(1.5f); private OvershootInterpolator mOvershootInterpolatorLarge = new OvershootInterpolator(2.0f); private InternalMoveEvent mStartTouchRaw; private InternalMoveEvent mEndTouchRaw; private FlingTracker mFlingTracker = null; private int mStartTouchX = -1; private int mStartTouchY = -1; private AnimationEventListener mAnimationListener; private OnTouchActionEventListener mOnTouchActionEventListener; public interface AnimationEventListener { public void onAnimationComplete(); public void onCancel(); } public DraggableHelper(View view, WindowManager.LayoutParams windowManagerParams, boolean setOnTouchListener, OnTouchActionEventListener onTouchEventListener) { mView = view; mAlive = true; mWindowManagerParams = windowManagerParams; mOnTouchActionEventListener = onTouchEventListener; mStartTouchRaw = new InternalMoveEvent(0, 0, 0); mEndTouchRaw = new InternalMoveEvent(0, 0, 0); if (setOnTouchListener) { mView.setOnTouchListener(mOnTouchListener); } } private void addMoveEvent(float x, float y, long t) { if (mStartTouchRaw.mTime == 0) { mStartTouchRaw.mTime = t; mStartTouchRaw.mX = x; mStartTouchRaw.mY = y; } mEndTouchRaw.mTime = t; mEndTouchRaw.mX = x; mEndTouchRaw.mY = y; } public boolean onTouchActionDown(MotionEvent event) { mTouchEvent.posX = mWindowManagerParams.x; mTouchEvent.posY = mWindowManagerParams.y; mTouchEvent.rawX = event.getRawX(); mTouchEvent.rawY = event.getRawY(); addMoveEvent(event.getRawX(), event.getRawY(), event.getEventTime()); if (mOnTouchActionEventListener != null) { mOnTouchActionEventListener.onActionDown(mTouchEvent); } mStartTouchX = mWindowManagerParams.x; mStartTouchY = mWindowManagerParams.y; mFlingTracker = FlingTracker.obtain(); mFlingTracker.addMovement(event); return true; } public boolean onTouchActionMove(MotionEvent event) { if (mStartTouchX == -1 && mStartTouchY == -1) { onTouchActionDown(event); } float touchXRaw = event.getRawX(); float touchYRaw = event.getRawY(); int deltaX = (int) (touchXRaw - mStartTouchRaw.mX); int deltaY = (int) (touchYRaw - mStartTouchRaw.mY); addMoveEvent(touchXRaw, touchYRaw, event.getEventTime()); mMoveEvent.dx = deltaX; mMoveEvent.dy = deltaY; mMoveEvent.rawX = touchXRaw; mMoveEvent.rawY = touchYRaw; if (mOnTouchActionEventListener != null) { mOnTouchActionEventListener.onActionMove(mMoveEvent); } event.offsetLocation(mWindowManagerParams.x - mStartTouchX, mWindowManagerParams.y - mStartTouchY); mFlingTracker.addMovement(event); return true; } public boolean hasAtLeast2TouchEvents() { return mStartTouchRaw.mTime != 0 && mEndTouchRaw.mTime != 0 && mEndTouchRaw.mTime != mStartTouchRaw.mTime; } public boolean onTouchActionUp(MotionEvent event) { mReleaseEvent.posX = mWindowManagerParams.x; mReleaseEvent.posY = mWindowManagerParams.y; mReleaseEvent.vx = 0.0f; mReleaseEvent.vy = 0.0f; mReleaseEvent.rawX = event.getRawX(); mReleaseEvent.rawY = event.getRawY(); if (hasAtLeast2TouchEvents()) { float touchTime = (mEndTouchRaw.mTime - mStartTouchRaw.mTime) / 1000.0f; mReleaseEvent.vx = (mEndTouchRaw.mX - mStartTouchRaw.mX) / touchTime; mReleaseEvent.vy = (mEndTouchRaw.mY - mStartTouchRaw.mY) / touchTime; } // *Should* always be true, but under certain circumstances, is not. #384 if (mFlingTracker != null) { mFlingTracker.computeCurrentVelocity(1000); float fvx = mFlingTracker.getXVelocity(); float fvy = mFlingTracker.getYVelocity(); mReleaseEvent.vx = fvx; mReleaseEvent.vy = fvy; mFlingTracker.recycle(); } if (mOnTouchActionEventListener != null) { mOnTouchActionEventListener.onActionUp(mReleaseEvent); } mStartTouchX = -1; mStartTouchY = -1; mStartTouchRaw.mTime = 0; mEndTouchRaw.mTime = 0; return true; } private View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(android.view.View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { return onTouchActionDown(event); } case MotionEvent.ACTION_MOVE: { return onTouchActionMove(event); } case MotionEvent.ACTION_UP: { return onTouchActionUp(event); } //case MotionEvent.ACTION_CANCEL: { // return true; //} } return false; } }; public void cancelAnimation() { AnimationEventListener listener = mAnimationListener; mAnimationListener = null; clearTargetPos(); if (listener != null) { listener.onCancel(); } } public float getAnimCompleteFraction() { float f = 1.0f; if (mAnimPeriod > 0.0f) { f = Util.clamp(0.0f, mAnimTime / mAnimPeriod, 1.0f); } return f; } public void clearTargetPos() { // TODO: This probably fires. It can be disabled temporarily if a pain, but should be fixed. Util.Assert(mAnimationListener == null, "non-null mAnimationListener"); mInitialX = -1; mInitialY = -1; mTargetX = mWindowManagerParams.x; mTargetY = mWindowManagerParams.y; mAnimPeriod = 0.0f; mAnimTime = 0.0f; } public void setExactPos(int x, int y) { if ( mWindowManagerParams.x == x && mWindowManagerParams.y == y) { return; } mWindowManagerParams.x = x; mWindowManagerParams.y = y; mTargetX = x; mTargetY = y; if (mAlive) { MainController.updateRootWindowLayout(mView, mWindowManagerParams); } } public void setTargetPos(int x, int y, float t, AnimationType type, AnimationEventListener listener) { try { Util.Assert(mAnimationListener == null, "non-null mAnimationListener"); } catch (AssertionError exc) { CrashTracking.logHandledException(exc); } mAnimationListener = listener; if (x != mTargetX || y != mTargetY) { if (type == AnimationType.DistanceProportion) { // Something > 0.016 will have a high likelihood of causing < 60fps float maxTime = 0.005f; float maxDistance = 50.0f; float d = Util.distance(x, y, mWindowManagerParams.x, mWindowManagerParams.y); t = maxTime * d / maxDistance; t = maxTime - Util.clamp(0.0f, t, maxTime); type = AnimationType.Linear; } if (t < 0.0001f) { clearTargetPos(); setExactPos(x, y); } else { mAnimType = type; mInitialX = mWindowManagerParams.x; mInitialY = mWindowManagerParams.y; mTargetX = x; mTargetY = y; mAnimPeriod = t; mAnimTime = 0.0f; } if (MainController.get() != null) { MainController.get().scheduleUpdate(); } } else if (listener != null) { mAnimationListener = null; listener.onAnimationComplete(); } } public int getXPos() { return mWindowManagerParams.x; } public int getYPos() { return mWindowManagerParams.y; } public WindowManager.LayoutParams getWindowManagerParams() { return mWindowManagerParams; } public boolean isAlive() { return mAlive; } public View getView() { return mView; } public boolean update(float dt) { if (mAnimTime < mAnimPeriod) { Util.Assert(mAnimPeriod > 0.0f, "mAnimPeriod:" + mAnimPeriod); mAnimTime = Util.clamp(0.0f, mAnimTime + dt, mAnimPeriod); float tf = mAnimTime / mAnimPeriod; float interpolatedFraction = 0.0f; switch (mAnimType) { case Linear: interpolatedFraction = mLinearInterpolator.getInterpolation(tf); break; case SmallOvershoot: interpolatedFraction = mOvershootInterpolatorSmall.getInterpolation(tf); break; case MediumOvershoot: interpolatedFraction = mOvershootInterpolatorMedium.getInterpolation(tf); break; case LargeOvershoot: interpolatedFraction = mOvershootInterpolatorLarge.getInterpolation(tf); break; } int x = (int) (mInitialX + (mTargetX - mInitialX) * interpolatedFraction); int y = (int) (mInitialY + (mTargetY - mInitialY) * interpolatedFraction); if ( mWindowManagerParams.x != x || mWindowManagerParams.y != y) { mWindowManagerParams.x = x; mWindowManagerParams.y = y; MainController.updateRootWindowLayout(mView, mWindowManagerParams); } MainController.get().scheduleUpdate(); if (mAnimTime >= mAnimPeriod) { mAnimTime = 0.0f; mAnimPeriod = 0.0f; if (mAnimationListener != null) { AnimationEventListener l = mAnimationListener; mAnimationListener = null; l.onAnimationComplete(); } } return true; } return false; } public void destroy() { MainController.removeRootWindow(mView); mAlive = false; } }