1. 程式人生 > >Android ViewGroup 觸控事件傳遞機制

Android ViewGroup 觸控事件傳遞機制

引言

上一篇部落格我們學習了Android View 觸控事件傳遞機制,不瞭解的同學可以檢視Android View 觸控事件傳遞機制。今天繼續學習Android觸控事件傳遞機制,這篇部落格將和大家一起探討ViewGroup的觸控事件傳遞機制。

示例

示例程式碼如下:

public class MainActivity extends ActionBarActivity {
    private String TAG = "MainActivity";
    private MyViewGroup parentView;
    private Button childView;

    @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); parentView = (MyViewGroup) findViewById(R.id.parent); childView = (Button) findViewById(R.id.child); childView.setOnClickListener(new
View.OnClickListener() { @Override public void onClick(View v) { Log.e(TAG, "childView=====onClick"); } }); parentView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.e(TAG, "parentView=====onTouch"
); return false; } }); parentView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.e(TAG, "parentView=====onClick"); } }); } }

自定義MyViewGroup,並且重寫dispatchTouchEvent方法新增列印日誌,重寫onInterceptTouchEvent方法新增列印日誌:

public class MyViewGroup extends LinearLayout {
    private String TAG = "MyViewGroup";

    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) {
        Log.e(TAG, "MyViewGroup=====dispatchTouchEvent "+ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "MyViewGroup=====onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }
}

佈局如下:

<com.xjp.viewgrouptouchdemo.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/child"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:background="@drawable/image1" />

</com.xjp.viewgrouptouchdemo.MyViewGroup>

分別點選Button按鈕和空白區域,列印結果如下:

08-01 17:02:56.792  14706-14706/com.xjp.viewgrouptouchdemo E/MyViewGroup﹕ MyViewGroup=====dispatchTouchEvent
08-01 17:02:56.792  14706-14706/com.xjp.viewgrouptouchdemo E/MainActivity﹕ childView=====onClick
08-01 17:03:31.046  14706-14706/com.xjp.viewgrouptouchdemo E/MyViewGroup﹕ MyViewGroup=====dispatchTouchEvent
08-01 17:03:31.046  14706-14706/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onTouch1
08-01 17:03:31.046  14706-14706/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onClick

從上面列印可以看出,在ViewGroup巢狀Button佈局中,僅僅點選Button按鈕時只會執行Button的觸控事件,不會執行ViewGroup的觸控事件。當你點選Button以外的空白區域時,才會執行ViewGroup的觸控事件。那為什麼在ViewGroup巢狀View時只會執行View的觸控事件而不執行ViewGroup的觸控事件呢?待著這個疑問,我們來分析下ViewGroup原始碼中的dispatchTouchEvent方法。為了方便起見,我這裡都是分析的Android2.0的原始碼。

ViewGroup#dispatchTouchEvent

@Override
     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) {
                 // this is weird, we got a pen down, but we thought it was
                 // already down!
                 // XXX: We should probably send an ACTION_UP to the current
                 // target.
                 mMotionTarget = null;
             }
             // If we're disallowing intercept or if we're allowing and we didn't
             // intercept
             if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                 // reset this event's action (just to protect ourselves)
                 ev.setAction(MotionEvent.ACTION_DOWN);
                 // We know we want to dispatch the event down, find a child
                 // who can handle it, start with the front-most child.
                 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)) {
                             // offset the event to the view's coordinate system
                             final float xc = scrolledXFloat - child.mLeft;
                             final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                             if (child.dispatchTouchEvent(ev))  {
                                 // Event handled, we have a target now.
                                mMotionTarget = child;
                                 return true;
                            }
                             // The event didn't get handled, try the next view.
                             // Don't reset the event's location, it's not
                             // necessary here.
                        }
                     }
                 }
             }
         }

         boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                 (action == MotionEvent.ACTION_CANCEL);

         if (isUpOrCancel) {
             // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
             mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
         }

         // The event wasn't an ACTION_DOWN, dispatch it to our target if
         // we have one.
         final View target = mMotionTarget;
         if (target == null) {
             // We don't have a target, this means we're handling the
            // event as a regular view.
             ev.setLocation(xf, yf);
            return super.dispatchTouchEvent(ev);
         }

         // if have a target, see if we're allowed to and want to intercept its
         // events
         if (!disallowIntercept && onInterceptTouchEvent(ev)) {
             final float xc = scrolledXFloat - (float) target.mLeft;
             final float yc = scrolledYFloat - (float) target.mTop;
             ev.setAction(MotionEvent.ACTION_CANCEL);
             ev.setLocation(xc, yc);
             if (!target.dispatchTouchEvent(ev)) {
                 // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
             }
            // clear the target
            mMotionTarget = null;
             // Don't dispatch this event to our own view, because we already
             // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
         }

         if (isUpOrCancel) {
             mMotionTarget = null;
         }

         // finally offset the event to the target's coordinate system and
         // dispatch the event.
         final float xc = scrolledXFloat - (float) target.mLeft;
         final float yc = scrolledYFloat - (float) target.mTop;
         ev.setLocation(xc, yc);

         return target.dispatchTouchEvent(ev);
     }

分析:
1. 程式碼3-8行,獲取當前手指在螢幕上觸控點選的座標位置,用於判斷當前手指觸控點選的是View區域還是ViewGroup區域。
2. 程式碼第10行,獲得disallowIntercept的值,disallowIntercept指的是是否禁用掉事件攔截功能,預設值是false,你可以呼叫requestDisallowInterceptTouchEvent方法修改它。
3. 程式碼第12行,手指觸控手勢是先ACTION_DOWN操作,所以條件滿足,進入if條件。
4. 程式碼第13-19行,清除當前手機螢幕上觸控點選物件,也就是將mMotionTarget設定為null。意思是在點選手機螢幕之前是沒有任何觸控點選物件的。
5. 程式碼第22行,由於disallowIntercept預設值是false,所以條件是否滿足完全取決於方法onInterceptTouchEvent返回值取反。而我們進入該方法會發現裡面的實現僅僅是返回一個false。也就是if條件滿足。
6. 程式碼第31行,通過一個for迴圈遍歷當前ViewGroup下所以子View。
7. 程式碼第35行,獲取遍歷子View在螢幕上的座標位置,然後程式碼第36行,判斷當前螢幕手指觸控點選座標是否包含遍歷的子View在螢幕上的座標位置範圍?如果包含,者表示當前手指觸控點選的地方是該子View,也就是點選了Button。否則表示當前手指觸控並沒有點選到ViewGroup中的子View,也就是點選到了空白區域。
8. 程式碼第41行,呼叫子View的dispatchTouchEvent方法來處理View的觸控事件分發,這裡一步就是我們上一篇部落格分析的 Android View 觸控事件傳遞機制入口。在這篇部落格中我們知道,當View是可點選的或者長安點選或者設定了setOnClickListener點選監聽事件的,View#dispatchTouchEvent方法一律返回true,否則返回false。所以當條件滿足,也就是子View設定了點選事件時ViewGroup#dispatchTouchEvent方法返回true,觸控物件mMotionTarget = child賦值成當前點選的子Viwe執行結束。因此這也驗證了上面示例程式碼,當button設定了點選事件時只執行了Button的onClick事件,並沒有執行任何關於ViewGroup的觸控點選事件。
9. 程式碼第66-72行,假如上面的View#dispatchTouchEvent方法返回false,表示子View不可點選(可以參考上一篇部落格),此時mMotionTarget依然為null,那麼target==null條件滿足。執行父類的dispatchTouchEvnet方法,也就是View的dispatchTouchEvent方法。由於ViewGroup的父類是View,所以此處表示執行了ViewGroup的dispatchTouchEvent方法。言外之意就是,當ViewGroup巢狀的子View不可點選且沒有設定setOnClickListener點選監聽事件時,點選View先觸發子View的觸控事件,然後在觸發ViewGroup的觸控事件,執行了ViewGroup#dispatchTouchEvent方法,並且返回了,後面程式碼不執行。
10. 程式碼第76-103行,主要是執行子View的ACTION_UP和ACTION_CANCEL手勢操作的,邏輯這裡就不具體分析了,可以參考上一篇部落格。

