Android實現(xiàn)左滑刪除控件
背景:在android開發(fā)中,列表是經(jīng)常會使用到的一個主要控件,列表中可以展示大量的數(shù)據(jù),像訂單、商品、通訊錄、瀏覽記錄或者關注列表等等??赡墚a(chǎn)品一開始需求只做簡單的數(shù)據(jù)展示,但后期隨著功能越來越多,越來越完善,產(chǎn)品可能說在列表里面增加一些交互能力。比如說訂單列表里面,一開始只是展示訂單數(shù)據(jù),后面需要加上刪除訂單的功能,以前Android中這種功能要的很多的可能就是長按操作這種的,因為程序猿只需要很少的代碼就能實現(xiàn)。但是ios的習慣操作是左滑刪除,為了保持統(tǒng)一的操作習慣,兩端保持一致,最終產(chǎn)品會讓Android程序猿去實現(xiàn)一種和ios一模一樣的功能。如果你的代碼已經(jīng)維護了很久,代碼量比較大,不愿意去大改,那么今天這個控件就能輕松的助你完成左滑刪除的功能。
先上效果圖:

設計思路:最好以最小的代碼侵入來實現(xiàn)左滑刪除的功能,在不破壞原來邏輯的基礎上,只需稍加改造便可具備左滑刪除的能力。
首先分析下左滑刪除的基礎原理:

