/* * Copyright 2012 Roman Nurik * Copyright 2013 Frankie Sardo * Copyright 2013 Niek Haarman * * 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.marshalchen.common.uimodule.listviewanimations.itemmanipulation.swipedismiss.contextualundo; import android.graphics.Rect; import android.view.*; import android.widget.AbsListView; import android.widget.AdapterView; import com.marshalchen.common.uimodule.listviewanimations.itemmanipulation.swipedismiss.DismissableManager; import com.marshalchen.common.uimodule.listviewanimations.itemmanipulation.swipedismiss.SwipeOnTouchListener; import com.marshalchen.common.uimodule.listviewanimations.util.AdapterViewUtil; import com.marshalchen.common.uimodule.nineoldandroids.animation.Animator; import com.marshalchen.common.uimodule.nineoldandroids.animation.AnimatorListenerAdapter; import static com.marshalchen.common.uimodule.nineoldandroids.view.ViewHelper.setAlpha; import static com.marshalchen.common.uimodule.nineoldandroids.view.ViewHelper.setTranslationX; import static com.marshalchen.common.uimodule.nineoldandroids.view.ViewPropertyAnimator.animate; /** * An {@link android.view.View.OnTouchListener} for the {@link ContextualUndoAdapter}. Don't use * this class directly, use ContextualUndoAdapter to wrap your * {@link android.widget.BaseAdapter}s. */ public class ContextualUndoListViewTouchListener implements SwipeOnTouchListener { // Cached ViewConfiguration and system-wide constant values private final int mSlop; private final int mMinFlingVelocity; private final int mMaxFlingVelocity; private final long mAnimationTime; // Fixed properties private final AbsListView mListView; private final Callback mCallback; private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero // Transient properties private float mDownX; private float mDownY; private boolean mSwiping; private VelocityTracker mVelocityTracker; private int mDownPosition; private View mDownView; private boolean mPaused; private boolean mDisallowSwipe; private boolean mIsParentHorizontalScrollContainer; private int mResIdOfTouchChild; private boolean mTouchChildTouched; private DismissableManager mDismissableManager; public interface Callback { void onViewSwiped(long dismissViewItemId, int dismissPosition); void onListScrolled(); } public ContextualUndoListViewTouchListener(final AbsListView listView, final Callback callback) { ViewConfiguration vc = ViewConfiguration.get(listView.getContext()); mSlop = vc.getScaledTouchSlop(); mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); mAnimationTime = listView.getContext().getResources().getInteger(android.R.integer.config_shortAnimTime); mListView = listView; mCallback = callback; } public void setEnabled(final boolean enabled) { mPaused = !enabled; } /** * Set the {@link com.marshalchen.common.uimodule.listviewanimations.itemmanipulation.swipedismiss.DismissableManager} to specify which views can or cannot be swiped. * @param dismissableManager null for no restrictions. */ @SuppressWarnings("UnusedDeclaration") public void setDismissableManager(final DismissableManager dismissableManager) { mDismissableManager = dismissableManager; } public AbsListView.OnScrollListener makeScrollListener() { return new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(final AbsListView absListView, final int scrollState) { setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); if (mPaused) { mCallback.onListScrolled(); } if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { mDisallowSwipe = true; } } @Override public void onScroll(final AbsListView absListView, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { } }; } @Override public boolean onTouch(final View view, final MotionEvent motionEvent) { if (mViewWidth < 2) { mViewWidth = mListView.getWidth(); } boolean result; switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: result = handleDownEvent(view, motionEvent); break; case MotionEvent.ACTION_MOVE: result = handleMoveEvent(view, motionEvent); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: result = handleUpCancelEvent(view, motionEvent); break; default: result = false; } return result; } private boolean handleDownEvent(final View view, final MotionEvent motionEvent) { mDisallowSwipe = false; if (mPaused) { return false; } // Find the child view that was touched (perform a hit test) Rect rect = new Rect(); int childCount = mListView.getChildCount(); int[] listViewCoords = new int[2]; mListView.getLocationOnScreen(listViewCoords); int x = (int) motionEvent.getRawX() - listViewCoords[0]; int y = (int) motionEvent.getRawY() - listViewCoords[1]; View child; for (int i = 0; i < childCount; i++) { child = mListView.getChildAt(i); child.getHitRect(rect); if (rect.contains(x, y)) { mDownView = child; break; } } if (mDownView != null && mDownView instanceof ContextualUndoView) { mDownX = motionEvent.getRawX(); mDownY = motionEvent.getRawY(); int downPosition = AdapterViewUtil.getPositionForView(mListView, mDownView); if (mDismissableManager != null) { long downId = mListView.getAdapter().getItemId(downPosition); if (!mDismissableManager.isDismissable(downId, downPosition)) { /* Cancel, not dismissable */ return false; } } mTouchChildTouched = !mIsParentHorizontalScrollContainer && mResIdOfTouchChild == 0; if (mResIdOfTouchChild != 0) { mIsParentHorizontalScrollContainer = false; final View childView = mDownView.findViewById(mResIdOfTouchChild); if (childView != null) { final Rect childRect = getChildViewRect(mListView, childView); if (childRect.contains((int) motionEvent.getX(), (int) motionEvent.getY())) { mTouchChildTouched = true; mListView.requestDisallowInterceptTouchEvent(true); } } } if (mIsParentHorizontalScrollContainer) { // Do it now and don't wait until the user moves more than // the slop factor. mTouchChildTouched = true; mListView.requestDisallowInterceptTouchEvent(true); } mDownY = motionEvent.getRawY(); mDownPosition = AdapterViewUtil.getPositionForView(mListView, mDownView); if (mTouchChildTouched) { mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(motionEvent); } else { mVelocityTracker = null; } } view.onTouchEvent(motionEvent); return true; } @SuppressWarnings("UnusedParameters") private boolean handleMoveEvent(final View view, final MotionEvent motionEvent) { if (mVelocityTracker == null || mPaused) { return false; } mVelocityTracker.addMovement(motionEvent); float deltaX = motionEvent.getRawX() - mDownX; float deltaY = motionEvent.getRawY() - mDownY; if (mTouchChildTouched && !mDisallowSwipe && Math.abs(deltaX) > mSlop && Math.abs(deltaX) > Math.abs(deltaY)) { mSwiping = true; mListView.requestDisallowInterceptTouchEvent(true); // Cancel ListView's touch (un-highlighting the item) MotionEvent cancelEvent = MotionEvent.obtain(motionEvent); cancelEvent.setAction(MotionEvent.ACTION_CANCEL | motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT); mListView.onTouchEvent(cancelEvent); cancelEvent.recycle(); } if (mSwiping) { setTranslationX(mDownView, deltaX); //noinspection MagicNumber setAlpha(mDownView, Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth))); return true; } return false; } @SuppressWarnings("UnusedParameters") private boolean handleUpCancelEvent(final View view, final MotionEvent motionEvent) { mDisallowSwipe = false; if (mVelocityTracker == null) { return false; } float deltaX = motionEvent.getRawX() - mDownX; mVelocityTracker.addMovement(motionEvent); mVelocityTracker.computeCurrentVelocity(1000); float velocityX = Math.abs(mVelocityTracker.getXVelocity()); float velocityY = Math.abs(mVelocityTracker.getYVelocity()); boolean dismiss = false; boolean dismissRight = false; final float absDeltaX = Math.abs(deltaX); if (absDeltaX > mViewWidth / 2) { dismiss = true; dismissRight = deltaX > 0; } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity && velocityY < velocityX && absDeltaX > mSlop) { dismiss = true; dismissRight = mVelocityTracker.getXVelocity() > 0; } if (dismiss) { // dismiss final long itemId = ((ContextualUndoView) mDownView).getItemId(); // before animation ends final int downPosition = mDownPosition; animate(mDownView).translationX(dismissRight ? mViewWidth : -mViewWidth).alpha(0).setDuration(mAnimationTime).setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(final Animator animation) { mCallback.onViewSwiped(itemId, downPosition); } }); } else { // cancel animate(mDownView).translationX(0).alpha(1).setDuration(mAnimationTime).setListener(null); } mVelocityTracker.recycle(); mVelocityTracker = null; mDownX = 0; mDownView = null; mDownPosition = AdapterView.INVALID_POSITION; mSwiping = false; return false; } @Override public boolean isSwiping() { return mSwiping; } private Rect getChildViewRect(final View parentView, View childView) { final Rect childRect = new Rect(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom()); if (parentView == childView) { return childRect; } ViewGroup parent; while ((parent = (ViewGroup) childView.getParent()) != parentView) { childRect.offset(parent.getLeft(), parent.getTop()); childView = parent; } return childRect; } void setIsParentHorizontalScrollContainer(final boolean isParentHorizontalScrollContainer) { mIsParentHorizontalScrollContainer = mResIdOfTouchChild == 0 && isParentHorizontalScrollContainer; } void setTouchChild(final int childResId) { mResIdOfTouchChild = childResId; if (childResId != 0) { setIsParentHorizontalScrollContainer(false); } } }