總結:有上面分析我們知道。

  • onInterceptTouchEvent方法是用於ViewGroup對子View的觸控事件攔截功能,預設返回false,不攔截子View的觸控事件,可以重寫該方法,返回true來攔截子View的觸控事件傳遞。此時只會執行ViewGroup的觸控事件傳遞。
  • 當子View是不可點選的且沒有設定setOnClickListener點選監聽事件時,會先執行子View的觸控事件,然後在執行ViewGroup的觸控事件。

現在倆驗證以上兩個結論。

onInterceptTouchEvent返回true

修改MyViewGroup程式碼

public class MyViewGroup extends LinearLayout {
    private String TAG = "MyViewGroup";

    public MyViewGroup(Context context) {
        super(context);
        requestDisallowInterceptTouchEvent(false);
    }

    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) {
        Log.e(TAG, "MyViewGroup=====dispatchTouchEvent ");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

點選Button按鈕,列印結果如下:

08-01 18:36:56.815  29910-29910/com.xjp.viewgrouptouchdemo E/MyViewGroup﹕ MyViewGroup=====dispatchTouchEvent
08-01 18:36:56.825  29910-29910/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onTouch1
08-01 18:36:56.825  29910-29910/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onClick

有列印可以看出,當重寫onInterceptTouchEvent方法返回true時,是不會執行Button的觸控點選事件的。也正好驗證了前面的結論:當ViewGroup重寫onInterceptTouchEvent方法返回true時,也就是攔截子View的觸控事件傳遞,此時只會執行ViewGroup的觸控事件。

子View不可點選且沒設定setOnClickListener

程式碼修改如下:

public class MainActivity extends ActionBarActivity {
    private String TAG = "MainActivity";
    private MyViewGroup parentView;
    private ImageView childView;

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

        parentView = (MyViewGroup) findViewById(R.id.parent);
        childView = (ImageView) findViewById(R.id.child);
        childView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "childView=====onTouch");
                return false;
            }
        });
        parentView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(TAG, "parentView=====onTouch" +event.getAction());
                return false;
            }
        });
        parentView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, "parentView=====onClick");
            }
        });

    }
}

將Button換成ImageView,並且不設定setOnClickListener事件。點選ImageView列印日誌如下:

08-01 18:52:44.720  31219-31219/com.xjp.viewgrouptouchdemo E/MyViewGroup﹕ MyViewGroup=====dispatchTouchEvent
08-01 18:52:44.720  31219-31219/com.xjp.viewgrouptouchdemo E/MainActivity﹕ childView=====onTouch
08-01 18:52:44.730  31219-31219/com.xjp.viewgrouptouchdemo E/MainActivity﹕ parentView=====onTouch0
parentView=====onClick

有列印可以看出,即執行了子View ImageView的觸控事件,也執行了ViewGroup的觸控事件。由於ImageView預設情況是不可點選的,因此:當子View不可點選或者麼有設定setOnClickListener點選事件時,點選子View是先執行View的觸控事件,然後在執行ViewGroup的觸控事件的。這也驗證了ViewGroup#dispatchTouchEvent小節的第9點。

最後附帶上一幅ViewGroup觸控事件傳遞流程圖

這裡寫圖片描述

總結

  1. 可以在ViewGroup裡重寫onInterceptTouchEvent方法來決定是否攔截子View的傳遞事件,系統預設返回false,表示不攔截子View的事件分發傳遞;如果重寫返回true,表示攔截子View的觸控事件。
  2. 當子View是可點選的或者設定了setOnClickListener點選事件時,android觸控事件分發是不會傳遞到ViweGroup的,也就是隻會執行View的觸控事件,不會執行ViewGroup的觸控事件。
  3. 當子View不可點選且沒有設定setOnClickListener點選事件時,Android觸控事件是先分發到View,View先執行dispatchTouchEvent觸控事件,然後在傳遞到ViewGroup,ViewGroup執行dispatchTouchEvent觸控事件。
  4. Android事件分發先傳遞到View,在由View決定是否傳遞到ViewGroup。