原理分析:
1. 正常狀態(tài)下,我們看到的是完整的內容部分,右側菜單部分因為超出屏幕所以不在視線范圍內。
2. 手指滑動過程中,容器的內容跟隨手指移動,從而拉出在屏幕外面的菜單區(qū)域。
3. 當手指松開的時候,我們先假定一種邏輯,如果菜單區(qū)域顯示超過一半,那就全部顯示;如果少于一半那就滑出隱藏。
滑動原理分析完了之后,我們大概就有了實現(xiàn)思路了:
首先我們的控件里面需要兩塊區(qū)域,因為以前可能已經(jīng)實現(xiàn)了列表item的顯示,如果能不做任何改動,直接把以前的item包含到我們的內容區(qū)域里面來,那么我們內容區(qū)域就輕松搞定了。
菜單區(qū)域,需要什么能力,就把相關的View也傳遞給我容器,然后容器放到相應位置。
談笑間,簡單兩步我們的左滑刪除容器已經(jīng)完成一個簡單的雛形了!
接下來就是代碼實現(xiàn):
步驟一:內容和菜單分別加入容器
/**
* 設置內容區(qū)域
* @param contentView
*/
public void addContentView(View contentView) {
this.mContentView = contentView;
this.mContentView.setTag("contentView");
View cv = findViewWithTag("contentView");
if (cv != null) {
this.removeView(cv);
}
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
this.addView(this.mContentView, layoutParams);
}
/**
* 設置右邊菜單區(qū)域
*/
public void addMenuView(View menuView) {
this.mMenuView = menuView;
this.mMenuView.setTag("menuView");
View mv = findViewWithTag("menuView");
if (mv != null) {
this.removeView(mv);
}
LayoutParams layoutParams = new LayoutParams(mRightCanSlide, ViewGroup.LayoutParams.MATCH_PARENT);
this.addView(this.mMenuView, layoutParams);
}
步驟二:左滑處理
/**
* 攔截觸摸事件
*
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionMasked = ev.getActionMasked();
Log.e(TAG, "onInterceptTouchEvent: actionMasked = " + actionMasked);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
mInitX = ev.getRawX() + getScrollX();
mInitY = ev.getRawY();
clearAnim();
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
}
if (mCardView != null) {
mCardView.requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_MOVE:
if (mInitX - ev.getRawX() < 0) {
// 讓父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
// 阻止ViewPager攔截事件
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
}
return false;
}
// y軸方向上達到滑動最小距離, x 軸未達到
if (Math.abs(ev.getRawY() - mInitY) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) > Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 讓父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
return false;
}
// x軸方向達到了最小滑動距離,y軸未達到
if (Math.abs(mInitX - ev.getRawX() - getScrollX()) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) <= Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 阻止父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mRecyclerView != null) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = true;
}
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 處理觸摸事件
* 需要注意何時處理左滑,何時不處理
*
* @param ev
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
int actionMasked = ev.getActionMasked();
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
mInitX = ev.getRawX() + getScrollX();
mInitY = ev.getRawY();
clearAnim();
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
}
if (mCardView != null) {
mCardView.requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_MOVE:
if (mInitX - ev.getRawX() < 0) {
// 讓父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
// 阻止ViewPager攔截事件
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
}
// y軸方向上達到滑動最小距離, x 軸未達到
if (Math.abs(ev.getRawY() - mInitY) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) > Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 讓父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
}
// x軸方向達到了最小滑動距離,y軸未達到
if (Math.abs(mInitX - ev.getRawX() - getScrollX()) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) <= Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 阻止父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
}
/** 如果手指移動距離超過最小距離 */
float translationX = mInitX - ev.getRawX();
// 如果滑動距離已經(jīng)大于右邊可伸縮的距離后, 應該重新設置initx
if (translationX > mRightCanSlide) {
mInitX = ev.getRawX() + mRightCanSlide;
}
// 如果互動距離小于0,那么重新設置初始位置initx
if (translationX < 0) {
mInitX = ev.getRawX();
}
translationX = translationX > mRightCanSlide ? mRightCanSlide : translationX;
translationX = translationX < 0 ? 0 : translationX;
// 向左滑動
if (translationX <= mRightCanSlide && translationX >= 0) {
scrollTo((int) translationX, 0);
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mRecyclerView != null) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = true;
}
upAnim();
return true;
default:
break;
}
return true;
}
以上兩個方法主要處理了左滑移動功能以及滑動沖突問題,如果用的是RecyclerView那么為了防止垂直方向的同向沖突,那么需要將外層的RecyclerView傳入左滑容器,在這個容器中會處理滑動沖突。
到這就已經(jīng)實現(xiàn)了左滑功能,并且解決掉了垂直方向上的滑動沖突,然后我們還要實現(xiàn)一個功能是:如果有一個item向左滑動并顯示出右邊的菜單區(qū)域,當手指再次按下或者列表滑動的時候,需要將已經(jīng)顯示菜單區(qū)域的item收起,恢復原來的狀態(tài)。為了提供這個能力,左滑容器里面提供一個菜單狀態(tài)變化的監(jiān)聽:
/**
* 刪除按鈕狀態(tài)變化監(jiān)聽
*/
public interface OnDelViewStatusChangeLister {
/**
* 狀態(tài)變化監(jiān)聽
* @param show 是否正在顯示
*/
void onStatusChange(boolean show);
}
/**
* 重置 菜單展開/菜單收起 狀態(tài)
*/
public void resetDelStatus() {
int scrollX = getScrollX();
if (scrollX == 0) {
return;
}
clearAnim();
mValueAnimator = ValueAnimator.ofInt(scrollX, 0);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
scrollTo(value, 0);
}
});
mValueAnimator.setDuration(mAnimDuring);
mValueAnimator.start();
}
菜單展開或者收起都會調用這個方法,方便第三方調用者處理狀態(tài)。
再者還有就是加上動畫,讓滑動更加柔和:
/**
* 手指抬起執(zhí)行動畫
*/
private void upAnim() {
int scrollX = getScrollX();
if (scrollX == mRightCanSlide || scrollX == 0) {
if (mStatusChangeLister != null) {
mStatusChangeLister.onStatusChange(scrollX == mRightCanSlide);
}
return;
}
clearAnim();
// 如果顯出一半松開手指,那么自動完全顯示。否則完全隱藏
if (scrollX >= mRightCanSlide / 2) {
mValueAnimator = ValueAnimator.ofInt(scrollX, mRightCanSlide);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
scrollTo(value, 0);
}
});
mValueAnimator.setDuration(mAnimDuring);
mValueAnimator.start();
if (mStatusChangeLister != null) {
mStatusChangeLister.onStatusChange(true);
}
}
else {
mValueAnimator = ValueAnimator.ofInt(scrollX, 0);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
scrollTo(value, 0);
}
});
mValueAnimator.setDuration(mAnimDuring);
mValueAnimator.start();
if (mStatusChangeLister != null) {
mStatusChangeLister.onStatusChange(false);
}
}
}
#最后貼上左滑刪除容器的完整代碼:
/**
* @author luowang
* @date 2020-08-19 17:31
* 左滑刪除View
*/
public class LeftSlideView extends LinearLayout {
/**
* tag
*/
public static final String TAG = "LeftSlideView";
/**
* 上下文
*/
private Context mContext;
/**
* 最小觸摸距離
*/
private int mTouchSlop;
/**
* 右邊可滑動距離
*/
private int mRightCanSlide;
/**
* 按下x
*/
private float mInitX;
/**
* 按下y
*/
private float mInitY;
/**
* 屬性動畫
*/
private ValueAnimator mValueAnimator;
/**
* 動畫時長
*/
private int mAnimDuring = 200;
/**
* 刪除按鈕的長度
*/
private int mDelLength = 76;
/**
* ViewPager
*/
private ViewPager mViewPager;
/**
* RecyclerView
*/
private RecyclerView mRecyclerView;
/** CardView */
private CardView mCardView;
/** 是否重新計算 */
private boolean isReCompute = true;
/** 狀態(tài)監(jiān)聽 */
private OnDelViewStatusChangeLister mStatusChangeLister;
/**
* 內容區(qū)域View
*/
private View mContentView;
/**
* 菜單區(qū)域View
*/
private View mMenuView;
public LeftSlideView(Context context) {
this(context, null);
}
public LeftSlideView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LeftSlideView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
init();
}
/**
* 初始化
*/
private void init() {
mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
mRightCanSlide = DPIUtil.dip2px(mContext, mDelLength);
setBackgroundColor(Color.TRANSPARENT);
// 水平布局
setOrientation(LinearLayout.HORIZONTAL);
initView();
}
/**
* 設置內容區(qū)域
* @param contentView
*/
public void addContentView(View contentView) {
this.mContentView = contentView;
this.mContentView.setTag("contentView");
View cv = findViewWithTag("contentView");
if (cv != null) {
this.removeView(cv);
}
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
this.addView(this.mContentView, layoutParams);
}
/**
* 設置右邊菜單區(qū)域
*/
public void addMenuView(View menuView) {
this.mMenuView = menuView;
this.mMenuView.setTag("menuView");
View mv = findViewWithTag("menuView");
if (mv != null) {
this.removeView(mv);
}
LayoutParams layoutParams = new LayoutParams(mRightCanSlide, ViewGroup.LayoutParams.MATCH_PARENT);
this.addView(this.mMenuView, layoutParams);
}
/**
* 設置Viewpager
*/
public void setViewPager(ViewPager viewPager) {
mViewPager = viewPager;
}
/**
* 設置RecyclerView
*/
public void setRecyclerView(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
}
/** 設置CardView */
public void setCardView(CardView cardView) {
mCardView = cardView;
}
/** 設置狀態(tài)監(jiān)聽 */
public void setStatusChangeLister(OnDelViewStatusChangeLister statusChangeLister) {
mStatusChangeLister = statusChangeLister;
}
/**
* 初始化View
*/
private void initView() {
}
/**
* 攔截觸摸事件
*
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int actionMasked = ev.getActionMasked();
Log.e(TAG, "onInterceptTouchEvent: actionMasked = " + actionMasked);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
mInitX = ev.getRawX() + getScrollX();
mInitY = ev.getRawY();
clearAnim();
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
}
if (mCardView != null) {
mCardView.requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_MOVE:
if (mInitX - ev.getRawX() < 0) {
// 讓父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
// 阻止ViewPager攔截事件
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
}
return false;
}
// y軸方向上達到滑動最小距離, x 軸未達到
if (Math.abs(ev.getRawY() - mInitY) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) > Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 讓父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
return false;
}
// x軸方向達到了最小滑動距離,y軸未達到
if (Math.abs(mInitX - ev.getRawX() - getScrollX()) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) <= Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 阻止父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mRecyclerView != null) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = true;
}
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 處理觸摸事件
* 需要注意何時處理左滑,何時不處理
*
* @param ev
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
int actionMasked = ev.getActionMasked();
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
mInitX = ev.getRawX() + getScrollX();
mInitY = ev.getRawY();
clearAnim();
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
}
if (mCardView != null) {
mCardView.requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_MOVE:
if (mInitX - ev.getRawX() < 0) {
// 讓父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
// 阻止ViewPager攔截事件
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
}
// y軸方向上達到滑動最小距離, x 軸未達到
if (Math.abs(ev.getRawY() - mInitY) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) > Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 讓父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
}
// x軸方向達到了最小滑動距離,y軸未達到
if (Math.abs(mInitX - ev.getRawX() - getScrollX()) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) <= Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 阻止父級容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
}
/** 如果手指移動距離超過最小距離 */
float translationX = mInitX - ev.getRawX();
// 如果滑動距離已經(jīng)大于右邊可伸縮的距離后, 應該重新設置initx
if (translationX > mRightCanSlide) {
mInitX = ev.getRawX() + mRightCanSlide;
}
// 如果互動距離小于0,那么重新設置初始位置initx
if (translationX < 0) {
mInitX = ev.getRawX();
}
translationX = translationX > mRightCanSlide ? mRightCanSlide : translationX;
translationX = translationX < 0 ? 0 : translationX;
// 向左滑動
if (translationX <= mRightCanSlide && translationX >= 0) {
scrollTo((int) translationX, 0);
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mRecyclerView != null) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = true;
}
upAnim();
return true;
default:
break;
}
return true;
}
/**
* 清除動畫
*/
private void clearAnim() {
if (mValueAnimator == null) {
return;
}
mValueAnimator.end();
mValueAnimator.cancel();
mValueAnimator = null;
}
/**
* 手指抬起執(zhí)行動畫
*/
private void upAnim() {
int scrollX = getScrollX();
if (scrollX == mRightCanSlide || scrollX == 0) {
if (mStatusChangeLister != null) {
mStatusChangeLister.onStatusChange(scrollX == mRightCanSlide);
}
return;
}
clearAnim();
// 如果顯出一半松開手指,那么自動完全顯示。否則完全隱藏
if (scrollX >= mRightCanSlide / 2) {
mValueAnimator = ValueAnimator.ofInt(scrollX, mRightCanSlide);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
scrollTo(value, 0);
}
});
mValueAnimator.setDuration(mAnimDuring);
mValueAnimator.start();
if (mStatusChangeLister != null) {
mStatusChangeLister.onStatusChange(true);
}
}
else {
mValueAnimator = ValueAnimator.ofInt(scrollX, 0);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
scrollTo(value, 0);
}
});
mValueAnimator.setDuration(mAnimDuring);
mValueAnimator.start();
if (mStatusChangeLister != null) {
mStatusChangeLister.onStatusChange(false);
}
}
}
/**
* 重置
*/
public void resetDelStatus() {
int scrollX = getScrollX();
if (scrollX == 0) {
return;
}
clearAnim();
mValueAnimator = ValueAnimator.ofInt(scrollX, 0);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
scrollTo(value, 0);
}
});
mValueAnimator.setDuration(mAnimDuring);
mValueAnimator.start();
}
/**
* 刪除按鈕狀態(tài)變化監(jiān)聽
*/
public interface OnDelViewStatusChangeLister {
/**
* 狀態(tài)變化監(jiān)聽
* @param show 是否正在顯示
*/
void onStatusChange(boolean show);
}
}
完整DEMO直通車
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Android判斷現(xiàn)在所處界面是否為home主桌面的方法
這篇文章主要介紹了Android判斷現(xiàn)在所處界面是否為home主桌面的方法,涉及Android界面判斷的相關技巧,需要的朋友可以參考下2015-05-05

