事件分發機制全解析

目錄介紹

  • 1.事件分發流程圖(究極重點)
  • 2.在遍歷子View時如何從內層的子View開始遍歷?
  • 3.滑動衝突有哪些場景?滑動衝突處理原則是什麼?
  • 4.ACTION_POINTER_DOWN ,event.getX(int index)何時發生?
  • 5.View的滑動方式有哪些?
  • 6.ScrollView裏面有一個button,而後按住button向上滑,講述事件傳遞過程?
  • 7.按住一個button,而後手指移到別處,click事件還能不能響應?

事件分發機制雖然你們都知道是什麼東西,但有可能其中的一些細節重點要點仍是不清晰,本文將結合實例帶你攻克事件分發。

1. 事件分發流程圖(究極重點)

  1. 通常來講,一組事件序列爲ACTION_DOWN(一個,手指點下)->ACTION_MOVE(N個,手指移動)->ACTION_UP(一個,手指擡起),必須以DOWN事件開始,UP事件結束
  2. 當一個View消費事件後,後續的事件都直接交由它去處理但有兩種情形須要注意:
    1. ViewGroup進行了攔截,後續事件將交由ViewGroup的onTouchEvent去處理。
    2. 能夠在處理事件的View的onTouchEvent()中手動的去調用其餘View的onTouchEvent()將事件強行傳遞給其餘View處理,但這樣違背了事件分發的本質。
  • (這裏指的消費事件其實並非onTouchEvent返回true而是ACTION_DOWN事件時是否返回true,當一個View消費事件後後續的MOVE和UP事件都交由當前這個消費了事件的View去處理。)
  1. onInterceptTouchEvent在DOWN事件和MOVE事件返回true進行攔截實際上是很是沒有必要的,若是在DOWN攔截那麼後續事件都不會交由子View去判斷,在UP事件攔截那麼消費了事件的子View的UP事件將沒法進行響應。
  2. 若是onInterceptTouchEvent在MOVE事件返回true的話那麼首先會發送一個ACTION_CANCEL事件給原先處理事件的View,以後後續的MOVE和UP事件將直接發送給其自身的onTouchEvent去處理且其自身的onInterceptTouchEvent將不會被調用。(也就是說onInterceptTouchEvent一旦返回true那麼以後的事件將不會在觸發其自身的onInterceptTouchEvent方法,onInterceptTouchEvent在返回true之後將再也不調用)。
  3. View的onTouchEvent默認消耗事件(ACTION_DOWN返回true),除非它是不可點擊的(clickable和longClickable同時爲false),若是View設置了onClickListener則clickable爲true,設置了onLongClickListener則longClickable爲true。View默認longClickable都爲false,clickable非狀況(如Button爲true,TextView爲false)。
  • View是否消耗事件順序:onTouch(setOnTouchListener)->onTouchEvent->setOnClickListener->setOnLongClickListener。
  1. View的enable不影響onTouchEvent的返回值,View當enable爲false時只要clickable爲true照樣能夠消費事件只不過ACTION_UP時不會有任何響應。

2.在遍歷子View時如何從內層的子View開始遍歷?

能夠經過重寫getChildDrawingOrder方法去改變遍歷規則。java

3.滑動衝突有哪些場景?滑動衝突處理原則是什麼?

滑動衝突的本質實際上是一個策略問題,在開發中咱們一般都是經過在子View中去調用requestDisallowInterceptTouchEvent方法配合父View中的onInterceptTouchEvent方法去使用。android

下邊給出一個例子:git

public class MyLayout extends LinearLayout{
    public MyLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:   //表示父類須要攔截
                    return true;
            default:
                break;
        }
        return false;    //若是設置攔截,除了down,其餘都是父類處理
    }
}
複製代碼
public class MyButton extends Button {
    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);//禁用父View的攔截方法。
                break;
            case MotionEvent.ACTION_MOVE:
                if(知足條件){
                getParent().requestDisallowInterceptTouchEvent(false);//解除父View的攔截的禁用。
                }
        }
        return true;
    }
}
複製代碼
  • 能夠看到當 ACTION_DOWN 事件時咱們在View自身的onTouchEvent中調用了 getParent().requestDisallowInterceptTouchEvent(true)這個方法,當此方法調用後在方法內部中會改變FLAG_DISALLOW_INTERCEPT標誌位爲true,這時在ViewGroup中的dispatchTouchEvent中若是檢測到FLAG_DISALLOW_INTERCEPT爲true的話將跳過onInterceptTouchEvent的調用而直接返回false,也就是父View直接不進行攔截,這時咱們的事件都將由子View去處理,同時也不用擔憂父View的攔截方法會對事件進行攔截,當咱們在移動時知足事件可被父View進行攔截時則須要調用getParent().requestDisallowInterceptTouchEvent(false)將父View的攔截方法的禁用解除掉,這時父View的onInterceptTouchEvent將可繼續去判斷是否須要進行事件的攔截。

在ViewGroup的dispatchTouchEvent中咱們能夠看到以下代碼:github

// 發生ACTION_DOWN事件或者已經發生過ACTION_DOWN,而且將mFirstTouchTarget賦值,才進入此區域,主要功能是攔截器
final boolean intercepted;
//onInterceptTouchEvent返回true後以後將再也不執行onInterceptTouchEvent方法,由於其將mFirstTouchTarget字段置爲了null。
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
    //disallowIntercept:是否禁用事件攔截的功能(默認是false),即不由用
    //能夠在子View經過調用requestDisallowInterceptTouchEvent方法對這個值進行修改,不讓該View攔截事件
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    //默認狀況下會進入該方法
    if (!disallowIntercept) {
        //調用攔截方法
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action);
    } else {
        intercepted = false;
    }
} else {
    // 當沒有觸摸targets,且不是down事件時,開始持續攔截觸摸。
    intercepted = true;
}
複製代碼

