Java tutorial
/* * 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.support.v4.view.MotionEventCompat; 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 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 = 2000; 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 track the position that has been pointed to. */ private int mSelectedPosition; /** * 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 #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() * 5; 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; mSelectedPosition = 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() & MotionEventCompat.ACTION_MASK; switch (action) { case MotionEvent.ACTION_DOWN: mDownMotionX = ev.getX(); mDownMotionY = ev.getY(); mSelectedPosition = INVALID_POSITION; mSelectedPosition = pointToPosition((int) mDownMotionX, (int) mDownMotionY); Log.d(TAG, "selectedPosition:" + mSelectedPosition); // If responsing to down action before the scroll has been // finished or in invalid position,it will lead to chaos of // itemswipeview. if (mSelectedPosition != INVALID_POSITION && mScroller.isFinished()) { mCurItemMainView = getChildAt(mSelectedPosition - getFirstVisiblePosition()) .findViewWithTag(ITEMMAIN_LAYOUT_TAG); mCurItemSwipeView = getChildAt(mSelectedPosition - 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 (mSelectedPosition != 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); } break; case MotionEvent.ACTION_MOVE: Log.d(TAG, "onTouchEvent:ACTION_MOVE"); mVelocityTracker.getYVelocity(); // determine whether the swipe action. if (Math.abs(getScrollXVelocity()) > mMinimumVelocity || (Math.abs(ev.getX() - mDownMotionX) > mTouchSlop && Math.abs(ev.getY() - mDownMotionY) < mTouchSlop)) { isSwiping = true; } 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 the // function might call itemClick function in the sliding. return true; } break; case MotionEvent.ACTION_UP: Log.d(TAG, "onTouchEvent:ACTION_UP"); if (isSwiping) { mLastItemMainView = mCurItemMainView; mLastItemSwipeView = mCurItemSwipeView; 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 && mSelectedPosition != INVALID_POSITION) { if (mScroller.computeScrollOffset()) { mLastItemMainView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); 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()"); } // 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); // Callback mRemoveItemCustomSwipeListener.onRemoveItemListener(mSelectedPosition); 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(mCurItemSwipeView.getLeft(), getChildAt(mSelectedPosition - getFirstVisiblePosition()).getTop(), mCurItemSwipeView.getRight(), getChildAt(mSelectedPosition - 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: mSwipeItemRightTriggerDeltaX = 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); } }