天天看點

Android滑動沖突解決方法Android滑動沖突解決方法滑動沖突View Touch事件分發Activity分發事件到ViewGroup解決滑動沖突的原理滑動沖突兩種解決辦法事件攔截Condition滑動沖突解決拓展Touch事件的來源深入篇

Android滑動沖突解決方法

滑動沖突

首先講解一下什麼是滑動沖突。當你需要在一個ScrollView中嵌套使用ListView或者RecyclerView的時候你會發現隻有ScrollView能夠滑動,而ListView或RecyclerView不能滑動,這個就違背了我們寫這段代碼的意願。我們想要的結果是當我們滑動ListView的時候ListView滑動,滑動ListView以外的地方的時候ScrollView滑動。這時候滑動沖突就産生了,我們需要想辦法解決這個沖突。

你可以在這裡看到這個引文的demo:https://github.com/onlynight/SlidingConfict

View Touch事件分發

首先我們了解下Android的控件組織結構。View是顯示元件的基類,ViewGroup繼承自View是布局的基類。ViewGroup中可包含View和ViewGroup,這樣就形成了View樹。View的Touch事件總是從View根節點開始向下傳遞的,根據點選的位置判斷該傳遞給哪個子View,直到子節點再沒有子節點這時候,如果這個事件被該View消耗那麼事件的傳遞就此結束,如果該View沒有使用這個事件那麼這個事件會依次向上傳遞直到有View消耗了這個事件,如果沒有View消耗這個事件,那麼該事件就會被傳遞給Activity處理。以上就是Vieww Touch事件傳遞的過程。

我們來看View的dispatchEvent方法:

//View.java
/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    return result;
}
           

從這段代碼我們可以看出OnTouchListener的優先級高于onTouchEvent。

下面我們再來看看ViewGroup的dispatchTouchEvent方法:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != ;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }

    // Check for cancelation.
    final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;

    //如果沒有攔截再分發下去處理
    if (!canceled && !intercepted) {
        ...
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            ...
        }
        ...
    }

    return handled;
}

/**
 * Transforms a motion event into the coordinate space of a particular child view,
 * filters out irrelevant pointer ids, and overrides its action if necessary.
 * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 */
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    ...
}
           

可以看到ViewGroup在處理事件前有一個touch事件是否被攔截onInterceptTouchEvent的判斷,如果被攔截則不再向下一級分發;如果沒有攔截則向下分發,處理方式會根據ViewGroup中是否包含子元素來判斷,如果包含子元素則将事件交由子元素處理touch事件

handled = child.dispatchTouchEvent(event);

,如果不包含子元素則由自身處理

handled = child.dispatchTouchEvent(event);

處理流程和View相同。

Android滑動沖突解決方法Android滑動沖突解決方法滑動沖突View Touch事件分發Activity分發事件到ViewGroup解決滑動沖突的原理滑動沖突兩種解決辦法事件攔截Condition滑動沖突解決拓展Touch事件的來源深入篇

實線箭頭為touch事件正向傳遞,虛線為向上傳遞touch事件。

通過上面的分發的邏輯我們可以知道父控件有能力把事件不傳遞給子View,進而不讓子控件接收Touch事件,那麼子控件有沒有能力讓父控件失去響應Touch事件的能力呢,下面我們來看看具體的源碼,看源碼的順序是由下而上的,這回我們反其道而行,我們知道事件的入口然後依次向下找。

Activity分發事件到ViewGroup

根據上面的圖我們知道View的touch事件是由Activity傳遞過來的,那麼我們先看看Activity有沒有類似的方法,正如我們所料,Activity的dispatchTouchEvent函數如下:

/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 *
 * @param ev The touch screen event.
 *
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
           

顯而易見我們要看的是

getWindow().superDispatchTouchEvent(ev)

,我們深入進去看到Window類中的這個方法:

//Window.java
/**
 * Used by custom windows, such as Dialog, to pass the touch screen event
 * further down the view hierarchy. Application developers should
 * not need to implement or call this.
 *
 */
public abstract boolean superDispatchTouchEvent(MotionEvent event);
           

Window類是個抽象類,它的唯一實作類是PhoneWindow,PhoneWindow類中的實作如下:

//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
           

mDecor是DecorView,我們看看這個DectorView是從哪裡來的:

