天天看點

你真的了解事件分發傳遞機制原理嗎

1、什麼是事件傳遞

可以了解為觸摸事件在Activity和控件之間,控件和控件之間的傳遞過程。

2、學習完有什麼用處

自定義控件處理控件之間的沖突,明白點選事件由哪個對象發出,經過哪些對象,最終達到哪個對象并最終得到處理。

3、學習之前應該了解什麼

3.1 什麼是ViewGroup

內建的View,可以充當其他view的容器

3.1 什麼是View

單一控件,例如 textview ,button等等

3.2 事件傳遞的重要方法,事件分發的三個方法

(1)dispathTouchEvent   事件分發

(2)onInterceptTouchEvent  阻攔機制

(3)onTouchEvent   消費機制 

3.3 明白 MotionEvent 的幾個處理事件

(1)MotionEvent.ACTION_DOWN:按下View(所有事件的開始)

(2)MotionEvent.ACTION_MOVE:滑動View

(3)MotionEvent.ACTION_CANCEL:非人為原因結束本次事件

(4)MotionEvent.ACTION_UP:擡起View(與DOWN對應)

4、知道事件傳遞的基本過程

圖檔總是比文字更容易讓人了解,這裡我們就選擇文字來讓我們開始了解事件傳遞的基本順序,如下圖布局

你真的了解事件分發傳遞機制原理嗎

我們可以把圖檔打比方作為下圖

你真的了解事件分發傳遞機制原理嗎

下圖就是基本的傳遞過程 

你真的了解事件分發傳遞機制原理嗎

Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View,在ViewGroup中通過onInterceptTouchEvent()對事件傳遞進行攔截onInterceptTouchEvent方法傳回true代表攔截事件,即不允許事件繼續向子View傳遞,然後就會調用自己本身的onTouchEvent事件,進行處理,此時的onTouchEvent也具有false & True的選擇,True就代表事件已經被我們的view所消化掉,false就會往上傳遞,一直到activity中;onInterceptTouchEvent() 傳回false代表不攔截事件,即允許事件繼續向子View傳遞;(預設傳回false),子View中如果将傳遞的事件消費掉,ViewGroup中将無法接收到任何事件。

5、知道各種點選事件的排列順序

onTouch和onClick事件同時發生

ontouch的執行先于onclick的執行

6、onTouch 以及 onTouchEvent 的差別

這兩個方法都是在View的dispatchTouchEvent中調用的,onTouch優先于onTouchEvent執行。如果在onTouch方法中通過傳回true将事件消費掉,onTouchEvent将不會再執行。

另外需要注意的是,onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能為空,第二目前點選的控件必須是enable的。是以如果你有一個控件是非enable的,那麼給它注冊onTouch事件将永遠得不到執行。對于這一類控件,如果我們想要監聽它的touch事件,就必須通過在該控件中重寫onTouchEvent方法來實作。

你真的了解事件分發傳遞機制原理嗎

7、知道view點選的調用過程

你真的了解事件分發傳遞機制原理嗎

8、onTouch 以及 onTouchEvent 的關系、深入

根據 view中 的 dispatchTouchEvent 源碼我們可以知道outouch執行的三個必須條件。

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}
           

1、mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)這三個條件都為真,就傳回true,否則就去執行onTouchEvent(event)方法并傳回。 

2、第二個條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷目前點選的控件是否是enable的,按鈕預設都是enable的,是以這個條件恒定為true。

3、第三個條件就比較關鍵了,mOnTouchListener.onTouch(this, event),其實也就是去回調控件注冊touch事件時的onTouch方法。

也就是說如果我們在onTouch方法裡傳回true,就會讓這三個條件全部成立,進而整個方法直接傳回true。如果我們在onTouch方法裡傳回false,就會再去執行onTouchEvent(event)方法。

是以總結說若要執行ouTouch事件,必須滿足以經被設定點選事件,還有滿足可點選,如 button 就是可以點選的view,imageview就是不可點選的view。第三個就是注冊了這個ontouch事件的view。 如果是那些沒有支援點選的我們可以選擇使用ontouchevent事件來進行處理。

9、為什麼onTouch執行比onclick的執行先

onclick事件執行在onTouchEvent裡面

我們可以得知在我們outouch中傳回true得時候,我們得onclick事件就會無法被執行到,由此我們可以判斷出,onclick事件是執行在onTouchEvent事件當中的。也是以我們可知onTouch執行比onclick的執行先

在onTouchEvent中們可以找到在一個事件下執行力下列操作

