/* * COPYRIGHT NOTICE * Copyright (C) 2015, xyczero <xiayuncheng1991@gmail.com> * * http://www.xyczero.com/ * * @license under the Apache License, Version 2.0 * * @file CustomSwipeListView.java * @brief Custom Swipe ListView * * @version 1.0 * @author xyczero * @date 2015/01/12 */ package com.xyczero.customswipelistview; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.widget.ListView; import android.widget.Scroller; /** * A view that shows items in a vertically scrolling list. The items come from * the {@link com.xyczero.customswipelistview.CustomSwipeBaseAdapter} associated with this view. * * @author xyczero */ public class CustomSwipeListView extends ListView { private static final String TAG = "com.xyczeo.customswipelistview"; /** * Indicates the tag of the adapter's itemMainView. */ public static final String ITEMMAIN_LAYOUT_TAG = "com.xyczeo.customswipelistview.itemmainlayout"; /** * Indicates the tag of the adapter's swipeLeftView. */ public static final String ITEMSWIPE_LAYOUT_TAG = "com.xyczeo.customswipelistview.swipeleftlayout"; /** * The unit is dip per second. */ private static final int MIN_VELOCITY = 500; private static final int MINIMUM_SWIPEITEM_TRIGGER_DELTAX = 5; /** * Touch mode of swipe. */ private static final int TOUCH_SWIPE_RIGHT = 1; private static final int TOUCH_SWIPE_LEFT = 2; private static final int TOUCH_SWIPE_AUTO = 3; private static final int TOUCH_SWIPE_NONE = 4; /** * Current touch mode of swipe; */ private int mCurTouchSwipeMode; /** * Rectangle used for hit testing children. */ private Rect mTouchFrame; private Scroller mScroller; private int mScreenWidth; private int mTouchSlop; private VelocityTracker mVelocityTracker; private int mMinimumVelocity; private int mMaximumVelocity; /** * Control the animation execution time. */ private final static int DEFAULT_DURATION = 250; private int mAnimationLeftDuration = DEFAULT_DURATION; private int mAnimationRightDuration = DEFAULT_DURATION; /** * The view that is shown in front of the listview by the position which the * finger point to currently; It indicates a general item view of the * listview; */ private View mCurItemMainView; /** * The view that is currently hidden in behind of {@link #mCurItemMainView} * by the position which the finger point to currently .It indicates a view * which might been shown when in the mode of {@link #TOUCH_SWIPE_LEFT} ; */ private View mCurItemSwipeView; /** * Same as {@link #mCurItemMainView} except that it was the last position * which the finger pointed to; */ private View mLastItemMainView; /** * Same as {@link #mCurItemSwipeView} except that it was the last position * which the finger pointed to; */ private View mLastItemSwipeView; /** * True if {@link #mLastItemSwipeView} is visible. */ private boolean isItemSwipeViewVisible; /** * True if clicking the position of {@link #mCurItemSwipeView}. Indicates * whether the listview will intercept the distribution of the touch event; */ private boolean isClickItemSwipeView; /** * True if triggering the swipe touch mode. Indicates whether trigger the * swipe touch mode. */ private boolean isSwiping; /** * Used to record the accumulation in the direction of X before determining * whether perform the swipe action. */ private float mAccumAbsDeltaX; /** * Used to record the accumulation in the direction of Y before determining * whether perform the swipe action */ private float mAccumAbsDeltaY; /** * Used to track the position that is pointed to. */ private int mCurSelectedPosition; /** * Used to track the position that was pointed to. */ private int mLastSelectedPosition; /** * Used to track the X coordinate when the first finger down to. */ private float mDownMotionX; /** * Used to track the Y coordinate when the first finger down to. */ private float mDownMotionY; /** * Control whether enable the {@link #isSwiping}. */ private boolean mEnableJudgeSwiping; /** * Control whether enable the {@link #TOUCH_SWIPE_RIGHT}. */ private boolean mEnableSwipeItemRight = true; /** * Control whether enable the {@link #TOUCH_SWIPE_LEFT}. */ private boolean mEnableSwipeItemLeft = true; /** * the minimum delta in x coordinate that whether triggers the * {@link #TOUCH_SWIPE_LEFT}. */ private int mSwipeItemLeftTriggerDeltaX; /** * the minimum delta in x coordinate that whether triggers the * {@link #TOUCH_SWIPE_RIGHT}. */ private int mSwipeItemRightTriggerDeltaX; /** * The listener that receives notifications when an item is removed in * {@link #TOUCH_SWIPE_RIGHT}. */ private RemoveItemCustomSwipeListener mRemoveItemCustomSwipeListener; public CustomSwipeListView(Context context) { super(context); initCustomSwipeListView(); } public CustomSwipeListView(Context context, AttributeSet attrs) { super(context, attrs); initCustomSwipeListView(); } public CustomSwipeListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initCustomSwipeListView(); } private void initCustomSwipeListView() { final Context context = getContext(); final ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); // set minimum velocity according to the MIN_VELOCITY. mMinimumVelocity = CustomSwipeUtils .convertDptoPx(context, MIN_VELOCITY); mScreenWidth = CustomSwipeUtils.getScreenWidth(context); mScroller = new Scroller(context); initSwipeItemTriggerDeltaX(); // set default value. mCurTouchSwipeMode = TOUCH_SWIPE_NONE; mCurSelectedPosition = INVALID_POSITION; } private void initSwipeItemTriggerDeltaX() { mSwipeItemLeftTriggerDeltaX = mScreenWidth / 3; mSwipeItemRightTriggerDeltaX = -mScreenWidth / 3; } private int getItemSwipeViewWidth(View itemSwipeView) { if (itemSwipeView != null) return mCurItemSwipeView.getLayoutParams().width; else return Integer.MAX_VALUE; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // just response single finger action. final int action = ev.getAction() & MotionEvent.ACTION_MASK; switch (action) { case MotionEvent.ACTION_DOWN: mDownMotionX = ev.getX(); mDownMotionY = ev.getY(); mCurSelectedPosition = INVALID_POSITION; mCurSelectedPosition = pointToPosition((int) mDownMotionX, (int) mDownMotionY); Log.d(TAG, "selectedPosition:" + mCurSelectedPosition); if (mCurSelectedPosition != INVALID_POSITION) { mCurItemMainView = getChildAt( mCurSelectedPosition - getFirstVisiblePosition()) .findViewWithTag(ITEMMAIN_LAYOUT_TAG); mCurItemSwipeView = getChildAt( mCurSelectedPosition - getFirstVisiblePosition()) .findViewWithTag(ITEMSWIPE_LAYOUT_TAG); isClickItemSwipeView = isInSwipePosition((int) mDownMotionX, (int) mDownMotionY); } Log.d(TAG, "onInterceptTouchEvent:ACTION_DOWN" + "--" + isClickItemSwipeView); break; case MotionEvent.ACTION_UP: // clear data and give initial value if (isClickItemSwipeView) { mCurItemSwipeView.setVisibility(GONE); mCurItemMainView.scrollTo(0, 0); mLastItemMainView = null; mLastItemSwipeView = null; isItemSwipeViewVisible = false; } recycleVelocityTracker(); Log.d(TAG, "onInterceptTouchEvent:ACTION_UP" + "--" + isClickItemSwipeView); break; case MotionEvent.ACTION_CANCEL: recycleVelocityTracker(); Log.d(TAG, "onInterceptTouchEvent:ACTION_CANCEL" + "--" + isClickItemSwipeView); break; default: return false; } // Return true and don't intercept the touch event if clicking the // itemswipeview. return !isClickItemSwipeView; } @Override public boolean onTouchEvent(MotionEvent ev) { // Just response single finger action. final int action = ev.getAction() & MotionEvent.ACTION_MASK; final int x = (int) ev.getX(); if (action == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); return super.onTouchEvent(ev); } //if the next action_move is coming after the second action_down //when the ItemSwipeView is still swiping with the first action, //it will not allow the following actions until the ItemSwipeView has finished the swiping. if (mEnableJudgeSwiping && isSwiping) { ev.setAction(MotionEvent.ACTION_CANCEL); return super.onTouchEvent(ev); } if (mCurSelectedPosition != INVALID_POSITION) { addVelocityTrackerMotionEvent(ev); switch (action) { case MotionEvent.ACTION_DOWN: Log.d(TAG, "onTouchEvent:ACTION_DOWN"); // If there is a itemswipeview and then don't click it // by the next down action,it will first return to original // state and cancel to response the following actions. if (isItemSwipeViewVisible) { if (!isClickItemSwipeView) { mLastItemSwipeView.setVisibility(GONE); mLastItemMainView.scrollTo(0, 0); } isItemSwipeViewVisible = false; ev.setAction(MotionEvent.ACTION_CANCEL); return super.onTouchEvent(ev); } mEnableJudgeSwiping = true; break; case MotionEvent.ACTION_MOVE: Log.d(TAG, "onTouchEvent:ACTION_MOVE"); mVelocityTracker.getYVelocity(); // This is a remedial action in case of the finger clicking down // quickly again after TOUCH_SWIPE_LEFT. // At that moment the mScroller may not finish so // isItemSwipeViewVisible is still false when the finger clicks // down. if (isItemSwipeViewVisible) { if (!isClickItemSwipeView) { mLastItemSwipeView.setVisibility(GONE); mLastItemMainView.scrollTo(0, 0); } isItemSwipeViewVisible = false; } // determine whether perform the swipe action for the whole touch event. if (mEnableJudgeSwiping) { mAccumAbsDeltaX = mAccumAbsDeltaX + Math.abs(ev.getX() - mDownMotionX); mAccumAbsDeltaY = mAccumAbsDeltaY + Math.abs(ev.getY() - mDownMotionY); if (mAccumAbsDeltaY >= mTouchSlop) { isSwiping = false; mEnableJudgeSwiping = false; } else if (mAccumAbsDeltaX >= mTouchSlop) { isSwiping = true; mEnableJudgeSwiping = false; } } else { mAccumAbsDeltaX = 0; mAccumAbsDeltaY = 0; } if (isSwiping) { int deltaX = (int) mDownMotionX - x; if (deltaX > 0 && mEnableSwipeItemLeft || deltaX < 0 && mEnableSwipeItemRight) { mDownMotionX = x; mCurItemMainView.scrollBy(deltaX, 0); } // if super.onTouchEvent() that been called there,it might // lead to the specified item out of focus due to // it might call itemClick function in the sliding. return true; } break; case MotionEvent.ACTION_UP: Log.d(TAG, "onTouchEvent:ACTION_UP"); if (isSwiping) { // Record the old view and position mLastItemMainView = mCurItemMainView; mLastItemSwipeView = mCurItemSwipeView; mLastSelectedPosition = mCurSelectedPosition; final int velocityX = getScrollXVelocity(); if (velocityX > mMinimumVelocity) { scrollByTouchSwipeMode(TOUCH_SWIPE_RIGHT, -mScreenWidth); } else if (velocityX < -mMinimumVelocity) { scrollByTouchSwipeMode(TOUCH_SWIPE_LEFT, getItemSwipeViewWidth(mLastItemSwipeView)); } else { scrollByTouchSwipeMode(TOUCH_SWIPE_AUTO, Integer.MIN_VALUE); } recycleVelocityTracker(); // TODO:To be optimized for not calling computeScroll // function. if (mScroller.isFinished()) { isSwiping = false; } // prevent to trigger OnItemClick by transverse sliding // distance too slow or too small OnItemClick events when in // swipe mode. ev.setAction(MotionEvent.ACTION_CANCEL); return super.onTouchEvent(ev); } break; default: break; } } return super.onTouchEvent(ev); } @Override public void computeScroll() { if (isSwiping && mLastSelectedPosition != INVALID_POSITION) { if (mScroller.computeScrollOffset()) { mLastItemMainView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); //when triggering the action_down,AbListview will call the abortAnimation function. //Then the mFinished will be true; //Other one, the computeScroll is callback by onDraw(). if (mScroller.isFinished()) { isSwiping = false; switch (mCurTouchSwipeMode) { case TOUCH_SWIPE_LEFT: // show itemswipeview mLastItemSwipeView.setVisibility(VISIBLE); isItemSwipeViewVisible = true; break; case TOUCH_SWIPE_RIGHT: if (mRemoveItemCustomSwipeListener == null) { throw new NullPointerException( "RemoveItemCustomSwipeListener is null, we should called setRemoveItemCustomSwipeListener()"); } // Callback mRemoveItemCustomSwipeListener .onRemoveItemListener(mLastSelectedPosition); // Before the view in the selected position is // deleted,it needs to return to original state because // the next position will be setted in this position. mLastItemMainView.scrollTo(0, 0); break; default: break; } } } } super.computeScroll(); } /** * True if clicking in the itemswipeview position. * * @param x the x coordinate which gets in the down action * @param y the y coordinate which gets in the down action * @return */ private boolean isInSwipePosition(int x, int y) { Rect frame = mTouchFrame; if (frame == null) { mTouchFrame = new Rect(); frame = mTouchFrame; } // The premise is that the itemswipeview is visible. if (isItemSwipeViewVisible) { frame.set( mLastItemSwipeView.getLeft(), getChildAt(mLastSelectedPosition - getFirstVisiblePosition()) .getTop(), mLastItemSwipeView.getRight(), getChildAt(mLastSelectedPosition - getFirstVisiblePosition()) .getBottom()); if (frame.contains(x, y)) { return true; } } return false; } private void addVelocityTrackerMotionEvent(MotionEvent ev) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); } private void recycleVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } /** * Get the velocity in the direction of x coordinate per second. * * @return */ private int getScrollXVelocity() { mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int velocity = (int) mVelocityTracker.getXVelocity(); return velocity; } /** * @param touchSwipeMode the swipe mode{@link #mCurTouchSwipeMode} * @param targetDelta The target delta in the direction of x coordinate that will be * sliding by ignoring the delta that has been sliding. */ private void scrollByTouchSwipeMode(int touchSwipeMode, int targetDelta) { mCurTouchSwipeMode = touchSwipeMode; switch (touchSwipeMode) { case TOUCH_SWIPE_RIGHT: scrollByTartgetDelta(targetDelta, mAnimationRightDuration); case TOUCH_SWIPE_LEFT: scrollByTartgetDelta(targetDelta, mAnimationLeftDuration); break; case TOUCH_SWIPE_AUTO: scrollByAuto(); break; default: break; } } /** * Calculate the actual delta in the direction of x coordinate by taking the * delta that has been sliding into consideration. * * @param targetDelta The target delta in the direction of x coordinate that will be * sliding by ignoring the delta that has been sliding. * @param animationDuration Animation execution time. */ private void scrollByTartgetDelta(final int targetDelta, int animationDuration) { final int itemMainScrollX = mLastItemMainView.getScrollX(); final int actualDelta = (targetDelta - itemMainScrollX); mScroller.startScroll(itemMainScrollX, 0, actualDelta, 0, animationDuration); postInvalidate(); } /** * Determine whether meet the trigger condition according to the delta that * has been sliding when the x velocity doesn't meet the trigger condition. */ private void scrollByAuto() { final int itemMainScrollX = mLastItemMainView.getScrollX(); if (itemMainScrollX >= mSwipeItemLeftTriggerDeltaX) { scrollByTouchSwipeMode(TOUCH_SWIPE_LEFT, getItemSwipeViewWidth(mLastItemSwipeView)); } else if (itemMainScrollX <= mSwipeItemRightTriggerDeltaX) { scrollByTouchSwipeMode(TOUCH_SWIPE_RIGHT, -mScreenWidth); } else { // Return to original state due to not meet the conditions. // TODO:To be optimized for not calling computeScroll function. mLastItemMainView.scrollTo(0, 0); mLastItemSwipeView.setVisibility(GONE); isItemSwipeViewVisible = false; } } /** * set the animation time in swiping left * * @param duration millisecond */ public void setAnimationLeftDuration(int duration) { mAnimationRightDuration = duration; } /** * set the animation time in swiping right * * @param duration millisecond */ public void setAnimationRightDuration(int duration) { mAnimationLeftDuration = duration; } public void setSwipeItemLeftEnable(boolean enable) { mEnableSwipeItemLeft = enable; } public void setSwipeItemRightEnable(boolean enable) { mEnableSwipeItemRight = enable; } public void setSwipeItemRightTriggerDeltaX(int dipDeltaX) { if (dipDeltaX < MINIMUM_SWIPEITEM_TRIGGER_DELTAX) return; final int pxDeltaX = CustomSwipeUtils.convertDptoPx(getContext(), dipDeltaX); setSwipeItemTriggerDeltaX(TOUCH_SWIPE_RIGHT, pxDeltaX); } public void setSwipeItemLeftTriggerDeltaX(int dipDeltaX) { if (dipDeltaX < MINIMUM_SWIPEITEM_TRIGGER_DELTAX) return; final int pxDeltaX = CustomSwipeUtils.convertDptoPx(getContext(), dipDeltaX); setSwipeItemTriggerDeltaX(TOUCH_SWIPE_LEFT, pxDeltaX); } private void setSwipeItemTriggerDeltaX(int touchMode, int pxDeltaX) { switch (touchMode) { case TOUCH_SWIPE_RIGHT: mSwipeItemRightTriggerDeltaX = pxDeltaX <= mScreenWidth ? -pxDeltaX : -mScreenWidth; break; case TOUCH_SWIPE_LEFT: mSwipeItemLeftTriggerDeltaX = pxDeltaX <= mScreenWidth ? pxDeltaX : mScreenWidth; break; default: break; } } /** * Register a callback to be invoked when an item in this Listview has been * removed in {@link #TOUCH_SWIPE_RIGHT}. * * @param removeItemCustomSwipeListener */ public void setRemoveItemCustomSwipeListener( RemoveItemCustomSwipeListener removeItemCustomSwipeListener) { mRemoveItemCustomSwipeListener = removeItemCustomSwipeListener; } /** * Interface definition for a callback to be invoked when an item in this * Listview has been removed in {@link #TOUCH_SWIPE_RIGHT}. */ public interface RemoveItemCustomSwipeListener { /** * Callback method to be invoked when an item in this Listview has been * removed. * * @param selectedPostion the position which has been removed. */ void onRemoveItemListener(int selectedPostion); } }