Android 自定義View學(xué)習(xí)(十三)——View觸控事件學(xué)習(xí)

學(xué)習(xí)資料:

個(gè)人理解:
View的事件體系主要包含兩個(gè)方面:觸控事件和滑動(dòng)事件
觸控事件主要學(xué)習(xí):MotionEvent,事件分發(fā)攔截機(jī)制
滑動(dòng)事件主要學(xué)習(xí):Velocity速度追蹤,GestureDector手勢(shì)檢測(cè),Scorller滑動(dòng)對(duì)象

本篇主要學(xué)習(xí)觸控事件,下篇進(jìn)行學(xué)習(xí)滑動(dòng)事件


1.View的事件分發(fā)攔截 <p>

View的測(cè)量方法學(xué)習(xí)大概了解了UI架構(gòu)圖。

ActivityonCreate()方法中調(diào)用了setContentVie(int myLayoutId),一些列方法回調(diào)之后,布局文件中的各種控件就添加到了ContentView,而ContentView則包含在Activity的根View也就是DecorView

View的觸控事件的整個(gè)過(guò)程可以分為: 事件傳遞,事件攔截,事件處理

View的測(cè)量過(guò)程是從外向內(nèi),由最外層DecorView開(kāi)始,而事件分發(fā)也是由最外層DecorView開(kāi)始。通常情況下最外層DecorView并不做管理,而是直接開(kāi)始考慮Activity


1.1 MotionEvent 觸摸事件 <p>

手指接觸屏幕的一些列事件就封裝在MotionEvent,典型的事件:

  • ACTION_DOWN 手指剛接觸屏幕
  • ACTION_MOVE 手指在屏幕滑動(dòng)
  • ACTION_UP 手指離開(kāi)屏幕

手指觸摸屏幕的坐標(biāo)可以通過(guò)getX/Y()getRawX/Y()方法拿到

getX()拿到的是相對(duì)于自身左上角的x坐標(biāo),getRawX()相對(duì)于屏幕左上角的x坐標(biāo)


1.2 點(diǎn)擊事件的分發(fā)攔截 <p>

點(diǎn)擊事件的分發(fā)有3個(gè)重要的方法:

  • public boolean dispachTouchEvent(MotionEvent event)
    返回結(jié)果表示是否攔截當(dāng)前事件。返回true,攔截;false,不攔截
    事件分發(fā)的第一步,當(dāng)事件傳遞到當(dāng)前View一定會(huì)調(diào)用。返回結(jié)果受此ViewonTouchEvent()方法和下級(jí)childViewdispachTouchEvent影響。雖然是事件分發(fā)第一步,但絕多數(shù)情況不推薦直接修改這個(gè)方法

  • public boolean onIntercepTouchEvent(MotionEvent event)
    返回結(jié)果用來(lái)判斷是否攔截某個(gè)事件。
    如果當(dāng)前view攔截了某個(gè)事件,在同一個(gè)事件的序列中,此方法便不會(huì)被再次調(diào)用

  • public boolean onTouchEvent(MotionEvent event)
    返回結(jié)果表示是否消費(fèi)了事件。true,消費(fèi)了,不用在審核了;false,不消費(fèi),給父容器處理


一段偽碼:

public boolean diapatchTouchEvent(MotionEvent event){
   boolean consume = false;
   //判斷是否攔截
   if(onIntercetTouchEvent(ev)){ //攔截
      consume = onTouchEvent(ev);//消費(fèi)事件
   }else{
      consume = child.dispatchTouchEvent(ev);//chileView開(kāi)始事件分發(fā)
   }
   return consume;  //返回事件攔截結(jié)果 默認(rèn)為false
}