4.ACTION_POINTER_DOWN ,event.getX(int index)何時發生?

獲取事件時需調用MotionEvent.getActionMasked()而不是MotionEvent.getAction(),只有MotionEvent.getActionMasked()能夠支持多點觸控。bash

常見值:ide

  • ACTION_DOWN :第一個手指按下(以前沒有任何手指觸摸到 View)
  • ACTION_UP :最後一個手指擡起(擡起以後沒有任何?指觸摸到 View,這個手指未必是 ACTION_DOWN 的那個手指)
  • ACTION_MOVE 有手指發生移動
  • ACTION_POINTER_DOWN 額外手指按下(按下以前已經有別的手指觸摸到 View)
  • ACTION_POINTER_UP 有手指擡起,但不是最後一個(擡起以後,仍然還有別的手指在觸摸着 View)

默認的event.getX()其實能夠理解爲 event.getX(0),這是針對於一根手指的狀況,再多點觸控的狀況下咱們須要經過調用event.getX(int index)來傳入參數以區別當前是第幾根手指在進行移動 (這裏的index是會變的,可是手指的ID是不會變的,咱們須要經過ID找到對應手指的index)佈局

多點觸控通常寫法實例: github.com/rengwuxian/…動畫

5.View的滑動方式有哪些?

大體可分爲下邊三個方法(只有layout方法是能夠真正改變View座標位置)ui

1.layout:

對View進行從新佈局定位。在onTouchEvent()方法中得到控件滑動先後的偏移。而後經過layout方法從新設置。this

// 視圖座標方式
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 記錄觸摸點座標
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                // 計算偏移量
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                // 在當前left、top、right、bottom的基礎上加上偏移量
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
//                        offsetLeftAndRight(offsetX);
//                        offsetTopAndBottom(offsetY);
                break;
        }
        return true;
    }
複製代碼

2.ScrollTo/ScrollBy:

本質是View內容的移動,須要經過父容器的該方法來滑動當前View,Scroller: 平滑滑動,經過重載computeScroll(),使用scrollTo/scrollBy完成滑動效果,Scroller只是一個移動的機制,真正仍是須要調用去scrollTo/scrollBy去進行移動。

Scroll中與之相關的各類API中的參數都要跟實際咱們認知相反,好比想往自身右邊移動100不是去調用scrollerBy(100,0)而是調用scrollerBy(-100,0)。

public class ScrollButton extends android.support.v7.widget.AppCompatButton {
    Scroller scroller;
    int direction = -1;

    public ScrollButton(Context context) {

        this(context,null);
    }

    public ScrollButton(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public ScrollButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        scroller = new Scroller(context);
    }

    @Override
    public void computeScroll() {
        if(scroller!=null){
            if(scroller.computeScrollOffset()){//判斷scroll是否完成
                ((View) getParent()).scrollTo(
                        scroller.getCurrX(),scroller.getCurrY()
                );//執行本段位移

                invalidate();//進行下段位移
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                scroller.startScroll(((int) getX()), ((int) getY()), ((int) getX())*direction,
                        ((int) getY())*direction);//開始位移,真正開始是在下面的invalidate
                direction*=-1;//改變方向
                invalidate();//開始執行位移
                break;
        }
        return super.onTouchEvent(event);
    }
}
複製代碼

3.屬性動畫:

動畫對View進行滑動: setTranslationX,setTranslationY。

6.ScrollView裏面有一個button,而後按住button向上滑,講述事件傳遞過程?

這裏的情形能夠理解爲上圖的情景,你們能夠自行帶入場景。

當手指按下時因爲scrollView中onInterceptTouchEvent沒對down事件進行攔截同時button的onTouchEvent是默認返回true的(clickable=true)那麼button首先會消耗down事件,當咱們手指移動時會觸發MOVE事件,這時ScrollView的攔截事件將進行攔截(onInterceptTouchEvent在MOVE時返回true)同時會發送 CANCLE事件給button(CANCLE的觸發時機是父View進行攔截後會發送給原先處理事件的子View通知它不要處理後續事件了),以後的MOVE和UP事件將直接交由ScrollView的onTouchEvent去處理同時其自身的onInterceptTouchEvent不會再被觸發(onInterceptTouchEvent返回true後將不被調用)。

7.按住一個button,而後手指移到別處,click事件還能不能響應?

不能響應。

當手指移動時在View的OnTouchEvent的MOVE事件中會不斷檢測當前手指是否在View區域內,若是出了View區域的話那麼會將mPressed這個標誌位置爲false,當手指擡起時在UP事件中若是mPressed爲false的話將不會觸發任何響應(必定要注意的是會觸發MOVE和UP事件,由於一個View在DOWN事件返回true後後續的事件序列都會交給其去處理,只不過在這種狀況下沒有任何響應效果)。

View的onTouchEvent:

public boolean onTouchEvent(MotionEvent event) {
   ....
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                //若是mPrivateFlags爲false則prepressed爲false,將不會執行後續UP事件中的任何邏輯
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                ....
                }
                break;
            ...
            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);
                              //判斷手指是否在View的區域中
                if (!pointInView(x, y, mTouchSlop)) {
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        removeLongPressCallback();
                        //若是手指移出View區域將改變mPrivateFlags
                        setPressed(false);
                    }
                }
                break;
        }
        return true;
    }
    return false;
}
複製代碼
相關文章
相關標籤/搜索