Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開發-分析ViewGroup、View的事件分發機制、結合職責鏈模式

Android開發-分析ViewGroup、View的事件分發機制、結合職責鏈模式

編輯:關於Android編程

介紹

介紹了職責鏈模式,作為理解View事件分發機制的基礎。
套用職責鏈模式的結構分析,當我們的手指在屏幕上點擊或者滑動,就是一個事件,每個顯示在屏幕上的View或者ViewGroup就是職責對象,它們通過Android中視圖層級組織關系,層層傳遞事件,直到有職責對象處理消耗事件,或者沒有職責對象處理導致事件消失。

關鍵概念介紹

要理解有關View的事件分發,先要看幾個關鍵概念

MotionEvent

當手指接觸到屏幕以後,所產生的一系列的事件中,都是由以下三種事件類型組成。
  1. ACTION_DOWN: 手指按下屏幕
  2. ACTION_MOVE: 手指在屏幕上移動
  3. ACTION_UP: 手指從屏幕上抬起
  例如一個簡單的屏幕觸摸動作觸發了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->…->ACTION_MOVE->ACTION_UP
  對於Android中的這個事件分發機制,其中的這個事件指的就是MotionEvent。而View的對事件的分發也是對MotionEvent的分發操作。可以通過getRawX和getRawY來獲取事件相對於屏幕左上角的橫縱坐標。通過getX()和getY()來獲取事件相對於當前View左上角的橫縱坐標。

重要的方法

public boolean dispatchTouchEvent(MotionEvent ev)

  這是一個對事件分發的方法。如果一個事件傳遞給了當前的View,那麼當前View一定會調用該方法。對於dispatchTouchEvent的返回類型是boolean類型的,返回結果表示是否消耗了這個事件,如果返回的是true,就表明了這個View已經被消耗,不會再繼續向下傳遞。  
  

public boolean onInterceptTouchEvent(MotionEvent ev)

  該方法存在於ViewGroup類中,對於View類並無此方法。表示是否攔截某個事件,ViewGroup如果成功攔截某個事件,那麼這個事件就不在向下進行傳遞。對於同一個事件序列當中,當前View若是成功攔截該事件,那麼對於後面的一系列事件不會再次調用該方法。返回的結果表示是否攔截當前事件,默認返回false。由於一個View它已經處於最底層,它不會存在子控件,所以無該方法。
  

public boolean onTouchEvent(MotionEvent event)

  這個方法被dispatchTouchEvent調用,用來處理事件,對於返回的結果用來表示是否消耗掉當前事件。如果不消耗當前事件的話,那麼對於在同一個事件序列當中,當前View就不會再次接收到事件。

上文部分內容來自《Android開發藝術探索》

代碼實驗

為了驗證和理解實際的運行狀態,重寫View和ViewGroup這些關鍵方法,打印方法調用。

代碼

繼承View重寫方法,加入結果打印。
注:View作為子控件,不存在內部子控件,所以傳入事件就視圖處理,而不存在攔截子控件的事件,所以沒有onInterceptTouchEvent方法

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

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

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


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean result=super.dispatchTouchEvent(ev);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(ev.getAction()));
        return result;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean result=super.onTouchEvent(event);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(event.getAction()));
        return result;
    }
}

繼承ViewGroup重寫方法,加入結果打印。
注:ViewGroup沒有實現onLayout布置控件位置,所以繼承LinearLayout,對分發不影響

public class MyViewGroup extends LinearLayout {
    public MyViewGroup(Context context) {
        super(context);
    }

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean result=super.dispatchTouchEvent(ev);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(ev.getAction()));
        return result;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean result=super.onTouchEvent(event);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(event.getAction()));
        return result;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result=super.onInterceptTouchEvent(ev);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(ev.getAction()));
        return result;
    }

}

最後把這兩個控件加入布局文件就可以了。


        

    

測試

直接運行

首先運行上面的代碼後,可以看到兩個色塊。用手機點擊裡面的MyViewGroup的子控件MyView。

First

相信我,不論怎麼滑動或者點擊都是一樣的結果,下面會分析這樣的情況發生原因。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMyBpZD0="分析">分析: 手指點擊在MyViewGroup中方法onInterceptTouchEvent開始調用,判斷是否攔截這個點擊事件。
ViewGroup2717行代碼,源碼中ViewGroup默認是不攔截事件的:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
因為ViewGroup不攔截點擊事件,事件開始分發,子控件View有機會得到事件,調用內部的兩個方法處理事件,因為我默認沒有做任何處理,View也不會處理事件,返回false。 最後因為Down事件沒有控件響應。如果不消耗當前事件的話,那麼對於在同一個事件序列當中,當前View就不會再次接收到事件。 所以手指在觸摸到屏幕之後滑動,View也就接受不到Move事件。所以手指怎麼滑動都沒有其他的日志結果打印。除非抬起,再按下生成新的事件,又看到同樣的打印結果。

讓子控件響應事件

修改代碼,給View添加點擊事件,也就是使用setOnClickListener簡單的處理。
然後手指點擊迅速抬起,要不然打印結果太多了。
View響應事件