對(duì)于一個(gè)根ViewGroup(A),點(diǎn)擊事件產(chǎn)生后,事件會(huì)先傳遞給A,首先會(huì)調(diào)AdispatchTouchEvent()。
在這個(gè)dispatchTouchEvent()方法內(nèi)部,調(diào)用A.onIenterceptTouchEvvent(),并對(duì)這個(gè)方法的返回值使用if()進(jìn)行判斷:

  • onIenterceptTouchEvvent()方法返回結(jié)果為true時(shí),就表示A要攔截當(dāng)前事件,接著AonTouchEvent()方法就會(huì)被調(diào)用
  • onIenterceptTouchEvvent()方法返回結(jié)果為false時(shí),表示A不攔截當(dāng)前事件,這時(shí)便會(huì)childView調(diào)用事件分發(fā)的第一步dispatchTouchEvent()方法,如此反復(fù),直到事件被消費(fèi)掉

傳遞順序:

Acticty -> Window -> ViewGroup -> View

消費(fèi)順序:

Acticty <- Window <- ViewGroup <- View

注意:
當(dāng)一個(gè)View(V)需要修理一個(gè)事件時(shí),當(dāng)V設(shè)置了onTouchListener()時(shí),onTouchListener()onTouch()就會(huì)被回調(diào)。事件具體會(huì)如何處理,要看onTouch()的返回值

  • onTouch()返回true,可以理解為onTouchListener消費(fèi)了事件,便不會(huì)傳遞給onTouchEvent()
  • onTouch()返回false,可以理解為onTouchListener不消費(fèi)事件,傳遞給onTouchEvent()來(lái)處理

onTouchEvent()方法中,只有V設(shè)置了onClickListener()時(shí),onClick()才會(huì)被回調(diào)

結(jié)論1:onTouchListener()優(yōu)先級(jí)比onTouchEvent()高,onClickListener()優(yōu)先級(jí)比onToucnEvent()低


當(dāng)一個(gè)事件由Activity(A)經(jīng)過(guò)ViewGroup(VG)傳遞到了一個(gè)View(V)時(shí),如果V.onTouchEvent()方法不處理,返回false時(shí),VG也不做處理,VG.onTouchEvent()方法也返回false,這個(gè)事件最終也便交給了A.onTouchEvent()方法來(lái)處理。

分發(fā)攔截方法

截圖來(lái)自GcsSloop安卓自定義View進(jìn)階-事件分發(fā)機(jī)制原理


2.類(lèi)生活舉例 <p>

下面用一些不恰當(dāng)?shù)膶?shí)例來(lái)演示事件分發(fā)攔截的過(guò)程


2.1 情景1 <p>

演示一個(gè)日常:老板派發(fā)工作,給經(jīng)理提出需求,經(jīng)理給組長(zhǎng)分發(fā)任務(wù),組長(zhǎng)再給程序員安排任務(wù)

  • Activity:老板
  • VG_Manager : 經(jīng)理
  • VG_GroupLoader:組長(zhǎng)
  • V_Programmer:程序員

經(jīng)理代碼:

public class VG_Manager extends LinearLayout {
    private final String TAG = "英勇青銅5";
    private Paint mPaint;

    public VG_Manager(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.WHITE);
        mPaint.setTextSize(70f);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText("經(jīng)理",30f,getHeight()-80f,mPaint);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.dispatchTouchEvent ---> 經(jīng)理接到老板發(fā)的任務(wù)通知");
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.onInterceptTouchEvent ---> 經(jīng)理攔截任務(wù),查看任務(wù)通知");
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&VG_Manager.onTouchEvent --->經(jīng)理把自己的任務(wù)做了");
        return super.onTouchEvent(event);
    }
}

組長(zhǎng)的代碼和經(jīng)理幾乎一摸一樣

程序員代碼:

public class V_Programmer extends View {
    private final String TAG = "英勇青銅5";
    private Paint mPaint;
    public V_Programmer(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.WHITE);
        mPaint.setTextSize(70f);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText("程序員",30f,getHeight()-50f,mPaint);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&V_Programmer.dispatchTouchEvent ---> 程序員接到組長(zhǎng)發(fā)的任務(wù)通知");
        return super.dispatchTouchEvent(event);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&V_Programmer.onTouchEvent ---> 程序員把自己的任務(wù)做完");
        return super.onTouchEvent(event);
    }
}

差別就在于View沒(méi)有onInterceptTouchEvent()方法