//PhoneWindow.java
private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();
        ...
    }

    ...

    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
    }

    ...
}

protected DecorView generateDecor() {
    return new DecorView(getContext(), -);
}

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

protected ViewGroup generateLayout(DecorView decor) {
    ...

    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }

    ...

    return contentParent;
}

//Activity.java
/**
 * Set the activity content from a layout resource.  The resource will be
 * inflated, adding all top-level views to the activity.
 *
 * @param layoutResID Resource ID to be inflated.
 *
 * @see #setContentView(android.view.View)
 * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
 */
public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
           

這裡我們就看出了mDecorView中包含了mContentParent,并且DecorView繼承自FramLayout,是以touch事件的分發也符合View的事件分發,mDecorView之後會添加到Activity關聯的Window上(這裡我們不再深究),下面我們來看DecorView的superDispatchTouchEvent:

//PhoneWindow.java#DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}
           

至此,activity的dispatchTouchEvent方法就最終分發到了我們的布局上,最後總結一下:

Activity#dispatchTouchEvent -> PhoneWindow#superDispatchTouchEvent ->
DecorView#superDispatchTouchEvent -> ViewGroup#dispatchTouchEvent -> View#dispatchTouchEvent
           

解決滑動沖突的原理

看了上面的源碼解析,我們知道Viewtouch事件分發過程中重要的三個函數:

  • dispatchTouchEvent

    負責touch事件的分發
  • onInterceptTouchEvent

    負責攔截touch事件
  • onTouchEvent

    最終處理touch事件

其中dispatchTouchEvent和onInterceptTouchEvent可以控制touch事件流不傳遞給子控件,這兩個方法中可以控制事件流的向下分發,那麼是不是有方法控制事件流向上分發呢?我們找到ViewGroup中有這樣一個函數:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != ;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }

    // Check for cancelation.
    final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;

    //如果沒有攔截再分發下去處理
    if (!canceled && !intercepted) {
        ...
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            ...
        }
        ...
    }

    return handled;
}

/**
 * {@inheritDoc}
 */
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != )) {
        // We're already in this state, assume our ancestors are too
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}
           

實際上requestDisallowInterceptTouchEvent是修改了disallowIntercept的狀态,再結合ViewGroup的dispatchTouchEvent方法檢視,我們就明白這個方法的最終意義。ViewGroup的子元素可以通過調用這個方法禁止ViewGroup攔截touch事件。到這裡我們就找到了自下而上的touch事件的攔截方法。

滑動沖突兩種解決辦法

1. 外部攔截法

通過上面的原理分析我們知道我們可以在dispatchTouchEvent的時候不分發事件或者onInterceptTouchEvent時候攔截事件,實際上onInterceptTouchEvent方法是一個空方法,是android專門提供給我們處理touch事件攔截的方法,是以這裡我們在onInterceptTouchEvent方法中攔截touch事件。

具體做法就是當你不想把事件傳遞給子控件的時候在onInterceptTouchEvent方法中傳回true即可攔截事件,這時候子控件将不會再接收到這一次的touch事件流(所謂touch事件流是以ACTION_DOWN開始,中間包含若幹個ACTION_MOVE,以ACTION_UP結束的一連串事件)。僞代碼如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if ( condition ) {
        return true;
    }
    return false;
}
           

這裡的condition将會再下一章節中具體講解。

2. 内部攔截法

首先,我們讓父控件攔截除了ACTION_DOWN以外的所有事件,如果連ACTION_DOWN都攔截那麼子控件将無法收到任何touch事件:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}
           

然後,在控件的内部分發事件的時候請求需要的事件(實際上就是禁止父控件攔截事件):

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            //通知父容器不要攔截事件
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:

            if ( <condition> ){
                //通知父容器攔截此事件
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            parent.requestDisallowInterceptTouchEvent(false);
            break;
        default:
            break;
    }

    return super.dispatchTouchEvent(ev);
}
           

這樣,就可以解決touch事件的沖突問題,從控件本身解決。内部攔截法使用起來稍顯複雜,需要修改兩個控件,一般情況下我們都通過外部攔截法解決滑動沖突,如果有特殊情況需要使用内部攔截法才會使用内部攔截法。

事件攔截Condition

試想以下情況:

