背景:在android開發(fā)中,列表是經(jīng)常會(huì)使用到的一個(gè)主要控件涩禀,列表中可以展示大量的數(shù)據(jù)料滥,像訂單、商品艾船、通訊錄葵腹、瀏覽記錄或者關(guān)注列表等等∮炱瘢可能產(chǎn)品一開始需求只做簡單的數(shù)據(jù)展示践宴,但后期隨著功能越來越多,越來越完善爷怀,產(chǎn)品可能說在列表里面增加一些交互能力阻肩。比如說訂單列表里面,一開始只是展示訂單數(shù)據(jù)运授,后面需要加上刪除訂單的功能烤惊,以前Android中這種功能要的很多的可能就是長按操作這種的乔煞,因?yàn)槌绦蛟持恍枰苌俚拇a就能實(shí)現(xiàn)。但是ios的習(xí)慣操作是左滑刪除撕氧,為了保持統(tǒng)一的操作習(xí)慣瘤缩,兩端保持一致,最終產(chǎn)品會(huì)讓Android程序猿去實(shí)現(xiàn)一種和ios一模一樣的功能伦泥。如果你的代碼已經(jīng)維護(hù)了很久剥啤,代碼量比較大,不愿意去大改不脯,那么今天這個(gè)控件就能輕松的助你完成左滑刪除的功能府怯。
先上效果圖:
設(shè)計(jì)思路:最好以最小的代碼侵入來實(shí)現(xiàn)左滑刪除的功能,在不破壞原來邏輯的基礎(chǔ)上防楷,只需稍加改造便可具備左滑刪除的能力牺丙。
首先分析下左滑刪除的基礎(chǔ)原理:
原理分析:
- 正常狀態(tài)下,我們看到的是完整的內(nèi)容部分复局,右側(cè)菜單部分因?yàn)槌銎聊凰圆辉谝暰€范圍內(nèi)冲簿。
- 手指滑動(dòng)過程中,容器的內(nèi)容跟隨手指移動(dòng)亿昏,從而拉出在屏幕外面的菜單區(qū)域峦剔。
- 當(dāng)手指松開的時(shí)候,我們先假定一種邏輯角钩,如果菜單區(qū)域顯示超過一半吝沫,那就全部顯示;如果少于一半那就滑出隱藏递礼。
滑動(dòng)原理分析完了之后惨险,我們大概就有了實(shí)現(xiàn)思路了:
- 首先我們的控件里面需要兩塊區(qū)域,因?yàn)橐郧翱赡芤呀?jīng)實(shí)現(xiàn)了列表item的顯示脊髓,如果能不做任何改動(dòng)辫愉,直接把以前的item包含到我們的內(nèi)容區(qū)域里面來,那么我們內(nèi)容區(qū)域就輕松搞定了将硝。
- 菜單區(qū)域恭朗,需要什么能力,就把相關(guān)的View也傳遞給我容器袋哼,然后容器放到相應(yīng)位置冀墨。
談笑間闸衫,簡單兩步我們的左滑刪除容器已經(jīng)完成一個(gè)簡單的雛形了涛贯!
接下來就是代碼實(shí)現(xiàn):
步驟一:內(nèi)容和菜單分別加入容器
/**
* 設(shè)置內(nèi)容區(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);
}
/**
* 設(shè)置右邊菜單區(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) {
// 讓父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
// 阻止ViewPager攔截事件
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
}
return false;
}
// y軸方向上達(dá)到滑動(dòng)最小距離, x 軸未達(dá)到
if (Math.abs(ev.getRawY() - mInitY) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) > Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 讓父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
return false;
}
// x軸方向達(dá)到了最小滑動(dòng)距離,y軸未達(dá)到
if (Math.abs(mInitX - ev.getRawX() - getScrollX()) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) <= Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 阻止父級(jí)容器攔截
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);
}
/**
* 處理觸摸事件
* 需要注意何時(shí)處理左滑蔚出,何時(shí)不處理
*
* @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) {
// 讓父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
// 阻止ViewPager攔截事件
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
}
// y軸方向上達(dá)到滑動(dòng)最小距離, x 軸未達(dá)到
if (Math.abs(ev.getRawY() - mInitY) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) > Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 讓父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
}
// x軸方向達(dá)到了最小滑動(dòng)距離弟翘,y軸未達(dá)到
if (Math.abs(mInitX - ev.getRawX() - getScrollX()) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) <= Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 阻止父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
}
/** 如果手指移動(dòng)距離超過最小距離 */
float translationX = mInitX - ev.getRawX();
// 如果滑動(dòng)距離已經(jīng)大于右邊可伸縮的距離后, 應(yīng)該重新設(shè)置initx
if (translationX > mRightCanSlide) {
mInitX = ev.getRawX() + mRightCanSlide;
}
// 如果互動(dòng)距離小于0虫腋,那么重新設(shè)置初始位置initx
if (translationX < 0) {
mInitX = ev.getRawX();
}
translationX = translationX > mRightCanSlide ? mRightCanSlide : translationX;
translationX = translationX < 0 ? 0 : translationX;
// 向左滑動(dòng)
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;
}
以上兩個(gè)方法主要處理了左滑移動(dòng)功能以及滑動(dòng)沖突問題,如果用的是RecyclerView那么為了防止垂直方向的同向沖突稀余,那么需要將外層的RecyclerView傳入左滑容器悦冀,在這個(gè)容器中會(huì)處理滑動(dòng)沖突。
到這就已經(jīng)實(shí)現(xiàn)了左滑功能睛琳,并且解決掉了垂直方向上的滑動(dòng)沖突盒蟆,然后我們還要實(shí)現(xiàn)一個(gè)功能是:如果有一個(gè)item向左滑動(dòng)并顯示出右邊的菜單區(qū)域,當(dāng)手指再次按下或者列表滑動(dòng)的時(shí)候师骗,需要將已經(jīng)顯示菜單區(qū)域的item收起历等,恢復(fù)原來的狀態(tài)。為了提供這個(gè)能力辟癌,左滑容器里面提供一個(gè)菜單狀態(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();
}
菜單展開或者收起都會(huì)調(diào)用這個(gè)方法寒屯,方便第三方調(diào)用者處理狀態(tài)。
再者還有就是加上動(dòng)畫黍少,讓滑動(dòng)更加柔和:
/**
* 手指抬起執(zhí)行動(dòng)畫
*/
private void upAnim() {
int scrollX = getScrollX();
if (scrollX == mRightCanSlide || scrollX == 0) {
if (mStatusChangeLister != null) {
mStatusChangeLister.onStatusChange(scrollX == mRightCanSlide);
}
return;
}
clearAnim();
// 如果顯出一半松開手指寡夹,那么自動(dòng)完全顯示。否則完全隱藏
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;
/**
* 右邊可滑動(dòng)距離
*/
private int mRightCanSlide;
/**
* 按下x
*/
private float mInitX;
/**
* 按下y
*/
private float mInitY;
/**
* 屬性動(dòng)畫
*/
private ValueAnimator mValueAnimator;
/**
* 動(dòng)畫時(shí)長
*/
private int mAnimDuring = 200;
/**
* 刪除按鈕的長度
*/
private int mDelLength = 76;
/**
* ViewPager
*/
private ViewPager mViewPager;
/**
* RecyclerView
*/
private RecyclerView mRecyclerView;
/** CardView */
private CardView mCardView;
/** 是否重新計(jì)算 */
private boolean isReCompute = true;
/** 狀態(tài)監(jiān)聽 */
private OnDelViewStatusChangeLister mStatusChangeLister;
/**
* 內(nèi)容區(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();
}
/**
* 設(shè)置內(nèi)容區(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);
}
/**
* 設(shè)置右邊菜單區(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);
}
/**
* 設(shè)置Viewpager
*/
public void setViewPager(ViewPager viewPager) {
mViewPager = viewPager;
}
/**
* 設(shè)置RecyclerView
*/
public void setRecyclerView(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
}
/** 設(shè)置CardView */
public void setCardView(CardView cardView) {
mCardView = cardView;
}
/** 設(shè)置狀態(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) {
// 讓父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
// 阻止ViewPager攔截事件
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
}
return false;
}
// y軸方向上達(dá)到滑動(dòng)最小距離, x 軸未達(dá)到
if (Math.abs(ev.getRawY() - mInitY) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) > Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 讓父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
return false;
}
// x軸方向達(dá)到了最小滑動(dòng)距離厂置,y軸未達(dá)到
if (Math.abs(mInitX - ev.getRawX() - getScrollX()) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) <= Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 阻止父級(jí)容器攔截
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);
}
/**
* 處理觸摸事件
* 需要注意何時(shí)處理左滑菩掏,何時(shí)不處理
*
* @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) {
// 讓父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
// 阻止ViewPager攔截事件
if (mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
}
// y軸方向上達(dá)到滑動(dòng)最小距離, x 軸未達(dá)到
if (Math.abs(ev.getRawY() - mInitY) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) > Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 讓父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(false);
isReCompute = false;
}
}
// x軸方向達(dá)到了最小滑動(dòng)距離,y軸未達(dá)到
if (Math.abs(mInitX - ev.getRawX() - getScrollX()) >= mTouchSlop
&& Math.abs(ev.getRawY() - mInitY) <= Math.abs(mInitX - ev.getRawX() - getScrollX())) {
// 阻止父級(jí)容器攔截
if (mRecyclerView != null && isReCompute) {
mRecyclerView.requestDisallowInterceptTouchEvent(true);
isReCompute = false;
}
}
/** 如果手指移動(dòng)距離超過最小距離 */
float translationX = mInitX - ev.getRawX();
// 如果滑動(dòng)距離已經(jīng)大于右邊可伸縮的距離后, 應(yīng)該重新設(shè)置initx
if (translationX > mRightCanSlide) {
mInitX = ev.getRawX() + mRightCanSlide;
}
// 如果互動(dòng)距離小于0农渊,那么重新設(shè)置初始位置initx
if (translationX < 0) {
mInitX = ev.getRawX();
}
translationX = translationX > mRightCanSlide ? mRightCanSlide : translationX;
translationX = translationX < 0 ? 0 : translationX;
// 向左滑動(dòng)
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;
}
/**
* 清除動(dòng)畫
*/
private void clearAnim() {
if (mValueAnimator == null) {
return;
}
mValueAnimator.end();
mValueAnimator.cancel();
mValueAnimator = null;
}
/**
* 手指抬起執(zhí)行動(dòng)畫
*/
private void upAnim() {
int scrollX = getScrollX();
if (scrollX == mRightCanSlide || scrollX == 0) {
if (mStatusChangeLister != null) {
mStatusChangeLister.onStatusChange(scrollX == mRightCanSlide);
}
return;
}
clearAnim();
// 如果顯出一半松開手指患蹂,那么自動(dòng)完全顯示。否則完全隱藏
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);
}
}