xml代碼:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.szlk.customview.eventl.VG_Manager
        android:layout_width="270dp"
        android:layout_height="480dp"
        android:background="@color/colorPrimary">

        <com.szlk.customview.eventl.VG_GroupLoader
            android:layout_width="180dp"
            android:layout_height="320dp"
            android:background="@color/colorAccent">

            <com.szlk.customview.eventl.V_Programmer
                android:layout_width="90dp"
                android:layout_height="160dp"
                android:background="@android:color/holo_orange_dark" />
        </com.szlk.customview.eventl.VG_GroupLoader>

    </com.szlk.customview.eventl.VG_Manager>

</RelativeLayout>

運(yùn)行效果也很簡(jiǎn)單

運(yùn)行效果

點(diǎn)擊程序員,查看Log信息

點(diǎn)擊程序員
點(diǎn)擊程序員事件過(guò)程

箭頭的方向大致就是一個(gè)事件的走向


2.2 情景2 <p>

有一天的老板的電腦突然出問(wèn)題了,老板重啟電腦,問(wèn)題沒(méi)有解決,老板于是便找來(lái)了經(jīng)理,經(jīng)理聽(tīng)了老板的描述后,覺(jué)得自己能搞定,于是便自己嘗試解決問(wèn)題,沒(méi)有去找組長(zhǎng)

簡(jiǎn)單修改經(jīng)理的代碼:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.dispatchTouchEvent ---> 被老板喊來(lái)修電腦");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.onInterceptTouchEvent ---> 聽(tīng)了老板描述問(wèn)題后,決定自己先給老板修一下");
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&VG_Manager.onTouchEvent --->經(jīng)理把電腦修好了");
        return super.onTouchEvent(event);
    }

主要是將onInterceptTouchEvent()方法返回值改true,也就是將事件攔截下來(lái)

點(diǎn)擊經(jīng)理,查看Log信息:

經(jīng)理給老板修電腦

經(jīng)理給老板修好電腦

組長(zhǎng)onInterceptTouchEvent()返回true和上圖就類(lèi)似了


上面的情況是假設(shè)經(jīng)理會(huì)修,如果經(jīng)理不會(huì)修,經(jīng)理喊來(lái)了組長(zhǎng),組長(zhǎng)看了老板的電腦后覺(jué)得,必須把程序員同學(xué)喊來(lái)了,于是他們兩個(gè)都沒(méi)有攔截事件,把任務(wù)安排給了程序員同學(xué),程序員同學(xué)到了老板的辦公室,給老板直接重裝了系統(tǒng)。程序員同學(xué)修理好后,覺(jué)得并沒(méi)有必要向組長(zhǎng)和經(jīng)理進(jìn)行匯報(bào),就把這件事給over掉了,也就是V_Programmer.onTouchEvent()返回true

修改代碼:

經(jīng)理代碼:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.dispatchTouchEvent ---> 被老板喊來(lái)修電腦");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_Manager.onInterceptTouchEvent ---> 聽(tīng)了老板描述問(wèn)題后,喊組長(zhǎng)過(guò)來(lái)");
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&VG_Manager.onTouchEvent --->經(jīng)理把組長(zhǎng)喊來(lái),任務(wù)完成");
        return false;
    }

組長(zhǎng)代碼:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_GroupLoader.dispatchTouchEvent ---> 組長(zhǎng)被經(jīng)理喊來(lái)給老板修電腦");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "&&&VG_GroupLoader.onInterceptTouchEvent ---> 組長(zhǎng)覺(jué)得老板的電腦問(wèn)題太大,喊來(lái)程序員");
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&VG_GroupLoader.onTouchEvent ---> 組長(zhǎng)安排給程序員,任務(wù)完成");
        Log.e(TAG, "&&&VG_GroupLoader.onTouchEvent ---> 默認(rèn)"+super.onTouchEvent(event));
        return false;
    }
    

程序員的代碼:

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&V_Programmer.dispatchTouchEvent ---> 程序員被喊來(lái)給老板修電腦");
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "&&&V_Programmer.onTouchEvent ---> 程序員給老板重裝系統(tǒng),修好,獎(jiǎng)金15元");
        return true;
    }

