王學崗高級UI9、10————事件分發機制

事件分發的流程:
被分發的對象是哪些?被分發的對象是用戶觸摸屏幕而產生的點擊事件,事件主要包括:按下、滑動、擡起和取消。這些事件被封裝成MotionEvent對象。該對象中的主要事件如下:
在這裏插入圖片描述
在這裏插入圖片描述

事件傳遞的順序爲:Activity -> Window ->DecorView(當前界面的底層容器)。一個點擊操作要是沒有被Activity下的任何View處理,即頂層DecorView的dispatchTouchEvent()方法返回false的話,則Activity的onTouchEvent()方法會被調用。
我們下面在源碼中追蹤下。
當我們點擊手機屏幕的時候,硬件會通知軟件,軟件底層程序(C/C++)會調用java層Activity的dispatchTouchEvent(MotionEvent ev)方法。

           public boolean dispatchTouchEvent(MotionEvent ev) {
                 //如果是down,說明是一個新的事件
3398          if (ev.getAction() == MotionEvent.ACTION_DOWN) {
3399              onUserInteraction();
3400          }
               //調用了PhoneWindow的superDispatchTouchEvent()方法,
               //把事件從Activity分發到DecorView
               //如果找不到消費當前事件的View,getWindow().superDispatchTouchEvent(ev)會返回false
3401          if (getWindow().superDispatchTouchEvent(ev)) {
3402              return true;
3403          }
              //返回Activity的onTouchEvent
3404          return onTouchEvent(ev);
3405      }
3406  

我們看下 onUserInteraction();

  public void onContentChanged() {
    }

這是一個空房法,開發者可以實現這個方法,屏幕點擊按下的時候,可以再這裏增加邏輯
getWindow()這裏返回的是一個Window對象。在Window對象中superDispatchTouchEvent(ev)方法是一個抽象方法,在其子類PhoneWindow中實現。我們看下PhoneWindow中superDispatchTouchEvent(ev)方法。


1828      @Override
1829      public boolean superDispatchTouchEvent(MotionEvent event) {
1830          return mDecor.superDispatchTouchEvent(event);
1831      }

mDecor是DecorView對象
我們進入DecorView對象中


439      public boolean superDispatchTouchEvent(MotionEvent event) {
440          return super.dispatchTouchEvent(event);
441      }

DecorView的父類是ViewGroup,所以 super.dispatchTouchEvent(event)調用的是ViewGroup裏的方法。
我們暫且不去管ViewGroup,回過頭來看看Activity的dispatchTouchEvent的返回值onTouchEvent(ev);


3401          if (getWindow().superDispatchTouchEvent(ev)) {
3402              return true;
3403          }

假設這段代碼不執行,即getWindow().superDispatchTouchEvent(ev)返回爲false;
就會執行 onTouchEvent(ev)

 public boolean onTouchEvent(MotionEvent event) {
                  //點擊範圍是否超過了window邊界,比如Dialog類型的Activity,點擊外面dialog消失
3143          if (mWindow.shouldCloseOnTouch(this, event)) {
3144              finish();
                       //表示事件被當前Activity消耗掉了
3145              return true;
3146          }
3147  
3148          return false;
3149      }
3150  

我們現在看下ViewGroup的dispatchTouchEvent(MotionEvent ev);


2541      @Override
2542      public boolean dispatchTouchEvent(MotionEvent ev) {
                //輔助功能,跨進程調用,與我們的事件分發作用不大
2543          if (mInputEventConsistencyVerifier != null) {
2544              mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
2545          }
2546  
2547          // If the event targets the accessibility focused view and this is it, start
2548          // normal event dispatch. Maybe a descendant is what will handle the click.
2549          if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
2550              ev.setTargetAccessibilityFocus(false);
2551          }
2552          //本方法的返回值,表示ViewGroup是否消耗了此事件
2553          boolean handled = false;
                  //檢測該事件是否在安全範圍內
