Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View控件的事件派發

Android View控件的事件派發

編輯:關於Android編程

引:一直想寫View,GroupView控件的事件派發流程.終於現在一口氣都寫出來.一鼓作氣!當然考慮了很久應該用怎麼樣的方式寫這些流程性的東西,應該用什麼用的語言來描述.後來想就按照聊天的方式把!盡量把整個派發過程都寫下來,並且實現一個簡單的山寨派發流程.有點只見樹不見山的感覺.但是覺得自己寫一次view的事件派發勝過再多的理論!

派發流程


首先我們來看看view的事件派發.

關鍵函數
public boolean dispatchTouchEvent(MotionEvent event);
public boolean onTouchEvent(MotionEvent event);

看看一個簡單的”演示代碼-1”.
繼承view並且在dispatchTouchEvent和onTouchEvent添加打印信息.

MainActivity.java

package com.dsliang.eventdispatch;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

EventDispatchView.java

package com.dsliang.eventdispatch;

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

/**
 * Created by dsliang on 16-7-31.
 */
public class EventDispatchView extends View {

    public static String Tag = EventDispatchView.class.getSimpleName();

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result;
        Log.d(Tag, "dispatchTouchEvent: " + event.getAction());

        result = super.dispatchTouchEvent(event);

        Log.d(Tag, "dispatchTouchEvent result: " + result);

        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        boolean result;

        Log.d(Tag, "onTouchEvent: " + event.getAction());

        result = super.onTouchEvent(event);

        Log.d(Tag, "onTouchEvent result: " + result);


        return result;
    }
}

activity_main.xml




    

這裡寫圖片描述

其實在上面的”演示代碼-1”裡面我們其實什麼事情都沒處理.看看在模擬器上運行的效果是怎麼樣把.

結論:當我們點擊紅色區域的時候.先後打印出dispatchTouchEvent,onTouchEvent.在這裡我們可以知道分發過程必定是先調用dispatchTouchEvent函數然後再調用onTouchEvent函數.

如果你的代碼這樣寫,那麼對於我這篇文章你就沒必要繼續看下去了.事實上我的代碼不應該這樣寫!

一切起於零


然後看看接下來的”演示代碼-2”.我們僅僅修改文件EventDispatchView.java文件.

EventDispatchView.java

package com.dsliang.eventdispatch;

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

/**
 * Created by dsliang on 16-7-31.
 */
public class EventDispatchView extends View {

    public static String Tag = EventDispatchView.class.getSimpleName();

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result;
        Log.d(Tag, "dispatchTouchEvent: " + event.getAction());

//        result = super.dispatchTouchEvent(event);
        result = false;

        Log.d(Tag, "dispatchTouchEvent result: " + result);

        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        boolean result;

        Log.d(Tag, "onTouchEvent: " + event.getAction());

//        result = super.onTouchEvent(event);
        result = false;

        Log.d(Tag, "onTouchEvent result: " + result);


        return result;
    }
}
我們在dispatchTouchEvent函數裡面返回false並且調用父類的dispatchTouchEvent方法.對onTouchEvent函數我們也進行同樣的修改.(對於此時,兩個函數的返回值是true/false對目前)

這裡寫圖片描述

結論:很明顯能看到onTouchEvent函數並沒有調用.那麼就是說明其實在父類的dispatchTouchEvent裡面一定是調用了onTouchEvent函數.

小試牛刀


這個道理你懂了以後我們要達到父類的相同效果,我們現在就針對dispatchTouchEvent進行一番修改.

現在我們也是只修改EventDispatchView.java文件.

EventDispatchView.java

package com.dsliang.eventdispatch;

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

/**
 * Created by dsliang on 16-7-31.
 */
public class EventDispatchView extends View {

    public static String Tag = EventDispatchView.class.getSimpleName();

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result;
        Log.d(Tag, "dispatchTouchEvent: " + event.getAction());

//        result = super.dispatchTouchEvent(event);
        result = doOnDispatchTouchEvent(event);


        Log.d(Tag, "dispatchTouchEvent result: " + result);

        return result;
    }

    private boolean doOnDispatchTouchEvent(MotionEvent event) {
        return onTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        boolean result;

        Log.d(Tag, "onTouchEvent: " + event.getAction());

//        result = super.onTouchEvent(event);
        result = false;

        Log.d(Tag, "onTouchEvent result: " + result);


        return result;
    }
}
添加doOnDispatchTouchEvent函數,在dispatchTouchEvent調用doOnDispatchTouchEvent函數,doOnDispatchTouchEvent函數調用onTouchEvent函數.這樣修改效果基本就和父類有一樣的行為了.

這裡寫圖片描述

從上面的效果圖也可以看出的確現在這樣修改以後和"演示代碼-1"行為上看起來好像已經是一致了.