Android滑動沖突解決方法Android滑動沖突解決方法滑動沖突View Touch事件分發Activity分發事件到ViewGroup解決滑動沖突的原理滑動沖突兩種解決辦法事件攔截Condition滑動沖突解決拓展Touch事件的來源深入篇

MapView的功能是内部可以任意滑動(包括上下,左右以及任意方向滑動),ScrollView需要上下滑動。這時候我們在MapView内部上下滑動時會出現什麼結果?我們期望的結果是MapView内部滑動,但是我們看到的實際情況卻是ScrollView在上下滑動,滑動沖突就産生了,解決這個滑動沖突的方法很簡單,直接上代碼:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (isMapViewTouched(ev)) {
            return false;
        } else {
            return super.onInterceptTouchEvent(ev);
        }
    }

private boolean isMapViewTouched(MotionEvent ev) {
    if (getChildCount() == ) {
        float touchX = ev.getX();
        float touchY = ev.getY() + getScrollY();

        LinearLayout baseLinear = (LinearLayout) getChildAt();
        for (int i = ; i < baseLinear.getChildCount(); i++) {
            View child = baseLinear.getChildAt(i);

            // add map view you want ignore
            if (isMapView(child)) {
                if (touchX < child.getRight() && touchX > child.getLeft() &&
                        touchY < child.getBottom() && touchY > child.getTop()) {
                    return true;
                }
            }
        }
    }
    return false;
}

private boolean isMapView(View child) {
    return child instanceof MapView ||
            child instanceof com.google.android.gms.maps.MapView;
}
           

isMapViewTouched這個函數就是我們這個情況下的condition,具體的含義就是目前點選的是MapView那麼所有的touch事件都不允許攔截,交由MapView處理。

這是一種很簡單的滑動沖突情況,沒有判斷滑動的方向以及速度等因素,一般的我們通過判斷滑動的方向作為判斷條件,下面我們再來看一種情況:

Android滑動沖突解決方法Android滑動沖突解決方法滑動沖突View Touch事件分發Activity分發事件到ViewGroup解決滑動沖突的原理滑動沖突兩種解決辦法事件攔截Condition滑動沖突解決拓展Touch事件的來源深入篇

ViewPager需要左右滑動,ListView需要上下滑動,當我們斜向滑動時就出現了滑動沖突。實際上ViewPage已經解決了這種滑動沖突,這裡我們假定它沒有解決這種滑動沖突,我們自己來解決這個滑動沖突。當我們斜向滑動時候示意圖如下:

Android滑動沖突解決方法Android滑動沖突解決方法滑動沖突View Touch事件分發Activity分發事件到ViewGroup解決滑動沖突的原理滑動沖突兩種解決辦法事件攔截Condition滑動沖突解決拓展Touch事件的來源深入篇

當我們從start滑動到end時,x方向的坐标變化我們稱之為dx,y方向的坐标變化我們稱之為dy。

  1. 當dx > dy時我們視其為水準滑動
  2. 當dx < dy時我們視其為豎直滑動

通過外部攔截法的代碼如下:

//ViewPager.java
int lastX = -;
int lastY = -;
boolean isHorizontal = false;
boolean hasDirection = false;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
    int currentX = ev.getX();
    int currentY = ev.getY();

    switch( action ){
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            int dx = Math.abs(currentX - lastX);
            int dy = Math.abs(currentY - lastY);

            // 這裡為了保證使用者體驗,當我們第一次滑動的方向即為這次touch事件流的滑動方向
            if ( hasDirection ) {
                return isHorizontal;
            } else {
                if ( dx > dy ) { // 水準滑動
                    isHorizontal = true;
                    return true;
                } else { // 豎直滑動
                    isHorizontal = false;
                    return false;
                }
            }

            hasDirection = true;
            lastX = currentX;
            lastY = currentY;
            break;
        case MotionEvent.ACTION_UP:
            hasDirection = false;
            break;
    }

    return super.onInterceptTouchEvent(ev);
}
           

滑動沖突解決拓展

滑動沖突的解決方法我們已經知道了,以後無論遇到多麼複雜的情況解決滑動沖突的原則都是不變的,根據你的業務需求進行不同的事件攔截即可。

Touch事件的來源深入篇

如果你想知道Activity中的Touch事件是從哪來的,你可以檢視任玉剛大神的這篇文章:http://blog.csdn.net/singwhatiwanna/article/details/50775201