SwipeMenuLayout
View for listView item
一 介紹
ListView 通常用來(lái)展示多個(gè)個(gè)體复亏,比如QQ 微信中的聯(lián)系人列表。一個(gè)比較常見(jiàn)的功能是側(cè)滑刪除臊泰。這個(gè)功能屬于比較常見(jiàn)的一個(gè)菜單,網(wǎng)絡(luò)上也有很多實(shí)現(xiàn)蚜枢。
1 scroller 方式缸逃。
最常見(jiàn)的一個(gè)實(shí)現(xiàn)是ListView 的Item View 為一個(gè)LinerLayout, 菜單在LinerLayout的最右端超出屏幕的位置针饥,當(dāng)手指滑動(dòng)的時(shí)候,通過(guò)scrollTo 的方法在ListView 中控制Item View 的滑動(dòng)需频,使菜單滑動(dòng)出來(lái)丁眼。但是在IOS 上菜單是隱藏在Item View 的下面,層疊式的昭殉,當(dāng)滑動(dòng)的時(shí)候不是拉出來(lái)的方式苞七,而是顯示出來(lái)。
2.NineOldAndroids
在屬性動(dòng)畫(huà)沒(méi)有加入android的遠(yuǎn)古時(shí)代挪丢,github 上有一個(gè)NineOldAndroids項(xiàng)目蹂风,有人通過(guò)這個(gè)實(shí)現(xiàn)一個(gè)和IOS接近,其原理是FrameLayout乾蓬, context 為顯示的內(nèi)容惠啄,menu嵌套在context下面,屬性動(dòng)畫(huà)的方式移動(dòng)context任内。
3.SwipeMenuListView
在github 上有一個(gè) https://github.com/baoyongzhang/SwipeMenuListView 實(shí)現(xiàn)效果和1 類似撵渡,但是View 移動(dòng)采用layout 方式,我修改了下 https://github.com/louiewh/SwipeMenuListView 實(shí)現(xiàn)效果和IOS 一樣死嗦。但是總覺(jué)這幾種方式都不太完美趋距,要么效果打了折扣,要么代碼量太大越走,方式復(fù)雜棚品,通常需要重寫(xiě)ListView 和Adapter。
二 原理
下面一個(gè)SwipeMenuLayout 大概不到300行的代碼廊敌,完美實(shí)現(xiàn)ListView 的側(cè)滑菜單铜跑。原理是繼承FrameLayout, 作為L(zhǎng)istView 的Item View 骡澈。在View中層疊兩層锅纺,上層context View 為要顯示的內(nèi)容,下面menu View 為菜單肋殴。重寫(xiě)SwipeMenuLayout的OnTouchEvent ,在OnTouchEvent 中控制context 的移動(dòng)囤锉。如圖:

image
1. init View.
定義ListView 的Item View,如果使用左菜單:
- 左菜單的ID為:swipe_left_menu
- 右菜單ID:swipe_right_menu
- context 菜單ID:swipe_context
這個(gè)是默認(rèn)ID护锤,這樣在SwipeMenuLayout會(huì)自動(dòng)找到菜單View ID :
public SwipeMenuLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initAttrs(attrs);
initUI();
}
private void initUI() {
mScroller = ScrollerCompat.create(getContext());
ViewConfiguration config = ViewConfiguration.get(getContext());
mTouchSlop = config.getScaledTouchSlop();
mLeftMenuViewId = getContext().getResources().getIdentifier(LEFTMENUVIEW, "id", getContext().getPackageName());
mRightMenuViewId = getContext().getResources().getIdentifier(RIGHTMENUVIEW, "id", getContext().getPackageName());
mContextViewId = getContext().getResources().getIdentifier(CONTEXTVIEW, "id", getContext().getPackageName());
if (mLeftMenuViewId == 0 || mRightMenuViewId == 0 || mContextViewId == 0) {
throw new RuntimeException(String.format("initUI Exception" ));
}
}
重寫(xiě) onMeasure方法官地, 在onMeasure 方法中調(diào)用findViewByID, 為什么在onMeasure 而不是在構(gòu)造函數(shù)中烙懦,因?yàn)樵跇?gòu)造函數(shù)中View 的子View還沒(méi)有初始化驱入,findViewByID 為空。需要強(qiáng)調(diào)的是菜單View 在init 的時(shí)候設(shè)置為不可以見(jiàn),這樣在開(kāi)始滑動(dòng)的時(shí)候亏较,菜單View不會(huì)干擾SwipeMenuLayout的滑動(dòng)事件
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
initView();
menuViewHide();
}
public void initView() {
if(mLeftMenuView == null && mLeftMenuViewId != View.NO_ID) {
mLeftMenuView = this.findViewById(mLeftMenuViewId);
}
if(mRightMenuView == null && mRightMenuViewId != View.NO_ID) {
mRightMenuView = this.findViewById(mRightMenuViewId);
}
if(mContextView == null && mContextViewId != View.NO_ID)
mContextView = this.findViewById(mContextViewId);
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mOnMenuClickListener != null) {
mOnMenuClickListener.onItemClick(v, mPosition);
}
}
});
}
2 處理滑動(dòng)事件
重寫(xiě)onTouchEvent 在down 事件中return true莺褒,攔截事件分發(fā),在move 事件中判斷滑動(dòng)距離是否大于閥值雪情,大于閥值遵岩,設(shè)置菜單View 可見(jiàn),移動(dòng)context View巡通,在UP 事件中處理動(dòng)畫(huà)尘执,根據(jù)滑動(dòng)的距離和方向,開(kāi)始對(duì)應(yīng)的動(dòng)畫(huà)扁达。
1. cancle 事件的處理正卧, ListView 的 OnInteruptTouchEvent 會(huì)判斷View 的Y軸滑動(dòng)距離,如果大于一定的距離跪解,攔截事件炉旷,響應(yīng)ListView 的上下滑動(dòng)。按照android 的標(biāo)準(zhǔn)處理叉讥,cancle 按照up 事件處理窘行。
2. ListView 和 SwipeMenuLayout 的事件沖突,如果SwipeMenuLayout 進(jìn)入側(cè)滑后图仓,上下滑動(dòng)距離過(guò)大罐盔,ListView 會(huì)攔截事件,所以一旦進(jìn)入側(cè)滑模式救崔,要禁止ListView 攔截事件.調(diào)用getParent().requestDisallowInterceptTouchEvent(true);
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
if(mLeftMenuView != null)
mLeftMargin = mLeftMenuView.getWidth();
if(mRightMenuView != null)
mRightMargin = mRightMenuView.getWidth();
if(mSlideView != null && this != mSlideView && mSlideView.isMenuOpen()) {
mSlideView.closeMenu();
event.setAction(MotionEvent.ACTION_CANCEL);
}
Log.d(TAG, "Event ACTION_DOWN mMenuShow:" + mMenuShow);
super.onTouchEvent(event);
return true;
case MotionEvent.ACTION_MOVE:
int dx = (int) (event.getX() - mDownX);
if(Math.abs(dx) < mTouchSlop)
break;
if(!mMenuShow ) {
menuViewShow(dx);
getParent().requestDisallowInterceptTouchEvent(true);
mSlideView = this;
}
if(dx > 0 && mLeftMenuView == null) break;
if(dx < 0 && mRightMenuView == null) break;
if(dx > 0 && dx > mLeftMargin) {
dx = mLeftMargin;
} else if (dx < 0 && dx < -mRightMargin) {
dx = -mRightMargin;
}
layoutContextView(dx);
if(mMenuShow)
event.setAction(MotionEvent.ACTION_CANCEL);
return super.onTouchEvent(event);
case MotionEvent.ACTION_CANCEL:
Log.d(TAG, "Event ACTION_CANCEL mMenuShow:" + mMenuShow);
case MotionEvent.ACTION_UP:
int dis = mContextView.getLeft();
/**
* dis > 0, move to right, dis < mLeftMargin/2, close menu, dis < mLeftMargin open menu.
* dis < 0, move to left, dis > -mRightMargin/2, close menu, dis > -mRightMargin, open menu
*/
if(dis > 0 && mLeftMenuView != null) {
if(dis < mLeftMargin/2) {
mScroller.startScroll(dis, 0, -dis, 0, mScrollTime);
} else if(dis < mLeftMargin) {
mScroller.startScroll(dis, 0, mLeftMargin-dis, 0, mScrollTime);
}
postInvalidate();
} else if(dis < 0 && mRightMenuView != null) {
if(dis > -mRightMargin/2) {
mScroller.startScroll(dis, 0, -dis, 0, mScrollTime); //close
} else if(dis > -mRightMargin) {
mScroller.startScroll(dis, 0, -mRightMargin-dis, 0, mScrollTime);
}
postInvalidate();
}
mDownX = 0;
Log.d(TAG, "Event ACTION_UP mMenuShow:" + mMenuShow);
if(mMenuShow)
event.setAction(MotionEvent.ACTION_CANCEL);
return super.onTouchEvent(event);
default:
return super.onTouchEvent(event);
}
return super.onTouchEvent(event);
}
3 contextView 的移動(dòng)和動(dòng)畫(huà)
contextView 的移動(dòng)方式采用layout 方法移動(dòng):
private void layoutContextView(int dx) {
if(mContextView != null)
mContextView.layout(dx, 0, mContextView.getMeasuredWidth()+dx, mContextView.getMeasuredHeight());
if(!mResterListener && (dx == mLeftMargin || dx == -mRightMargin)) {
Log.d(TAG, "registerListener dx:" + dx);
registerListener(dx);
} else if (mResterListener && (dx ==0 || (dx > 0 && dx != mLeftMargin ) || (dx < 0 && dx != -mRightMargin))) {
Log.d(TAG, "unregisterListener dx:" + dx);
unregisterListener(dx);
}
}
當(dāng)滑動(dòng)到一半的時(shí)候手里離開(kāi)惶看,這是菜單要關(guān)閉或者打開(kāi),動(dòng)畫(huà)采用Scroller 的方式六孵,在UP 事件中調(diào)用mScroller.startScroll纬黎, 重寫(xiě)computerScroll
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()) {
layoutContextView(mScroller.getCurrX());
postInvalidate();
}
}
4 監(jiān)聽(tīng)事件的處理
- 定義interface 和 事件注冊(cè),
- menu 菜單在init 的時(shí)候已經(jīng)設(shè)置不可見(jiàn)劫窒,在 layoutContextView 的時(shí)候本今,判斷滑動(dòng)距離,如果大于0 設(shè)置菜單menu 可見(jiàn)主巍,如果滑動(dòng)到了菜單寬度的位置冠息,菜單完全打開(kāi),注冊(cè)監(jiān)聽(tīng)事件孕索。避免menu View 和SwipeMenuLayout 滑動(dòng)事件沖突逛艰。
- 由于我們攔截了事件,ListView 的 OnItemClick 無(wú)法響應(yīng)搞旭,因此需要我們單獨(dú)寫(xiě)一個(gè)Listener散怖,處理整個(gè)SwipeMenuLayout 的點(diǎn)擊事件響應(yīng)唐断。把這個(gè)事件響應(yīng)放到整個(gè)SwipeMenuLayout,在initView() 函數(shù)中設(shè)置了OnClickListener杭抠,所以在 OnTouchEvent 中每個(gè)事件都調(diào)用了 super.OnTouchEvent()。但是這樣處理的話響應(yīng)滑動(dòng)的同時(shí)也響應(yīng)了OnClickListener恳啥, 需要處理滑動(dòng)和OnClickListener 的事件沖突偏灿,CANCAL 事件來(lái)了,一旦我們進(jìn)入了滑動(dòng)模式钝的,在MOVE 事件中發(fā)送CANCAL事件給super翁垂,OnClickListener就無(wú)法響應(yīng)。
public interface OnMenuClickListener {
void onMenuClick(View v, int position);
void onItemClick(View v, int position);
}
private void registerListener(int dis) {
if(mLeftMenuView != null && dis == mLeftMargin) {
mLeftMenuView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnMenuClickListener != null)
mOnMenuClickListener.onMenuClick(v, mPosition);
}
});
}
if(mRightMenuView != null && dis == -mRightMargin) {
mRightMenuView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnMenuClickListener != null)
mOnMenuClickListener.onMenuClick(v, mPosition);
}
});
}
mResterListener = true;
}
private void unregisterListener(int dis) {
if(mLeftMenuView != null && dis > 0) {
mLeftMenuView.setOnClickListener(null);
}
if(mRightMenuView != null && dis < 0) {
mRightMenuView.setOnClickListener(null);
}
mResterListener = false;
}
5 使用
- 定義ListView 的Item XML 注意左右菜單 和context View的ID硝桩;左右菜單根據(jù)需要定義沿猜,不需要不定義即可
- 在重寫(xiě)Adapter 的時(shí)候 getView 中設(shè)置position, 重寫(xiě)OnMenuClickListener碗脊,設(shè)置監(jiān)聽(tīng)事件:
((SwipeMenuLayout)convertView).setPosition(position);
((SwipeMenuLayout)convertView).setOnMenuClickListener(ListViewActivity.this);
三 總結(jié)
不需要重寫(xiě)ListView 和 Adapter啼肩,菜單可以用XML定義好即可,是不是很簡(jiǎn)單衙伶。
傳送門(mén):github