再次點(diǎn)擊程序員,Log信息

程序員修電腦

這里我出了點(diǎn)問(wèn)題,實(shí)際打印結(jié)果將上面的Log信息完整重復(fù)打印了3遍,有時(shí)候4遍,我查找了一會(huì)也沒(méi)查出來(lái)為啥,有知道我哪里出錯(cuò)的同學(xué),請(qǐng)告訴我 : )

2016.11.09補(bǔ)充
打印3,4次的原因知道了,因?yàn)闆](méi)有對(duì)事件類(lèi)型進(jìn)行判斷,MotionEvent中并不是只有DOWN一個(gè)事件,加上類(lèi)型判斷就會(huì)只打印一次了

此時(shí)的事件流程圖

程序員給老板修電腦

3.一些結(jié)論 <p>

這些結(jié)論摘自Android開(kāi)發(fā)藝術(shù)探索

  • 同一個(gè)序列從手指落在屏幕開(kāi)始,以down事件開(kāi)始,中間數(shù)量不定的move事件,最終以up事件結(jié)束,整個(gè)過(guò)程都是一個(gè)事件
  • 一般,一個(gè)事件只能由一個(gè)View消費(fèi)
  • 一個(gè)View(V)對(duì)事件進(jìn)行了攔截,該事件只能由這個(gè)V來(lái)消費(fèi)
  • 某個(gè)View(V)(不是ViewGroup)一旦開(kāi)始處理事件,如果它不消費(fèi)ACTION_DOWN事件,也就是onTouchEvent()返回false,那么同一事件序列的其他事件都不會(huì)交給這個(gè)V消費(fèi),并且事件將重新交給V的父容器的onTouchEvent()進(jìn)行消費(fèi)
  • 某個(gè)View(V)不消耗除ACTION_DOWN以外的其他事件,這個(gè)點(diǎn)擊事件便會(huì)消失,此時(shí)父元素的onTouchEvent()不會(huì)被調(diào)用,并且V可以持續(xù)收到后續(xù)事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給Activity處理。ps:這條并不理解到底啥意思
  • ViewGroup(VG)默認(rèn)不會(huì)攔截任何事件,VG.onInterceptTouchEvent()默認(rèn)返回false
  • View(V)沒(méi)有onIntercepTouchEvent(),一旦點(diǎn)擊事件傳遞給V,V.onTouchEvent()便會(huì)消費(fèi)這個(gè)事件
  • View(V)onTouchEvent()默認(rèn)都會(huì)消耗事件,返回true(ps:這里有疑問(wèn),我測(cè)試返回為false)。除非V是不可不可點(diǎn)擊的(clickablelongClickable同時(shí)為false)。V的的longClickable默認(rèn)都為false,clickable要看控件,ButtonclickabletrueTextViewfalse。
  • View(V)enable屬性不影響onTouchEvent的默認(rèn)返回值。哪怕Vdisable狀態(tài),只要Vclickable或者longClickable有一個(gè)返回為true,VonTouchEvent就返回true
  • onClicck進(jìn)行回調(diào)前提是View是可以點(diǎn)擊的,并且收到了downup事件
  • 事件的傳遞是由外向內(nèi)的,事件總是先傳遞給父容器,然后父容器向下傳遞。通過(guò)requestDisallowInterceptTouchEvent方法,可以在childView中干預(yù)父容器事件的分發(fā)過(guò)程,但ACTION_DOWN事件除外

這里后面幾條并不是很理解。事件分發(fā)的源碼,也就在Android開(kāi)發(fā)藝術(shù)探索中大體看了看


4.最后 <p>

又是十一,記得去年十一找同學(xué)開(kāi)始學(xué)習(xí)Android做一些小的Demo,經(jīng)過(guò)一年的學(xué)習(xí),感覺(jué)也算入門(mén)了 : )

國(guó)慶快樂(lè)

本人很菜,有錯(cuò)誤,請(qǐng)指出

共勉 : )

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容