Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View 事件分發機制源碼詳解(View篇)

Android View 事件分發機制源碼詳解(View篇)

編輯:關於Android編程

前言

在Android View 事件分發機制源碼詳解(ViewGroup篇)一文中,主要對ViewGroup#dispatchTouchEvent的源碼做了相應的解析,其中說到在ViewGroup把事件傳遞給子View的時候,會調用子View的dispatchTouchEvent,這時分兩種情況,如果子View也是一個ViewGroup那麼再執行同樣的流程繼續把事件分發下去,即調用ViewGroup#dispatchTouchEvent;如果子View只是單純的一個View,那麼調用的是View#dispatchTouchEvent。因此,本文將分析View(非ViewGroup)的事件分發、處理機制。

View#dispatchTouchEvent

事件來到View的時候,會調用該方法,前提是你的自定義View沒有重寫該方法。我們先看看它的源碼:

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)) {  // 1
               result = true;
            }

        if (!result && onTouchEvent(event)) {  // 2
                result = true;
        }
    }
    ...
    return result;
}

我們只看重點部分,這裡有一個判斷if(onFilterTouchEventForSecurity(event)),這個主要是判斷當前事件到來的時候,窗口有沒有被遮擋,如果被遮擋則會直接返回false,從而中斷事件的處理。如果窗口沒被遮擋,那麼會正常處理事件。在IF體內部,首先定義了一個ListenerInfo,那麼這個ListenerInfo是什麼呢?我們跟進去看看:

static class ListenerInfo {

        public OnClickListener mOnClickListener;

        protected OnLongClickListener mOnLongClickListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;
        ...
    }

可以看到,這是View裡面的一個內部類,定義了一系列的Listener,其中有我們經常用到的onClickListener,這裡是獲取當前View所設置的Listener。接著是①號處的一個判斷,判斷當前View是否設置了onTouchListener,如果設置了onTouchListener的話,則會調用onTouchListener.onTouch方法,然後根據onTouch方法的返回值來設置result,表示事件是否被處理。**這裡可以看出:**onTouchListener的優先級最高,如果在onTouchListener#onTouch中返回true即消耗了事件,那麼就無必要繼續執行下面的語句了。如果沒有設置onTouchListener或者該監聽器內部沒有消耗事件,那麼就會執行②號代碼,來調用View#onTouchEvent()。

View#onTouchEvent

由於源碼較長,這裡分段來講述。
1、先看下面這一段:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // 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)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

以上判斷了當前View是否可用,如果不可用則進入IF體,根據注釋我們知道,即使是不可以狀態下的View,如果它自身是可點擊或者可長按的話,一樣會消耗事件,只是不作出任何反應罷了。
2、接著往下看:

if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

這裡判斷是否設置了mTouchDelegate,這個表示View的代理,即如果設置了代理,那麼當前View的點擊事件會交給代理的View來處理,調用代理View的onTouchEvent方法,如果代理View消耗了事件,那麼相當於當前View消耗了事件。
3、接下來便是onTouchEvent對View事件的具體處理了:

if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                ...
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // 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();
                        }
                    }
                }
            ...
            break;
        ...
    }
    return true;
}

首先是判斷當前View是否可以點擊或者長按,其中一個為true的話,就會進入IF體。進入IF體後,是對事件進行判斷,可以看到最後會返回true,即事件最後會被消耗。也就是說,如果一個View是clickable或者long_clickable的話,該onTouchEvent方法會返回true,把事件消耗掉
我們看看對ACTION_UP的事件進行響應的部分,首先會判斷當前View是否是pressed狀態,即按下狀態,如果是按下狀態就會觸發performClick()方法,我們看看這個方法做了什麼,View#performClick:

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

可以看出,這裡檢測了當前View是否設置了onClickListener,如果設置了那麼回調它的onClick方法,所以我們平時對一個Button設置點擊事件之後,都會在其onTouchEvent方法的ACTION_UP邏輯裡面得到回調。
這裡可以得出結論:onTouchListener、onTouchEvent、onClickListener三者的優先級是:onTouchListener>onTouchEvent>onClickListener。

至此,對於View的事件分發、處理過程分析完畢,接下來總結一下:
1、事件傳遞給View的時候,會調用dispatchTouchEvent()方法,但是View沒有onIntercept方法,所以會接著調用onTouchEvent()方法。
2、如果一個View是可點擊的(clickable或long_clickable),那麼它默認會消耗事件。對於一個Button來說,默認是可點擊的,對於一個textView來說,默認是不可點擊的,而對於一個自定義View來說,默認也是不可點擊的,可以在xml布局中設置View的點擊性質。
3、如果對一個View設置了onClickListener監聽,那麼確保它的可點擊的,而且接收到了ACTION_DOWN和ACTION_UP事件。

驗證性試驗

