Android滑動(dòng)沖突處理


物語(yǔ).jpeg

導(dǎo)言

Android中的滑動(dòng)沖突很常見,例如ScrollView/ListView,ViewPager/ViewPager,相信各位或多或少都了解Android事件分發(fā)機(jī)制,以及滑動(dòng)沖突產(chǎn)生的原理。網(wǎng)上相關(guān)的文章也很多,并且都講解的很詳細(xì)。但那畢竟是別人的成果,我覺得有必要通過(guò)一篇文章來(lái)記錄自己的理解。

大綱

我將從下面幾個(gè)方面來(lái)理解事件分發(fā)和解決滑動(dòng)沖突:

  1. 理解四個(gè)方法
  2. Android事件分發(fā)機(jī)制
  3. 解決滑動(dòng)沖的思路
  4. 一個(gè)滑動(dòng)沖突場(chǎng)景
  5. 總結(jié)
  6. 參考文章

1.理解四個(gè)方法

講到Android事件分發(fā)機(jī)制和解決滑動(dòng)沖突,就離不開這四個(gè)方法:

  • dispatchTouchEvent(MotionEvent ev)
  • onInterceptTouchEvent(MotionEvent ev)
  • onTouchEvent(MotionEvent ev)
  • requestDisallowInterceptTouchEvent(boolean disallowIntercept)

大概介紹一下前三個(gè)方法的關(guān)系:

