/* * Copyright (C) 2008 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 com.android.launcher2; import android.content.Context; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.util.Log; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; import java.util.ArrayList; public class SwipeController { private static final String TAG = "Launcher.SwipeController"; private static final int FRAME_DELAY = 1000 / 30; private static final float DECAY_CONSTANT = 0.65f; private static final float SPRING_CONSTANT = 0.0009f; // configuration private SwipeListener mListener; private int mSlop; private float mSwipeDistance; // state private VelocityTracker mVelocityTracker; private boolean mCanceled; private boolean mTracking; private int mDownX; private int mDownY; private float mMinDest; private float mMaxDest; private long mFlingTime; private long mLastTime; private int mDirection; private float mVelocity; private float mDest; private float mAmount; public interface SwipeListener { public void onStartSwipe(); public void onFinishSwipe(int amount); public void onSwipe(float amount); } public SwipeController(Context context, SwipeListener listener) { ViewConfiguration config = ViewConfiguration.get(context); mSlop = config.getScaledTouchSlop(); DisplayMetrics display = context.getResources().getDisplayMetrics(); mSwipeDistance = display.heightPixels / 2; // one half of the screen mListener = listener; } public void setRange(float min, float max) { mMinDest = min; mMaxDest = max; } public void cancelSwipe() { mCanceled = true; } public boolean onInterceptTouchEvent(MotionEvent ev) { onTouchEvent(ev); // After we return true, onIntercept doesn't get called any more, so this is // a good place to do the callback. if (mTracking) { mListener.onStartSwipe(); } return mTracking; } public boolean onTouchEvent(MotionEvent ev) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); final int screenX = (int)ev.getRawX(); final int screenY = (int)ev.getRawY(); final int deltaX = screenX - mDownX; final int deltaY = screenY - mDownY; final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: // Remember location of down touch mCanceled = false; mTracking = false; mDownX = screenX; mDownY = screenY; break; case MotionEvent.ACTION_MOVE: if (!mCanceled && !mTracking) { if (Math.abs(deltaX) > mSlop) { mCanceled = true; mTracking = false; } if (Math.abs(deltaY) > mSlop) { mTracking = true; } } if (mTracking && !mCanceled) { track(screenY); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: if (mTracking && !mCanceled) { fling(screenY); } mVelocityTracker.recycle(); mVelocityTracker = null; break; } return mTracking || mCanceled; } /** * Set the value, performing an animation. Make sure that you have set the * range properly first, otherwise the value may be clamped. */ public void animate(float dest) { go(dest, 0); } /** * Set the value, but don't perform an animation. Make sure that you have set the * range properly first, otherwise the value may be clamped. */ public void setImmediate(float dest) { go(dest, dest); } /** * Externally start a swipe. If dest == amount, this will end up just immediately * setting the value, but it will perform the proper start and finish callbacks. */ private void go(float dest, float amount) { mListener.onStartSwipe(); dest = clamp(dest); mDirection = dest > amount ? 1 : -1; // if they're equal it doesn't matter mVelocity = mDirection * 0.002f; // TODO: density. mAmount = amount; mDest = dest; mFlingTime = SystemClock.uptimeMillis(); mLastTime = 0; scheduleAnim(); } private float clamp(float v) { if (v < mMinDest) { return mMinDest; } else if (v > mMaxDest) { return mMaxDest; } else { return v; } } /** * Perform the callbacks. */ private void track(int screenY) { mAmount = clamp((screenY - mDownY) / mSwipeDistance); mListener.onSwipe(mAmount); } private void fling(int screenY) { mVelocityTracker.computeCurrentVelocity(1); mVelocity = mVelocityTracker.getYVelocity() / mSwipeDistance; Log.d(TAG, "mVelocity=" + mVelocity); mDirection = mVelocity >= 0.0f ? 1 : -1; mAmount = clamp((screenY-mDownY)/mSwipeDistance); if (mAmount < 0) { mDest = clamp(mVelocity < 0 ? -1.0f : 0.0f); } else { mDest = clamp(mVelocity < 0 ? 0.0f : 1.0f); } mFlingTime = SystemClock.uptimeMillis(); mLastTime = 0; scheduleAnim(); } private void scheduleAnim() { boolean send = true; if (mDirection > 0) { if (mAmount > (mDest - 0.01f)) { send = false; } } else { if (mAmount < (mDest + 0.01f)) { send = false; } } if (send) { mHandler.sendEmptyMessageDelayed(1, FRAME_DELAY); } else { mListener.onFinishSwipe((int)(mAmount >= 0 ? (mAmount+0.5f) : (mAmount-0.5f))); } } Handler mHandler = new Handler() { public void handleMessage(Message msg) { long now = SystemClock.uptimeMillis(); final long t = now - mFlingTime; final long dt = t - mLastTime; mLastTime = t; final float timeSlices = dt / (float)FRAME_DELAY; float distance = mDest - mAmount; mVelocity += timeSlices * mDirection * SPRING_CONSTANT * distance * distance / 2; mVelocity *= (timeSlices * DECAY_CONSTANT); mAmount += timeSlices * mVelocity; mAmount += distance * timeSlices * 0.2f; // cheat mListener.onSwipe(mAmount); scheduleAnim(); } }; }