以下是驗證性試驗,根據這兩篇文章所述內容來設置不同的場景來驗證以上的源碼分析的正確性。
①首先新建一個ViewGroupA,繼承自LinearLayout,重寫了三個重要方法,但是只是打印了事件,dispatchTouchEvent和onIntercept會調用父類的響應方法,而onTouchEvent方法則返回true。代碼如下:

public class ViewGroupA extends LinearLayout {

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

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

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog", "ViewGroupA onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewGroupA onTouchEvent ACTION_MOVE");
                break;
        }
        return true;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog","ViewGroupA dispatchTouchEvent down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewGroupA dispatchTouchEvent move");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog","ViewGroupA onInterceptTouchEvent down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewGroupA onInterceptTouchEvent move");
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

②接下來是在ViewGroupA內部的一個子View,ViewA,重寫了dispatchToucheEvent和onTouchEvent方法,如下所示:

package com.chenyu.viewstudy;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by Administrator on 2016/4/17.
 */
public class ViewA extends View {

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

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

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog","ViewA onTouchEvent down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewA onTouchEvent move");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("cylog","ViewA onTouchEvent up");
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d("cylog","ViewA dispatchTouchEvent down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","ViewA dispatchTouchEvent move");
                break;
        }
        return super.dispatchTouchEvent(event);
    }
}

③MainActivity內部只是設置了布局,並無別的代碼,這裡不再貼出。
④xml布局文件如下:


    
        
    

我們先看看布局圖如下:
布局
上面藍色區域是ViewGroupA,紅色區域是ViewA,運行程序,我們在紅色區域滑動一下,結果如下所示:
這裡寫圖片描述
可以看出,事件正常分發,從ViewGroup開始到View,並在View中得到處理。
以下開始改變條件:
1、ViewGroup攔截ACTION_DOWN事件
在ViewGroupA中做出如下改動:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()){ ... } //對ACTION_DWON攔截,返回true。 if (ev.getAction() == MotionEvent.ACTION_DOWN){ return true; } return super.onInterceptTouchEvent(ev); }

運行,結果如下所示:
驗證1
可以看出,ViewGroupA攔截了ACTION_DOWN事件,那麼ViewA接收不到事件了,所以後面的全部事件都由ViewGroupA處理。

2、ViewGroup攔截ACTION_MOVE事件
同樣,在ViewGroupA中做出如下改動:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            ...
        }
        if (ev.getAction() == MotionEvent.ACTION_MOVE){
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

運行結果如下:
驗證2
可以看出,ViewA還是能正常處理ACTION_DOWN事件,但是由於ACTION_MOVE事件被ViewGroup攔截了,所以ViewGroup來處理ACTION_MOVE事件,我們注意到,onIntercept方法來攔截成功後,後續的事件分發流程並不會再次調用,所以一個View攔截了事件後,後續的所有事件都交由這個View處理,並不會再次判斷是否需要攔截,所以這也符合上一篇文章的分析。

3、基於第2點攔截了MOVE事件,同時ViewGroup的onTouchEvent返回值修改,原來是直接返回true的,表示消耗了事件,那麼這裡直接返回super.onTouchEvent(ev):

@Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action){
            ...
        }
        return super.onTouchEvent(event);
    }

同時在Activity中重寫onTouchEvent()方法:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                Log.d("cylog","Activity onTouchEvent ACTION_MOVE");
                break;
        }
        return super.onTouchEvent(event);
    }

結果如下:
驗證3
可以看出,super.onTouchEvent(ev)返回了false,表示不消耗事件,為什麼會這樣呢?根據本文分析,一個View只有在可點擊的狀態下,自身的onTouchEvent方法才會返回true,這裡調用的是super.onTouchEvent表示調用父類的onTouchEvent方法,又由於ViewGroupA繼承自LinearLayout,本身是不可點擊的,所以這裡自然會返回false。然後我們看到,最終這些沒被消耗的時候回到了Activity,被Activity消耗掉了。其實這也很好理解,上一篇文章說過,事件的分發是從Activity開始的,不斷往下尋找能消耗事件的子元素,但如果事件沒被子元素消耗,則會逐層返回到Activity。
所以這裡得出結論:如果View不消耗除了ACTION_DOWN事件之外的其他事件(因為ACTION_DWON事件會初始化事件序列),這個View依然也會接收後續的事件,同時這些沒被消耗的事件最終會被Activity消耗。

4、ViewGroupA不做任何修改,對ViewA修改,為ViewA設置onTouchListener和onClickListener

View viewA = findViewById(R.id.viewa);
        viewA.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        Log.d("cylog","ViewA onTouchListener down");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.d("cylog", "ViewA onTouchListener move");
                }
                return true;
            }
        });
        viewA.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("cylog","ViewA onClickListener ");
            }
        });

結果如下:
驗證4閱讀。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved