在項目中,如果要用到滑動控件嵌套滑動控件,總會讓人很心塞。因爲很可能會出現衝突的問題。這裏舉個例子,利用事件分發機制,處理側滑菜單控件和列表中的側滑刪除控件間的衝突。
分析
提到側滑刪除,一個經典的例子就是QQ了。QQ的首頁是一個大的側滑菜單控件,嵌套一個列表,列表裏面再嵌套側滑刪除的控件。我們就仿照這個樣式,看看能不能做一個和它類似的效果。
這裏關注的重點是在滑動手勢的處理上,簡單分析一下需要做什麼處理:
(下面把側滑菜單控件稱作菜單控件,列表側滑刪除控件稱作刪除控件。)
在首頁上下滑動時,滾動列表。
菜單控件關閉的情況下,如果列表裏面沒有展開的刪除項,則手指向右滑動是滑動菜單控件,向左滑動是滑動刪除控件。
如果列表裏面有展開的刪除控件,則菜單控件和列表項都不可滑動。除了刪除按鍵,點擊其他區域,都是將展開項關閉。
當手指滑動刪除控件時,手指滑動到屏幕的任意區域都可以滑動展開項。
菜單控件打開的情況下,點擊右邊主頁區域,將菜單控件關閉。
有點複雜的感覺啊,我們一個個來解決。
我自定義了上面說到的三個控件,根據嵌套關係,從大到小分別是:
- 菜單控件SwipeMenuLayout
- 列表控件MyRecyclerView
- 刪除控件SwipeDeleteLayout
其中,SwipeMenuLayout和SwipeDeleteLayout都是繼承自FrameLayout,用ViewDragHelper實現滑動效果。MyRecyclerView則繼承自RecyclerView。
我們知道事件分發和三個方法有關:
- 負責分發的dispatchTouchEvent
- 負責攔截的onInterceptTouchEvent
- 負責消費的onTouchEvent
簡單概括一下這個機制就是:分發從父到子,消費從子到父。
一般我們不對分發做特殊處理,下面按執行順序看看三個控件的onInterceptTouchEvent和onTouchEvent方法是怎麼寫的。
onInterceptTouchEvent
onInterceptTouchEvent方法的返回值決定是否攔截事件。
菜單控件
這部分要稍微囉嗦一點。我們先看看菜單關閉的情況,這時如果手指向右滑且沒有展開的刪除控件,我們就可以把事件攔截了,所以onInterceptTouchEvent可以寫成這樣:
if (mState == State.CLOSE) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
mDownX = ev.getRawX();
mDownY = ev.getRawY();
}
break;
case MotionEvent.ACTION_MOVE: {
float deltaX = ev.getRawX() - mDownX;
float deltaY = ev.getRawY() - mDownY;
//向右滑動且列表沒有展開項且橫向滑動距離比豎向滑動距離大,則攔截
if (deltaX > 0 &&
MainAdapter.mOpenItems.size() == 0 &&
Math.abs(deltaY / deltaX) < 1) {
return true;
}
}
break;
}
}
mState代表當前側滑控件的狀態,MainAdapter.mOpenItems保存的是當前打開的刪除控件。我使用Math.abs(deltaY / deltaX)是否小於1來判斷手指的滑動方向。
這裏還有兩種不攔截的情況,向左滑動或者有展開項的話,都是和側滑菜單沒關係的,滑動事件裏面再加入以下代碼:
//如果是向左滑,且豎直滑動距離大於橫向滑動距離,不攔截
//MainPage打開的item個數大於0,不攔截
if ((deltaX < 0 && Math.abs(deltaY / deltaX) > 1) ||
MainAdapter.mOpenItems.size() > 0) {
return false;
}
接下來是菜單打開的情況。這時候當手指點擊了右側的主頁面區域是需要攔截並且將菜單關閉。如果手指向右滑動則不需要攔截:
if (mState == State.OPEN) {
//完全展開時並且點到主頁面,攔截並關閉菜單
if (mMainContent.getLeft() <= mRange && ev.getRawX() > mRange) {
return true;
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
//如果是向右滑,不攔截
float deltaX = ev.getRawX() - mDownX;
if (deltaX > 0) {
return false;
}
break;
}
}
mRange是側滑出來的菜單寬度,關閉菜單的操作可以放在ViewDragHelper的Callback房裏裏處理。
除了上面這些情況,默認情況下是否攔截交給ViewDragHelper處理就好了,調用它的shouldInterceptTouchEvent方法。
完整代碼如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mState == State.CLOSE) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
mDownX = ev.getRawX();
mDownY = ev.getRawY();
}
break;
case MotionEvent.ACTION_MOVE: {
float deltaX = ev.getRawX() - mDownX;
float deltaY = ev.getRawY() - mDownY;
//向右滑動且列表沒有展開項且橫向滑動距離比豎向滑動距離大,則攔截
if (deltaX > 0 &&
MainAdapter.mOpenItems.size() == 0 &&
Math.abs(deltaY / deltaX) < 1) {
return true;
}
//如果是向左滑,且豎直滑動距離大於橫向滑動距離,不攔截
//MainPage打開的item個數大於0,不攔截
if ((deltaX < 0 && Math.abs(deltaY / deltaX) > 1) ||
MainAdapter.mOpenItems.size() > 0) {
return false;
}
}
break;
}
} else if (mState == State.OPEN) {
//完全展開時並且點到主頁面,攔截並關閉菜單
if (mMainContent.getLeft() <= mRange && ev.getRawX() > mRange) {
return true;
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
//如果是向右滑,不攔截
float deltaX = ev.getRawX() - mDownX;
if (deltaX > 0) {
return false;
}
break;
}
}
return mDragHelper.shouldInterceptTouchEvent(ev);
}
列表控件
列表裏面其實只做了一個處理,就是判斷上下滑動的時候就把事件攔截了:
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = e.getRawX();
mDownY = e.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//豎向滑動時攔截事件
float deltaX = e.getRawX() - mDownX;
float deltaY = e.getRawY() - mDownY;
if (deltaY != 0.0 &&
Math.abs(deltaX / deltaY) < 1) {
return true;
}
break;
}
return super.onInterceptTouchEvent(e);
}
刪除控件
這裏什麼都不用做,交給ViewDragHelper就好了:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragHelper.shouldInterceptTouchEvent(ev);
}
onTouchEvent
onTouchEvent方法的返回值決定是否消費事件。
刪除控件
刪除控件的onTouchEvent又有幾個地方要做特殊處理的。當有展開的刪除項時,點擊別的刪除項時就將展開的關閉。這樣就可以了:
//存在已展開的控件且當前控件爲關閉狀態,則將所有展開控件關閉
if (MainAdapter.mOpenItems.size() > 0 && mState == State.CLOSE) {
return false;
}
這裏我沒有消費事件,也沒有進行關閉的操作,因爲我把關閉的操作交給父控件去處理了,否則會有卡頓的現象(QQ就有這個問題)。
如果點擊的是展開的刪除項左邊區域,這個又比較特殊了。因爲手指按下之後,有可能是滑動,也可能是點擊。滑動的話是滑動刪除項,點擊則是將刪除項關閉。所以我們要判斷一下用戶是否有滑動的操作:
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
float deltaX = event.getRawX() - mDownX;
if (Math.abs(deltaX) > 50) {
isDrag = true;
}
break;
case MotionEvent.ACTION_UP:
if (!isDrag &&
event.getRawX() <= mWidth - mBackWidth) {
close();
return true;
}
isDrag = false;
break;
}
當活動距離大於50時,我就把它當做是一個滑動操作,這時候把滑動交給ViewDragHelper處理,否則就將當前控件關閉。
最後還有一個,當我滑動刪除控件時,如果手指滑到了別的地方,滑動的依然是當前這個刪除控件。換一個說法,其實就是一旦滑動了,父控件就不能再攔截我的滑動事件了。其實ViewGroup裏面有一個requestDisallowInterceptTouchEvent方法,傳true的時候,相當於通知它的所有父控件不要再攔截了。所以可以這樣來處理:
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_CANCEL:
requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_UP:
requestDisallowInterceptTouchEvent(false);
break;
}
完整代碼如下:
public boolean onTouchEvent(MotionEvent event) {
//存在已展開的控件且當前控件爲關閉狀態,則將所有展開控件關閉
if (MainAdapter.mOpenItems.size() > 0 && mState == State.CLOSE) {
MainAdapter.closeAll();
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
requestDisallowInterceptTouchEvent(true);
float deltaX = event.getRawX() - mDownX;
if (Math.abs(deltaX) > 50) {
isDrag = true;
}
break;
case MotionEvent.ACTION_CANCEL:
requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_UP:
requestDisallowInterceptTouchEvent(false);
if (!isDrag &&
event.getRawX() <= mWidth - mBackWidth) {
//展開狀態下,點擊左側部分將其關閉
close();
return true;
}
isDrag = false;
break;
}
mDragHelper.processTouchEvent(event);
return true;
}
列表控件
當有展開刪除項且點擊了別的刪除項的時候,把關閉的操作繼續往父控件拋就好了:
public boolean onTouchEvent(MotionEvent e) {
return MainAdapter.mOpenItems.size() == 0 && super.onTouchEvent(e);
}
菜單控件
在這裏處理一下上面說的那種情況:
public boolean onTouchEvent(MotionEvent event) {
if (MainAdapter.mOpenItems.size() > 0) {
MainAdapter.closeAll();
return true;
}
mDragHelper.processTouchEvent(event);
return true;
}
效果
扯了這麼多,看下效果吧:
搞半天其實也就這樣而已。
小結
這篇有點囉嗦啊,裏面涉及到的細節比較多。最後可能還會存在一些問題,這裏主要是提供利用事件分發機制,處理手勢衝突的思路。
寫這個的時候發現QQ也有一些小問題,比如QQ在刪除控件展開的情況下,按住刪除控件左邊區域下滑後,再左右滑,會出現列表跳動的問題。
大家可以點下面去看源碼。就到這吧,妥妥的。