public boolean performClick() {  
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
    if (mOnClickListener != null) {  
        playSoundEffect(SoundEffectConstants.CLICK);  
        mOnClickListener.onClick(this);  
        return true;  
    }  
    return false;  
}
           

 現在我們就可以得知原因。

10、viewgroup的點選調用過程

根據下列源碼我們得知

點選viewgroup時候,我們會先去尋找viewgroup下面的所有子view,知道找到view為目前點選的區域在進行調用child.dispatchTouchEvent(ev).

for( int i = count - 1;  i >=0;i--) {

        final View child = children[i];

        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE

                || child.getAnimation() != null) {

            child.getHitRect(frame);

            if (frame.contains(scrolledXInt, scrolledYInt)) {

                final float xc = scrolledXFloat - child.mLeft;

                final float yc = scrolledYFloat - child.mTop;

                ev.setLocation(xc, yc);

                child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;

                if (child.dispatchTouchEvent(ev)) {

                    mMotionTarget = child;

                    return true;
                }
            }
        }
    }
           

當你點選了某個控件,首先會去調用該控件所在布局的dispatchTouchEvent方法,然後在布局的dispatchTouchEvent方法中找到被點選的相應控件,再去調用該控件的dispatchTouchEvent方法。如果我們點選了MyLayout中的按鈕,會先去調用MyLayout的dispatchTouchEvent方法,可是你會發現MyLayout中并沒有這個方法。那就再到它的父類LinearLayout中找一找,發現也沒有這個方法。那隻好繼續再找LinearLayout的父類ViewGroup,你終于在ViewGroup中看到了這個方法,按鈕的dispatchTouchEvent方法就是在這裡調用的。修改後的示意圖如下所示: button存在于mylayout當中

你真的了解事件分發傳遞機制原理嗎

我們通過此也來看一下ViewGroup中的dispatchTouchEvent方法的源碼,如下

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    final View target = mMotionTarget;
    if (target == null) {
        ev.setLocation(xf, yf);
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        }
        return super.dispatchTouchEvent(ev);
    }
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        ev.setAction(MotionEvent.ACTION_CANCEL);
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
        }
        mMotionTarget = null;
        return true;
    }
    if (isUpOrCancel) {
        mMotionTarget = null;
    }
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        mMotionTarget = null;
    }
    return target.dispatchTouchEvent(ev);
}
           

可以看到一個條件判斷,如果disallowIntercept和!onInterceptTouchEvent(ev)兩者有一個為true,就會進入到這個條件判斷中。disallowIntercept是指是否禁用掉事件攔截的功能,預設是false,就是不抑制,可以傳遞給子view,否則相反也可以通過調用requestDisallowInterceptTouchEvent方法對這個值進行修改。那麼當第一個值為false的時候就會完全依賴第二個值來決定是否可以進入到條件判斷的内部,第二個值是什麼呢?竟然就是對onInterceptTouchEvent方法的傳回值取反!也就是說如果我們在onInterceptTouchEvent方法中傳回false,就會讓第二個值為true,進而進入到條件判斷的内部,如果我們在onInterceptTouchEvent方法中傳回true,就會讓第二個值為false,進而跳出了這個條件判斷。

11、(注意)

我們都知道如果給一個控件注冊了touch事件,每次點選它的時候都會觸發一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。這裡需要注意,如果你在執行ACTION_DOWN的時候傳回了false,後面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,隻有前一個action傳回true,才會觸發後一個action。

參考着我們前面分析的源碼,點選可以允許點選的控件的時候 。首先在onTouch事件裡傳回了false,就一定會進入到onTouchEvent方法中,然後我們來看一下onTouchEvent方法的細節。由于我們點選了按鈕,就會進入到第14行這個if判斷的内部,然後你會發現,不管目前的action是什麼,最終都一定會走到第89行,傳回一個true。 

public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
                    if (!mHasPerformedLongPress) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();
                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }
                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
                        mPrivateFlags |= PRESSED;
                        refreshDrawableState();
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }
                    removeTapCallback();
                }
                break;
            case MotionEvent.ACTION_DOWN:
                if (mPendingCheckForTap == null) {
                    mPendingCheckForTap = new CheckForTap();
                }
                mPrivateFlags |= PREPRESSED;
                mHasPerformedLongPress = false;
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                break;
            case MotionEvent.ACTION_CANCEL:
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
                removeTapCallback();
                break;
            case MotionEvent.ACTION_MOVE:
                final int x = (int) event.getX();
                final int y = (int) event.getY();
                // Be lenient about moving outside of buttons
                int slop = mTouchSlop;
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                        (y < 0 - slop) || (y >= getHeight() + slop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();
                        // Need to switch from pressed to not pressed
                        mPrivateFlags &= ~PRESSED;
                        refreshDrawableState();
                    }
                }
                break;
        }
        return true;
    }
    return false;
}
           