但是現在我又注意到一個問題!event.getAction函數返回是0?進去MotionEvent類看看0代表什麼.其實0就是ACTION_DOWN.原來是按鈕按下事件.但是也是不科學吧?稍稍有android事件的常事我們都會意識到.點擊過程至少會有三個事件會觸發.分別是按下,移動,抬起.怎麼說再不賴也得有抬起事件把?

這裡寫圖片描述

來到這裡,首先拋出一個結論.如果dispatchTouchEvent在按下事件返回false說明此控件並沒有消耗此次事件.那麼系統(在view的角度觸發你可以認為是系統,但是准確來說應該是你的包含你空間的布局容器)會認為你對接下來的一系列事件(移動,抬起)都不感興趣.簡單說就是在收到按下事件的時候返回false,接下來的移動,抬起事件都不會傳遞到次控件.(這個結論在下一篇”Android GroupView控件的事件派發”會具體闡述說明)

稍稍做潤色


嗯嗯,那麼稍稍把onTouchEvent的會返回值修改一下.這個就是我們的”演示代碼-3”了.同樣也是只修改EventDispatchView.java文件.

EventDispatchView.java

package com.dsliang.eventdispatch;

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

/**
 * Created by dsliang on 16-7-31.
 */
public class EventDispatchView extends View {

    public static String Tag = EventDispatchView.class.getSimpleName();

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result;
        Log.d(Tag, "dispatchTouchEvent: " + event.getAction());

//        result = super.dispatchTouchEvent(event);
        result = doOnDispatchTouchEvent(event);


        Log.d(Tag, "dispatchTouchEvent result: " + result);

        return result;
    }

    private boolean doOnDispatchTouchEvent(MotionEvent event) {
        return onTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        boolean result;

        Log.d(Tag, "onTouchEvent: " + event.getAction());

//        result = super.onTouchEvent(event);
        result = true;

        Log.d(Tag, "onTouchEvent result: " + result);


        return result;
    }
}

現在這樣可以看的按下,移動,抬起都派發給我們的EventDispatchView控件了!

這裡寫圖片描述

現在我們先來一個小總結.
1,dispatchTouchEvent函數會調用onTouchEvent函數
2,只有在按下到來的時候dispatchTouchEvent返回true才會接收到移動,抬起事件.

初試鋒芒


看起來這一節應該是結束的節奏了吧?圖樣圖森破了!
你是忘了在使用控件的時候,可以設置點擊事件和接聽滑動事件麼?
接下來看看”演示代碼-4”.為了突出點擊事件,滑動事件.dispatchTouchEvent和onTouchEvent只調用父類的方法.另外給EventDispatchView設置點擊事件和滑動事件的監聽函數.

MainActivity.java

package com.dsliang.eventdispatch;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class MainActivity extends Activity {

    public static String Tag = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        View view = findViewById(R.id.viewEventDispatchView);

        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d(Tag, "onClick");
            }
        });

        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                Log.d(Tag, "onTouch");
                return true;
            }
        });
    }
}

EventDispatchView.java

package com.dsliang.eventdispatch;

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

/**
 * Created by dsliang on 16-7-31.
 */
public class EventDispatchView extends View {

    public static String Tag = EventDispatchView.class.getSimpleName();

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result;
        Log.d(Tag, "dispatchTouchEvent: " + event.getAction());

        result = super.dispatchTouchEvent(event);
//        result = doOnDispatchTouchEvent(event);


        Log.d(Tag, "dispatchTouchEvent result: " + result);

        return result;
    }

    private boolean doOnDispatchTouchEvent(MotionEvent event) {
        return onTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        boolean result;

        Log.d(Tag, "onTouchEvent: " + event.getAction());

        result = super.onTouchEvent(event);
//        result = true;

        Log.d(Tag, "onTouchEvent result: " + result);


        return result;
    }
}

activity_main.xml




    

這裡有兩個地方需要注意:
1,onTouch函數返回false.onTouch函數的返回值會對派發事件有什麼影響?
2,dispatchTouchEvent函數和onTouchEvent函數均使用父類方法的返回值作為返回值.意味著我們只能獲取到按下事件.(並沒有消耗按下事件)

這裡寫圖片描述

仔細看看,當我們設置了點擊事件監聽函數之後super.onTouchEvent(event)居然返回true了!不科學呀,明顯很之前的有矛盾把?

先給出結論:設置了點擊事件回調函數後會改變super.onTouchEvent函數的默認行為.

    /**
     * Register a callback to be invoked when this view is clicked. If this view is not
     * clickable, it becomes clickable.
     *
     * @param l The callback that will run
     *
     * @see #setClickable(boolean)
     */
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

...

    /**
     * Enables or disables click events for this view. When a view
     * is clickable it will change its state to "pressed" on every click.
     * Subclasses should set the view clickable to visually react to
     * user's clicks.
     *
     * @param clickable true to make the view clickable, false otherwise
     *
     * @see #isClickable()
     * @attr ref android.R.styleable#View_clickable
     */
    public void setClickable(boolean clickable) {
        setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
    }

...

設置點擊回調函數以後,setClickable函數將CLICKABLE置1了.(flags的CLICKABLE位).

public boolean onTouchEvent(MotionEvent event) {

       ...

        /*
        當設置了點擊事件回調函數,次條件成立.然後無論是
        ACTION_UP,ACTION_DOWN,ACTION_CANCEL,ACTION_MOVE都返回true
        */
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:

                    ...

                    break;

                case MotionEvent.ACTION_DOWN:

                   ...

                    break;

                case MotionEvent.ACTION_CANCEL:

                   ...

                    break;

                case MotionEvent.ACTION_MOVE:

                    ...

                    break;
            }

            return true;
        }

        return false;
    }


...

    public boolean dispatchTouchEvent(MotionEvent event) {

        ...

        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;
            }
        }

        ...

    }

...
事實上只有設置了點擊回調函數/長按回調函數均會返回ture.所以也解釋了為什麼設置回調函數後super.onTouchEvent返回值變成true了.

仔細的同學可能發現了onClick回調函數是在ACTION_UP事件裡面調用.這很符合我們使用習慣!哪一個軟件不是松開手才調用按鈕事件呢?具體情況是performClick函數負責調用onClick函數.

當然有更仔細的同學發現另一個問題了!super.dispatchTouchEvent在調用onTouchEvent函數之前會調用onTouch函數並且根據onTouch函數的返回值判斷是否返回!!!這意味了什麼?意味著如果你同時設置了onClick函數onTouch的情況下,如果onTouch返回false.那麼一切都正常你不會發現什麼很玄的東西.但是一旦你講onTouch返回true.那麼問題就會來了.onClick函數沒有如願的調用.就下下面展示的圖片一樣.

這裡寫圖片描述

專屬山寨版


到這裡我們摸透view派發事件的默認行為了,那麼我們模仿來寫一個屬於我們理解的派發過程把!

“演示代碼-5”

MainActivity.java

package com.dsliang.eventdispatch;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class MainActivity extends Activity {

    public static String Tag = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        View view = findViewById(R.id.viewEventDispatchView);

        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d(Tag, "onClick");
            }
        });

        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                Log.d(Tag, "onTouch");
                return true;
            }
        });
    }
}

EventDispatchView.java

package com.dsliang.eventdispatch;

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

/**
 * Created by dsliang on 16-7-31.
 */
public class EventDispatchView extends View {

    public static String Tag = EventDispatchView.class.getSimpleName();

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

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

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

    private OnTouchListener mTouchListener = null;

    /*
    onTouch子類無法訪問,當設置onTouch的時候保存對象的引用
     */
    @Override
    public void setOnTouchListener(OnTouchListener l) {
        super.setOnTouchListener(l);
        this.mTouchListener = l;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result;
        Log.d(Tag, "dispatchTouchEvent: " + event.getAction());

//        result = super.dispatchTouchEvent(event);

        if (null != this.mTouchListener && this.mTouchListener.onTouch(this, event)) {
            return true;
        }

        result = doOnDispatchTouchEvent(event);


        Log.d(Tag, "dispatchTouchEvent result: " + result);

        return result;
    }

    private boolean doOnDispatchTouchEvent(MotionEvent event) {
        return onTouchEvent(event);
    }

    public void callOnClickListener() {

        /*
        performClick()已經封裝了怎麼調用onclick函數
         */
        if (isClickable()) {
            performClick();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {


        Log.d(Tag, "onTouchEvent: " + event.getAction());

//        result = super.onTouchEvent(event);


        /*
        可以點擊,可以長按
         */
        if (isClickable() || isLongClickable()) {

            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    ;
                    break;
                case MotionEvent.ACTION_UP:
                    callOnClickListener();
                    break;
                case MotionEvent.ACTION_MOVE:
                    ;
                    break;
                case MotionEvent.ACTION_CANCEL:
                    ;
                    break;

            }

            return true;
        }

        return false;

    }
}

activity_main.xml




    

理論上現在我們的EventDispatchView控件就擁有了系統默認的行為了.(在事件派發方面)

現在然我們總結一下view在事件派發方面的幾個關鍵地方:

1,事件派發首先調用doOnDispatchTouchEvent函數,然後調用onTouchEvent函數
2,根據ACTION_DOWN事件是否給消耗判,斷接下來能否接收其余的一連串事件.
3,onClick回調函數在ACTION_UP事件中才調用
4,系統在沒有設置點擊回調函數/長按回調函數的情況下view不會消耗事件.(ACTION_DOWN事件)
5,onTouch回調函數的返回值會影響點擊回調函數/長按回調函數的調用

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