2554          if (onFilterTouchEventForSecurity(ev)) {
2555              final int action = ev.getAction();
2556              final int actionMasked = action & MotionEvent.ACTION_MASK;
2557  
2558              // Handle an initial down.處理最初的按下事件
2559              if (actionMasked == MotionEvent.ACTION_DOWN) {
                         /**
                         當開始一個新的觸摸手勢時,扔掉所有以前的狀態,框架可能由於應用程序的切換,ANR,
                         或其它一些狀態更改而放棄了上一個手勢的UP或cancel事件
                         */
2560                  // Throw away all previous state when starting a new touch gesture.
2561                  // The framework may have dropped the up or cancel event for the previous gesture
2562                  // due to an app switch, ANR, or some other state change.
                          //取消並清除所有觸摸目標
2563                  cancelAndClearTouchTargets(ev);
2564                  resetTouchState();//重置所有觸摸狀態,爲新循環做準備
2565              }
2566  
2567              // Check for interception.事件攔截的檢查
2568              final boolean intercepted;
2569              if (actionMasked == MotionEvent.ACTION_DOWN
2570                      || mFirstTouchTarget != null) {
2571                  final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
2572                  if (!disallowIntercept) {//允許攔截,ViewGroup要消耗這個事件
2573                      intercepted = onInterceptTouchEvent(ev);
2574                      ev.setAction(action); // restore action in case it was changed
2575                  } else {
2576                      intercepted = false;
2577                  }
2578              } else {
2579                  // There are no touch targets and this action is not an initial down
2580                  // so this view group continues to intercept touches.
2581                  intercepted = true;
2582              }
2583  
2584              // If intercepted, start normal event dispatch. Also if there is already
2585              // a view that is handling the gesture, do normal event dispatch.
2586              if (intercepted || mFirstTouchTarget != null) {
2587                  ev.setTargetAccessibilityFocus(false);
2588              }
2589  
2590              // Check for cancelation.
2591              final boolean canceled = resetCancelNextUpFlag(this)
2592                      || actionMasked == MotionEvent.ACTION_CANCEL;
2593  
2594              // Update list of touch targets for pointer down, if needed.
2595              final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
2596              TouchTarget newTouchTarget = null;
2597              boolean alreadyDispatchedToNewTouchTarget = false;
2598              if (!canceled && !intercepted) {
2599  
2600                  // If the event is targeting accessibility focus we give it to the
2601                  // view that has accessibility focus and if it does not handle it
2602                  // we clear the flag and dispatch the event to all children as usual.
2603                  // We are looking up the accessibility focused host to avoid keeping
2604                  // state since these events are very rare.
2605                  View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
2606                          ? findChildWithAccessibilityFocus() : null;
2607  
2608                  if (actionMasked == MotionEvent.ACTION_DOWN
2609                          || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
2610                          || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
2611                      final int actionIndex = ev.getActionIndex(); // always 0 for down
2612                      final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
2613                              : TouchTarget.ALL_POINTER_IDS;
2614  
2615                      // Clean up earlier touch targets for this pointer id in case they
2616                      // have become out of sync.
2617                      removePointersFromTouchTargets(idBitsToAssign);
2618  
2619                      final int childrenCount = mChildrenCount;
2620                      if (newTouchTarget == null && childrenCount != 0) {
2621                          final float x = ev.getX(actionIndex);
2622                          final float y = ev.getY(actionIndex);
2623                          // Find a child that can receive the event.
2624                          // Scan children from front to back.
2625                          final ArrayList<View> preorderedList = buildTouchDispatchChildList();
2626                          final boolean customOrder = preorderedList == null
2627                                  && isChildrenDrawingOrderEnabled();
2628                          final View[] children = mChildren;
2629                          for (int i = childrenCount - 1; i >= 0; i--) {
2630                              final int childIndex = getAndVerifyPreorderedIndex(
2631                                      childrenCount, i, customOrder);
2632                              final View child = getAndVerifyPreorderedView(
2633                                      preorderedList, children, childIndex);
2634  
2635                              // If there is a view that has accessibility focus we want it
2636                              // to get the event first and if not handled we will perform a
2637                              // normal dispatch. We may do a double iteration but this is
2638                              // safer given the timeframe.
2639                              if (childWithAccessibilityFocus != null) {
2640                                  if (childWithAccessibilityFocus != child) {
2641                                      continue;
2642                                  }
2643                                  childWithAccessibilityFocus = null;
2644                                  i = childrenCount - 1;
2645                              }
2646  
2647                              if (!canViewReceivePointerEvents(child)
2648                                      || !isTransformedTouchPointInView(x, y, child, null)) {
2649                                  ev.setTargetAccessibilityFocus(false);
2650                                  continue;
2651                              }
2652  
2653                              newTouchTarget = getTouchTarget(child);
2654                              if (newTouchTarget != null) {
2655                                  // Child is already receiving touch within its bounds.
2656                                  // Give it the new pointer in addition to the ones it is handling.
2657                                  newTouchTarget.pointerIdBits |= idBitsToAssign;
2658                                  break;
2659                              }
2660  
2661                              resetCancelNextUpFlag(child);
2662                              if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
2663                                  // Child wants to receive touch within its bounds.
2664                                  mLastTouchDownTime = ev.getDownTime();
2665                                  if (preorderedList != null) {
2666                                      // childIndex points into presorted list, find original index
2667                                      for (int j = 0; j < childrenCount; j++) {
2668                                          if (children[childIndex] == mChildren[j]) {
2669                                              mLastTouchDownIndex = j;
2670                                              break;
2671                                          }
2672                                      }
2673                                  } else {
2674                                      mLastTouchDownIndex = childIndex;
2675                                  }
2676                                  mLastTouchDownX = ev.getX();
2677                                  mLastTouchDownY = ev.getY();
2678                                  newTouchTarget = addTouchTarget(child, idBitsToAssign);
2679                                  alreadyDispatchedToNewTouchTarget = true;
2680                                  break;
2681                              }
2682  
2683                              // The accessibility focus didn't handle the event, so clear
2684                              // the flag and do a normal dispatch to all children.
2685                              ev.setTargetAccessibilityFocus(false);
2686                          }
2687                          if (preorderedList != null) preorderedList.clear();
2688                      }
2689  
2690                      if (newTouchTarget == null && mFirstTouchTarget != null) {
2691                          // Did not find a child to receive the event.
2692                          // Assign the pointer to the least recently added target.
2693                          newTouchTarget = mFirstTouchTarget;
2694                          while (newTouchTarget.next != null) {
2695                              newTouchTarget = newTouchTarget.next;
2696                          }
2697                          newTouchTarget.pointerIdBits |= idBitsToAssign;
2698                      }
2699                  }
2700              }
2701  
2702              // Dispatch to touch targets.//觸摸目標
2703              if (mFirstTouchTarget == null) {
2704                  // No touch targets so treat this as an ordinary view.沒有觸摸目標,將此視圖視爲普通視圖
2705                  handled = dispatchTransformedTouchEvent(ev, canceled, null,
2706                          TouchTarget.ALL_POINTER_IDS);
2707              } else {
2708                  // Dispatch to touch targets, excluding the new touch target if we already
2709                  // dispatched to it.  Cancel touch targets if necessary.
2710                  TouchTarget predecessor = null;
2711                  TouchTarget target = mFirstTouchTarget;
2712                  while (target != null) {
2713                      final TouchTarget next = target.next;
2714                      if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
2715                          handled = true;
2716                      } else {
2717                          final boolean cancelChild = resetCancelNextUpFlag(target.child)
2718                                  || intercepted;
2719                          if (dispatchTransformedTouchEvent(ev, cancelChild,
2720                                  target.child, target.pointerIdBits)) {
2721                              handled = true;
2722                          }
2723                          if (cancelChild) {
2724                              if (predecessor == null) {
2725                                  mFirstTouchTarget = next;
2726                              } else {
2727                                  predecessor.next = next;
2728                              }
2729                              target.recycle();
2730                              target = next;
2731                              continue;
2732                          }
2733                      }
2734                      predecessor = target;
2735                      target = next;
2736                  }
2737              }
2738  
2739              // Update list of touch targets for pointer up or cancel, if needed.
2740              if (canceled
2741                      || actionMasked == MotionEvent.ACTION_UP
2742                      || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
2743                  resetTouchState();
2744              } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
2745                  final int actionIndex = ev.getActionIndex();
2746                  final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
2747                  removePointersFromTouchTargets(idBitsToRemove);
2748              }
2749          }
2750  
2751          if (!handled && mInputEventConsistencyVerifier != null) {
2752              mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
2753          }
2754          return handled;
2755      }

攔截方法只有ViewGroup有,它有兩種用法,一種是自定義一種容器,重寫

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

一個是在其它類裏面調用該類的

  RelativeLayout relativeLayout = new RelativeLayout(this);
        relativeLayout.requestDisallowInterceptTouchEvent(false);//默認是false

再看下View的這個方法

  public boolean dispatchTouchEvent(MotionEvent event) {
 ……………………………………
12507              if (li != null && li.mOnTouchListener != null
12508                      && (mViewFlags & ENABLED_MASK) == ENABLED
                                   //調用外部設置的監聽
12509                      && li.mOnTouchListener.onTouch(this, event)) {
12510                  result = true;
12511              }
12512              //調用View本身的onTouchEcent
12513              if (!result && onTouchEvent(event)) {
12514                  result = true;
12515              }
12516          }
12517  
12518          if (!result && mInputEventConsistencyVerifier != null) {
12519              mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
12520          }
12521  
12522          // Clean up after nested scrolls if this is the end of a gesture;
12523          // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
12524          // of the gesture.
12525          if (actionMasked == MotionEvent.ACTION_UP ||
12526                  actionMasked == MotionEvent.ACTION_CANCEL ||
12527                  (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
12528              stopNestedScroll();
12529          }
12530  
12531          return result;
12532      }

這個mOnTouchListener 是我們自己設置的監聽,就是下面這個方法

  RelativeLayout relativeLayout = new RelativeLayout(this);
        relativeLayout.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                return false;
            }
        });

如果沒有設置這個監聽,會調用它自己的onTouchEvent方法。


12513              if (!result && onTouchEvent(event)) {
12514                  result = true;
12515              }
12516          }

我們看下onTouchEvent(event)方法

 public boolean onTouchEvent(MotionEvent event) {
13719          final float x = event.getX();
13720          final float y = event.getY();
13721          final int viewFlags = mViewFlags;
13722          final int action = event.getAction();
13723  
13724          
13742  
13743          if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
13744              switch (action) {
13745                  case MotionEvent.ACTION_UP:
13746                     
13757                  
13770                     
13799                                  
13820  
13821                    
13843                      break;
13844  
13845                  case MotionEvent.ACTION_CANCEL:
13846                 
13855                      break;
13856  
13857                  case MotionEvent.ACTION_MOVE:
13858                 
13873                      break;
13874              }
13875  
13876              return true;
13877          }
13878  
13879          return false;
13880      }
13881  

在MotionEvent.ACTION_UP裏面,存在我們的onClick事件。在MotionEvent.ACTION_DOWN中,存在我們的長按事件。
我們現在有下面這種情況,幾個控件疊壓在一起,我們點擊它重合的地方,事件是如何處理的呢?
首先會遍歷所有的子View,然後獲取子View的Z值,根據z值進行處理。
我們最後總結下時間分發的流程
在這裏插入圖片描述

1,多指觸控的相關API
在這裏插入圖片描述

在這裏插入圖片描述

2,事件衝突
事件衝突分爲三種:
(1)外部滑動與內部滑動方向不一致,比如外部是上下滑動,內部是左右滑動
    當我們內部控件斜着滑動的時候,外部控件也可能出現滑動的情況。