viewGroup與view中的dispatchTouchEvent中的差別是 viewgroup會先判斷目前 上級view的阻攔事件,以及請求不阻攔事件是否有發生的前提下,在進行深入的查找目前點選的子view,根據點選位置找到子view的時候,再去執行子view的dispatchTouchEvent,然後就與view的dispatchTouchEvent的操作為相同的操作。

12、為什麼我們在

onTouchEvent中傳回ture不會執行onclick事件呢

OnClickListener.onClick()

是在原生的

View.onTouchEvent()

方法裡面回調的, 你自己重寫了這個方法, 而且不調用

super.onTouchEvent(event)

的話當然就不會回調

onClick()

方法了.

13、如果子控件被限制住了,應該如何進行使他的操作沒有被限制?

根據上面源碼我們可以,重寫onTouchEvent ,使父控件不會限制住,更改二選一的條件,是父控件尋找子控件。
           
getParent().requestDisallowInterceptTouchEvent(true);
           

14、demo案例 解決 scrollview嵌套 listview無法滾動的情形

效果圖

你真的了解事件分發傳遞機制原理嗎
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:gravity="center"
                android:text="上部分" />

            <com.example.jie.foreverdemo.Fragment.ListViewCustom
                android:id="@+id/list"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"></com.example.jie.foreverdemo.Fragment.ListViewCustom>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:gravity="center"
                android:text="下部分" />
        </LinearLayout>
    </ScrollView>

</LinearLayout>
           

重寫listview

package com.example.jie.foreverdemo.Fragment;

import android.content.Context;
import android.util.AttributeSet;
import android.util.EventLog;
import android.view.MotionEvent;
import android.widget.ListView;

/**
 * Created by jie on 2018/10/10.
 */

public class ListViewCustom extends ListView {
    public ListViewCustom(Context context) {
        super(context);
    }

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

    public ListViewCustom(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /*** @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_UP:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_MOVE:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            default:
                break;
        }
        //執行父類原先邏輯
        super.onTouchEvent(event);
        return true;
    }
}
           

重點是重寫 onTouchEvent 使用   getParent().requestDisallowInterceptTouchEvent(true);來去除限制,允許找到目前控件

執行父類原先邏輯,才不會讓clcik事件消失。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_UP:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_MOVE:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            default:
                break;
        }
        //執行父類原先邏輯
        super.onTouchEvent(event);
        return true;
    }
           

activity類

public class DispatchDemoActivity extends Activity {
    @BindView(R.id.list)
    ListView list;
    private List<Integer> listdata = new ArrayList<Integer>();
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_dispatch_demo);
        ButterKnife.bind(this);
        initdata();
        initview();
    }

    private void initdata() {
        for (int i = 0; i <= 20; i++) {
            listdata.add(i);
        }
    }

    private void initview() {
        list.setAdapter(new ArrayAdapter<Integer>(this, android.R.layout.simple_list_item_1, listdata));
        setListViewHeightBasedOnChildren(list);
    }

    /**
     * 2* @param listView
     */
    public void setListViewHeightBasedOnChildren(ListView listView) {
        // 擷取ListView對應的Adapter
        ListAdapter listAdapter = listView.getAdapter();
        if (listAdapter == null) {
            return;
        }

        int totalHeight = 0;
        for (int i = 0, len = listAdapter.getCount(); i < len; i++) {
            // listAdapter.getCount()傳回資料項的數目
            View listItem = listAdapter.getView(i, null, listView);
            // 計算子項View 的寬高
            listItem.measure(0, 0);
            // 統計所有子項的總高度
            totalHeight += listItem.getMeasuredHeight();
        }

        ViewGroup.LayoutParams params = listView.getLayoutParams();
        params.height = (totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1))) / 2;
        // listView.getDividerHeight()擷取子項間分隔符占用的高度
        // params.height最後得到整個ListView完整顯示需要的高度
        listView.setLayoutParams(params);
    }
}
           

出現此類問題的都是皆是因為父類控件中斷了事件的傳遞給下一層的子類控件

相同的例子呢還有viewpager多重嵌套問題,解決的方法呢,也是跟我上面舉例的demo是一樣的

thanks!!!

參考文章:https://blog.csdn.net/guolin_blog/article/details/9097463

                  https://blog.csdn.net/guolin_blog/article/details/9153747

                  https://segmentfault.com/q/1010000007080819