分析:產生點擊事件,子控件onTouchEvent可以處理事件,得到Down事件,同時影響ViewGroup父控件dispatchTouchEvent返回ture可以向下分發,後繼的UP事件也相繼的傳進來。

讓父控件攔截事件

在原基礎上,再次修改代碼,給ViewGroup父控件的onInterceptTouchEvent方法返回true,表示攔截事件。然後給ViewGroup添加點擊監聽回調setOnClickListener

ViewGorup攔截事件

分析:有了上面的基礎,這裡就什麼好說的了,因為父控件攔截了事件,同時能夠響應事件,所有的事件都發送到ViewGroup上。

總結

通過上面的3個實驗的結果,可以大概對ViewGourp的事件分發有個基本的認識。
所以通過抽象源碼提取關鍵實現,可以有下面的大概處理邏輯。

在ViewGroup中有如下邏輯:

 public boolean dispatchTouchEvent(MotionEvent ev) {

        boolean consume =false;//默認不處理
        if (onInterceptTouchEvent(ev)){//首先判斷是否攔截
            consume=onTouchEvent(ev);//看是否能夠處理
        }else {
        //如果不攔截,遍歷子控件,這裡省略掉
        //調用子控件的分發方法,下發事件。
            consume=getChildView.dispatchTouchEvent(ev);
        }
        return consume;
    }

結合職責鏈模式分析

我在上篇博客介紹分析了職責鏈模式,用該模式的思想來分析。

可以畫出這樣的UML圖:
ViewGroupUML圖

當然這是簡略的畫法,實際會復雜得多,而且View和VIewGorup采用設計模式組合模式的思想構造。

Cilent:表示發出請求的對象,在這裡應該對應的是Activity。 ViewHandler:內部以數組的方式持有後繼者,使用的時候采用從最後一位遍歷。dispatchTouchEvent方法表示統一的事件請求方法

View和ViewGourp:都是實際的實現職責類,內部通過調用其他的方法判斷是否能夠處理請求事件。

鏈的構造:
很明顯鏈的構造是在ViewHandler中組裝的,內部鏈的實現方式。onTouchEvent和onInterceptTouchEvent都會影響鏈的構造。動態的生成職責鏈。

ViewGroup/View的事件分發機制總結

最後提出一些結論,給大家一些對事件分發的提示和總結。說不定面試的時候就用上了呢。
提示:ViewGroup在繼承關系上繼承View,所以下文可以用View指代ViewGroup。具體的原因自行Google。

同一事件序列是從手指觸摸屏幕開始算起,手指離開屏幕結束。也就是Down事件開始+不定數目的Move事件+Up事件 正常情況下,一個事件序列只能被一個VIew攔截且消耗。因為一旦某個View攔截了此事件,那麼同一事件序列內的所有事件都會直接交給它處理,所以同一事件序列中事件不能分發給兩個View同時處理。

這個就是我們在處理一些嵌套滑動時候遇到的主要問題。子控件攔截了事件,View對這個滑動事件不想要處理的時候,只能拋棄這個事件,而不會把這些傳給父view去處理。這就是滑動的嵌套的父子控件同方向滑動不流暢的原因。好消息時NestedScrollView的出現很好的解決了這個問題。

某個View一旦決定攔截事件之後,它的onInterceptTouchEvent不會再調用,所以的後繼事件都直接給它處理而不再詢問是否攔截。這在上的打印結果可以得到驗證。 某個View一旦開始處理事件,如果它不消耗Down事件(onTouchEvent返回了false)那麼同一事件序列中的其他事件都不會再交給它處理,並且事件將重新交由父View處理,即父View的onTouchEvent會被調用。也就是一旦事件交給了View而它沒有消耗掉事件,之後的事件序列都不會再分發給它,父控件會開始嘗試處理事件。這點在第一個張實驗結果圖可以得到驗證。 如果View不消耗除Down以外的事件,那麼這個點擊事件會消失,並且父View的onTouchEvent不會調用,並且當前View可以持續收到後續事件,最終這些消失的點擊事件會傳遞給Activity處理。 ViewGroup默認不攔截任何事件,具體請看上文。 真正的View沒有onInterceptTouchEvent方法,一旦有事件發給它,它的onTouchEvent就會調用。 onClick會發生的前提是當前View可點擊,並且它收到了Down事件和Up事件。 事件傳遞過程是由外向內傳遞的。即事件總是先傳給父View,然後由父View決定分發。通過requestDisallowInterceptTouchEvent方法可以在子View中干預父View的事件分發過程,但是Down事件除外。

總結

本文部分內容來自《Android開發藝術探索》,書裡面有具體的源碼分析,感興趣的可以去看。 本文主要以實驗論證部分書中結論,並且提出滑動沖突的一個解決方案使用NestedScrollView,並且Android源碼中很多控件都實現了NestedScrollView方法。 本文還結合職責鏈模式分析View事件分發機制。通過更高層次的抽象分析幫助理解實現原理。結合部分源碼分析,並沒有陷入源碼中不能自拔。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved