/* * Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner, * Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain, * Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter, * Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann, * Samuel Zweifel * * This file is part of Jukefox. * * Jukefox is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or any later version. Jukefox is * distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * Jukefox. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2011 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 ch.ethz.dcg.pancho3.tablet.widget; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.graphics.RectF; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; public class SwipeHelper { static final String TAG = "SwipeHelper"; private static final boolean DEBUG = false; private static final boolean DEBUG_INVALIDATE = false; private static final boolean SLOW_ANIMATIONS = false; // DEBUG; private static final boolean CONSTRAIN_SWIPE = true; private static final boolean FADE_OUT_DURING_SWIPE = true; private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; public static final int X = 0; public static final int Y = 1; private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec private int MAX_DISMISS_VELOCITY = 2000; // dp/sec private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width // where fade starts static final float ALPHA_FADE_END = 0.5f; // fraction of thumbnail width // beyond which alpha->0 private float mPagingTouchSlop; private Callback mCallback; private int mSwipeDirection; private VelocityTracker mVelocityTracker; private float mInitialTouchPos; private boolean mDragging; private View mCurrView; private View mCurrAnimView; private boolean mCanCurrViewBeDimissed; private float mDensityScale; public SwipeHelper(int swipeDirection, Callback callback, float densityScale, float pagingTouchSlop) { mCallback = callback; mSwipeDirection = swipeDirection; mVelocityTracker = VelocityTracker.obtain(); mDensityScale = densityScale; mPagingTouchSlop = pagingTouchSlop; } public void setDensityScale(float densityScale) { mDensityScale = densityScale; } public void setPagingTouchSlop(float pagingTouchSlop) { mPagingTouchSlop = pagingTouchSlop; } private float getPos(MotionEvent ev) { return mSwipeDirection == X ? ev.getX() : ev.getY(); } private float getTranslation(View v) { return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); } private float getVelocity(VelocityTracker vt) { return mSwipeDirection == X ? vt.getXVelocity() : vt.getYVelocity(); } private ObjectAnimator createTranslationAnimation(View v, float newPos) { ObjectAnimator anim = ObjectAnimator.ofFloat(v, mSwipeDirection == X ? "translationX" : "translationY", newPos); return anim; } private float getPerpendicularVelocity(VelocityTracker vt) { return mSwipeDirection == X ? vt.getYVelocity() : vt.getXVelocity(); } private void setTranslation(View v, float translate) { if (mSwipeDirection == X) { v.setTranslationX(translate); } else { v.setTranslationY(translate); } } private float getSize(View v) { return mSwipeDirection == X ? v.getMeasuredWidth() : v.getMeasuredHeight(); } private float getAlphaForOffset(View view) { float viewSize = getSize(view); final float fadeSize = ALPHA_FADE_END * viewSize; float result = 1.0f; float pos = getTranslation(view); if (pos >= viewSize * ALPHA_FADE_START) { result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize; } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) { result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize; } // Make .03 alpha the minimum so you always see the item a bit-- slightly below // .03, the item disappears entirely (as if alpha = 0) and that discontinuity looks // a bit jarring return Math.max(0.03f, result); } // invalidate the view's own bounds all the way up the view hierarchy public static void invalidateGlobalRegion(View view) { invalidateGlobalRegion( view, new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); } // invalidate a rectangle relative to the view's coordinate system all the way up the view // hierarchy public static void invalidateGlobalRegion(View view, RectF childBounds) { //childBounds.offset(view.getTranslationX(), view.getTranslationY()); if (DEBUG_INVALIDATE) { Log.v(TAG, "-------------"); } while (view.getParent() != null && view.getParent() instanceof View) { view = (View) view.getParent(); view.getMatrix().mapRect(childBounds); view.invalidate((int) Math.floor(childBounds.left), (int) Math.floor(childBounds.top), (int) Math.ceil(childBounds.right), (int) Math.ceil(childBounds.bottom)); if (DEBUG_INVALIDATE) { Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) + "," + (int) Math.floor(childBounds.top) + "," + (int) Math.ceil(childBounds.right) + "," + (int) Math.ceil(childBounds.bottom)); } } } public boolean onInterceptTouchEvent(MotionEvent ev) { return maybeStartDragging(ev); } private boolean maybeStartDragging(MotionEvent ev) { final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mDragging = false; mCurrView = mCallback.getChildAtPosition(ev); mVelocityTracker.clear(); if (mCurrView != null) { mCurrAnimView = mCallback.getChildContentView(mCurrView); mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); mVelocityTracker.addMovement(ev); mInitialTouchPos = getPos(ev); } break; case MotionEvent.ACTION_MOVE: if (mCurrView != null) { mVelocityTracker.addMovement(ev); float pos = getPos(ev); float delta = pos - mInitialTouchPos; if (Math.abs(delta) > mPagingTouchSlop) { mCallback.onBeginDrag(mCurrView); mDragging = true; mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView); } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mDragging = false; mCurrView = null; mCurrAnimView = null; break; } return mDragging; } /** * @param view * The view to be dismissed * @param velocity * The desired pixels/second speed at which the view should move */ public void dismissChild(final View view, float velocity) { /*final View animView = mCallback.getChildContentView(view); final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); float newPos; if (velocity < 0 || (velocity == 0 && getTranslation(animView) < 0) // if we use the Menu to dismiss an item in landscape, animate up || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) { newPos = -getSize(animView); } else { newPos = getSize(animView); } int duration = MAX_ESCAPE_ANIMATION_DURATION; if (velocity != 0) { duration = Math.min(duration, (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math .abs(velocity))); } else { duration = DEFAULT_ESCAPE_ANIMATION_DURATION; } animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator anim = createTranslationAnimation(animView, newPos); anim.setInterpolator(sLinearInterpolator); anim.setDuration(duration); anim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { mCallback.onChildDismissed(view); animView.setLayerType(View.LAYER_TYPE_NONE, null); } }); anim.addUpdateListener(new AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { animView.setAlpha(getAlphaForOffset(animView)); } invalidateGlobalRegion(animView); } }); anim.start();*/ mCallback.onChildDismissed(view); } public void snapChild(final View view, float velocity) { final View animView = mCallback.getChildContentView(view); final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView); ObjectAnimator anim = createTranslationAnimation(animView, 0); int duration = SNAP_ANIM_LEN; anim.setDuration(duration); anim.addUpdateListener(new AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { animView.setAlpha(getAlphaForOffset(animView)); } invalidateGlobalRegion(animView); } }); anim.start(); } public boolean onTouchEvent(MotionEvent ev) { if (!mDragging) { maybeStartDragging(ev); } if (!mDragging) { return false; } mVelocityTracker.addMovement(ev); final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_OUTSIDE: case MotionEvent.ACTION_MOVE: if (mCurrView != null) { float delta = getPos(ev) - mInitialTouchPos; // don't let items that can't be dismissed be dragged more than // maxScrollDistance if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) { float size = getSize(mCurrAnimView); float maxScrollDistance = 0.15f * size; if (Math.abs(delta) >= size) { delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; } else { delta = maxScrollDistance * (float) Math.sin(delta / size * (Math.PI / 2)); } } setTranslation(mCurrAnimView, delta); if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView)); } invalidateGlobalRegion(mCurrView); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (mCurrView != null) { float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; float velocity = getVelocity(mVelocityTracker); float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker); // Decide whether to dismiss the current view boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView); boolean childSwipedFastEnough = Math.abs(velocity) > escapeVelocity && Math.abs(velocity) > Math.abs(perpendicularVelocity) && velocity > 0 == getTranslation(mCurrAnimView) > 0; boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) && (childSwipedFastEnough || childSwipedFarEnough); if (dismissChild) { // flingadingy dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); } else { // snappity mCallback.onDragCancelled(mCurrView); snapChild(mCurrView, velocity); } } break; } return true; } public interface Callback { View getChildAtPosition(MotionEvent ev); View getChildContentView(View v); boolean canChildBeDismissed(View v); void onBeginDrag(View v); void onChildDismissed(View v); void onDragCancelled(View v); } }