/**
 * 偽代碼
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onInterceptTouchEvent(ev)) {
        return onTouchEvent(ev);
    }
    return child.dispatchTouchEvent(ev);;
}

dispatchTouchEvent()

正如其方法名,該方法是用來(lái)傳遞事件的,傳遞的順序是Activity -> ViewGroup -> View
只要事件傳遞到當(dāng)前view,就一定會(huì)調(diào)用該方法,返回結(jié)果表示是否消費(fèi)該事件:

  • true : 消費(fèi)該事件,不再繼續(xù)傳遞
  • false : 事件不再向下傳遞,并且把事件交給parent處理
  • super : 事件繼續(xù) 向下傳遞

onInterceptTouchEvent()

是否攔截事件(不再向下傳遞),該方法只存在于ViewGroup,一旦攔截,那么當(dāng)前整個(gè)事件序列不會(huì)再調(diào)用該方法,后續(xù)事件都交給當(dāng)前ViewGroup處理。返回結(jié)果表示是否攔截:

  • true : 攔截,事件不再向下傳遞
  • false/super : 不攔截,事件繼續(xù)傳遞

onTouchEvent()

該方法用來(lái)處理事件,處理的順序是View -> ViewGroup -> Activity,返回結(jié)果表示是否處理事件:

  • true : 處理事件,不再向下傳遞
  • false : 不處理事件,同一個(gè)事件序列里面,該View無(wú)法收到后續(xù)事件
  • super : 交給上層View處理

requestDisallowInterceptTouchEvent()

該方法是在子view中請(qǐng)求父view不要攔截事件,參數(shù)disallowIntercept的值表示:

  • true : 請(qǐng)求所有父view不要攔截事件,即當(dāng)前事件序列不走父view的onInterceptTouchEvent()方法,直接向下傳遞
  • false : 請(qǐng)求所有父View攔截事件,即子view不需要處理該事件,直接交給父view處理

該方法的作用下面還會(huì)詳細(xì)介紹。

2.Android事件分發(fā)機(jī)制

在介紹事件分發(fā)機(jī)制之前,先介紹一下事件序列(上文有提到過(guò)):


事件序列.png

注:一般情況下,事件列都是以DOWN事件開始,UP事件結(jié)束,中間有很多MOVE事件

接下來(lái),用一張圖看明白事件傳遞的過(guò)程中的方法調(diào)用:


事件分發(fā)圖.png

假如事件傳遞不中斷的話,方法調(diào)用的整個(gè)流程如下圖:


事件方法調(diào)用順序.png

如果仔細(xì)看上面兩張圖,大家基本就能明白事件的傳遞流程了,下面我用文字描述一下整個(gè)流程:

  1. 事件從Activity的dispatchTouchEvent開始傳遞,傳遞給ViewGroup
  2. 如果ViewGroup的onInterceptTouchEvent不攔截事件,則繼續(xù)向下面(ViewGroup或者View)傳遞,如果攔截了,則事件直接交給ViewGroup的onTouchEvent處理
  3. 當(dāng)事件傳遞到了View,View就會(huì)調(diào)用onTouchEvent處理事件,正常情況下,還會(huì)把事件交給ViewGroup的onTouchEvent處理
  4. ViewGroup處理事件之后,正常情況下,又會(huì)交給Activity的onTouchEvent處理。

這里再把幾個(gè)需要注意的點(diǎn)提一下:

  • 如果ViewGroup的onInterceptTouchEvent方法攔截了事件,事件序列的后續(xù)事件不會(huì)再調(diào)用次方法,也不會(huì)向下傳遞,都直接交給該ViewGroup處理
  • 如果View沒(méi)有對(duì)ACTION_DOWN事件進(jìn)行消費(fèi),事件序列的后續(xù)事件都不會(huì)傳遞過(guò)來(lái)了

3.滑動(dòng)沖突解決方案

面對(duì)滑動(dòng)沖突,我們可以有2種解決思路:

  1. 外部攔截法:是指我們可以重寫parent的onInterceptTouchEvent方法,判斷當(dāng)前的事件是否需要攔截,偽代碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean isIntercept = false;
    switch(event.getAction) {
        case MotionEvent.ACTION_DOWN:
            isIntercept = false;
            //todo 記錄點(diǎn)擊初始位置
            break;
        case MotionEvent.ACTION_MOVE:
            if (子控件不需要處理滑動(dòng)事件) {
                isIntercept = true;
            } else {
                isIntercept = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            isIntercept = false;
            break;
    }
    super.onInterceptTouchEvent(event);
    return isIntercept;
}

在這里,down事件必須返回false,不然事件無(wú)法傳遞到子view,后續(xù)事件序列都會(huì)交給parent處理。而up事件也需要返回false,因?yàn)閡p事件對(duì)parent來(lái)說(shuō)沒(méi)有什么意義,其次若子view處理事件,卻沒(méi)有收到up事件會(huì)讓子view的onClick事件無(wú)法觸發(fā)。

  1. 內(nèi)部攔截法:是指我們可以重寫child的dispatchTouchEvent方法,判斷是否需要讓parent攔截事件,偽代碼如下:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch(event.getAction) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptToucehEvent(true);
            //todo 記錄點(diǎn)擊初始位置
            break;
        case MotionEvent.ACTION_MOVE:
            if (子控件需要處理滑動(dòng)事件) {
                getParent().requestDisallowInterceptToucehEvent(true);
            } else {
                getParent().requestDisallowInterceptToucehEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.onDispatchTouchEvent(event);
}

如果parent.requestDisallowInterceptTouchEvent(true)傳入?yún)?shù)為true,則parent就不會(huì)執(zhí)行onInterceptTouchEvent方法,直接把事件交給子view處理。
requestDisallowInterceptTouchEvent()方法的邏輯:parent的dispatchTouchEvent()每次down事件,都會(huì)把它置為false,即攔截事件,走parent的onInterceptTouchEvent()方法,而在onInterceptTouchEvent()方法中,有一個(gè)屬性mIsBeingDragged,當(dāng)dy(滾動(dòng)距離)>mTouchSlop的時(shí)候置為true,down事件和dy<mTouchSlop時(shí)為false,最后onInterceptTouchEvent()方法返回mIsBeingDragged,說(shuō)明即使parent攔截了事件,但滾動(dòng)距離比較小的時(shí)候,事件仍可以傳遞給子view,子view可以在onTouchEvent方法中調(diào)用parent.requestDisallowInterceptTouchEvent()方法

一個(gè)滑動(dòng)沖突的場(chǎng)景

這里舉一個(gè)很簡(jiǎn)單的例子
場(chǎng)景:ViewPager嵌套ViewPager的滑動(dòng)沖突
解決思路:內(nèi)部攔截法,當(dāng)子ViewPager的position處于0且dx>0,或者子ViewPager的position處于adapter.count-1且dx<0時(shí),把事件交給父ViewPager,其他時(shí)候都是子ViewPager處理即可
代碼實(shí)現(xiàn):

package study.self.zf.scrollconflict.widget;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class ChildViewPager extends ViewPager {
    private int mStartX;
    private int mStartY;

    public ChildViewPager(Context context) {
        super(context);
    }

    public ChildViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mStartX = (int) ev.getX();
                mStartY = (int) ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = (int) getX() - mStartX;
                int dy = (int) getY() - mStartY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    int position = getCurrentItem();
                    int allCount = getAdapter().getCount();
                    boolean isInterceptByParent = (position == 0 && dx > 0) || ((position == allCount -1) && dx < 0);
                    getParent().requestDisallowInterceptTouchEvent(!isInterceptByParent);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

總結(jié)

從以上的講解可以看出來(lái),滑動(dòng)沖突并不難,而且思路也很簡(jiǎn)單,無(wú)非就是從dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()、requestDisallowInterceptTouchEvent()方法入手,分析什么時(shí)候parent處理事件,什么時(shí)候子view處理事件即可。

參考文章:

http://www.itdecent.cn/p/ff3b55441444
http://www.itdecent.cn/p/7e92121814ed

寫于2018.05.21下午18:00(位置:深圳南山)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容