(2)外部滑動與內部滑動方向一致,外部和內部都是上下或者都是左右滑動
(3)最外部是上下滑動,次外層是左右滑動,最內層又是上下滑動
事件攔截的策略

外部攔截法:所謂外部攔截,是指點擊事件都先經過父容器的攔截處理,如果父容器需要就攔截,這種方法比較符合點擊事件的分發機制。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在該方法內部做相應的攔擊即可。
在這裏插入圖片描述
內部攔截法:
是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交給父容器處理,這種方法和android的事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,跟外部攔截法相比較爲複雜。內部攔截法需要重寫子元素的dispatchTouchEvent方法
在這裏插入圖片描述
網上很多自定義控件會造成滑動衝突,就是因爲她沒使用內部攔截方式去處理滑動事件衝突。以RecyclerView爲例,我們ScrollView嵌套一個RecyclerView,即使我們不作任何處理,RecyclerView也可以很順暢的滑動
我們看下以下佈局

<?xml version="1.0" encoding="utf-8"?>
<com.example.shijianfenfa.ConScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

        <com.example.shijianfenfa.ConScrollView1
            android:id="@+id/viewpager"
            android:layout_width="match_parent"
            android:layout_height="200dp">

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

                <TextView
                    android:id="@+id/text"
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#f00"
                    android:text="TwoScrollView1" />

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#f00"
                    android:text="TwoScrollView1" />

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#f00"
                    android:text="TwoScrollView1" />

            </LinearLayout>
        </com.example.shijianfenfa.ConScrollView1>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rel_view"
            android:layout_width="match_parent"
            android:layout_height="400dp"
            app:layoutManager="LinearLayoutManager"/>

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

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:textColor="#f00"
                android:text="TwoScrollView" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:textColor="#f00"
                android:text="TwoScrollView" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:textColor="#f00"
                android:text="TwoScrollView" />

        </LinearLayout>

    </LinearLayout>
</com.example.shijianfenfa.ConScrollView>

如果我們不作任何處理的話,ConScrollView1(繼承ScrollView)是無法滑動的。我們看下外部攔截法是如何處理的。

package com.example.shijianfenfa;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ScrollView;

import androidx.annotation.RequiresApi;

public class ConScrollView extends ScrollView {

    private float mTouchSlop;
    private float downY;

    public ConScrollView(Context context) {
        super(context);
        init(context);
    }


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

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

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public ConScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {
        //是一個距離,表示滑動的時候,手的移動要大於這個距離纔開始移動控件。如果小於這個距離就不觸發移動控件,
        // 如viewpager就是用這個距離來判斷用戶是否翻頁。
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }
    //外部攔截法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i("zhang_xin", super.onInterceptTouchEvent(ev)+"===>down");//false
                downY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("zhang_xin", super.onInterceptTouchEvent(ev)+"===>move");//不作任何處理的話會是false false……true。移動一段時間後變爲true
                float moveY = getY();
                if(Math.abs(moveY - downY)>mTouchSlop){
                    //外部容器不攔截,交給子控件處理。
                    return false;//如果返回爲true,ConScrollView1無法移動
                }
                break;
            case MotionEvent.ACTION_UP:
                Log.i("zhang_xin", super.onInterceptTouchEvent(ev)+"===>up");//不打印輸出
                break;
        }

        return super.onInterceptTouchEvent(ev);
    }
}

在這裏插入圖片描述
在這裏插入圖片描述
內部攔截法

package com.example.shijianfenfa;

import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ScrollView;
import android.widget.Toast;

import androidx.annotation.RequiresApi;

public class ConScrollView1 extends ScrollView {

    private float mTouchSlop;
    private float downY;

    public ConScrollView1(Context context) {
        super(context);
        init(context);
    }

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

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

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public ConScrollView1(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }
    //內部攔截法

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //請求父view不要攔截事件,保證子View能夠接收到action_move
                downY = ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float moveY = ev.getY();
                if (Math.abs(moveY - downY) > mTouchSlop) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;


        }
        return super.dispatchTouchEvent(ev);
    }


}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章