Android面試必問!View 事件分發機制,看這一篇就夠了!

Button123發表於2021-05-24

在 Android 開發當中,View 的事件分發機制是一塊很重要的知識。不僅在開發當中經常需要用到,面試的時候也經常被問到。

如果你在面試的時候,能把這塊講清楚,對於校招生或者實習生來說,算是一塊不錯的加分項。對於工作幾年的我們來說,這是必須掌握的,講不明白,那你回去等通知吧,哈哈。

目錄大概如下:

  1. View 事件分發機制簡介
  2. View 常見滑動衝突解決
  3. View 雙擊,多擊事件是怎麼實現的
  4. 手勢識別
  5. 小結

View 事件分發機制簡介

View 觸控事件

對於螢幕的點選,滑動,抬起等一系的動作,其實都是由一個一個MotionEvent物件組成的。根據不同動作,主要有以下三種事件型別:

1.ACTION_DOWN:手指剛接觸螢幕,按下去的那一瞬間產生該事件 2.ACTION_MOVE:手指在螢幕上移動時候產生該事件 3.ACTION_UP:手指從螢幕上鬆開的瞬間產生該事件 4.ACTION_CANCEL 當前 View 的手勢被打斷,後續不會再收到任何事件

從 ACTION_DOWN 開始到 ACTION_UP/ACTION_CANCEL 結束我們稱為一個事件序列

正常情況下,無論你手指在螢幕上有多麼騷的操作,最終呈現在 MotionEvent 上來講無外乎下面 3 種 case。

  1. 點選後抬起,也就是單擊操作:ACTION_DOWN -> ACTION_UP
  2. 點選後再風騷的滑動一段距離,再抬起:ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
  3. 某些情況下,我們可能會沒有收到 ACTION_UP 事件,是收到 ACTION_CANCEL 事件。

ACTION_CANCEL 一般是指 ChildView 原先擁有事件處理權,後面由於某些原因,該處理權需要交回給上層去處理,ChildView便會收到 ACTION_CANCEL 事件。對於一些復位或者重置操作,我們應該在 ACTION_UP 和 ACTION_CANCEL 裡面同時進行處理

程式碼邏輯上是:上層判斷之前交給ChildView的事件處理權需要收回來了,便會做事件的攔截處理,攔截時給ChildView發一個ACTION_CANCEL事件

幾個主要方法

我們知道,View 的事件分發機制主要涉及到以下幾個方法

  • dispatchTouchEvent ,這個方法主要是用來分發事件的
  • onInterceptTouchEvent,這個方法主要是用來攔截事件的(需要注意的是 ViewGroup 才有這個方法,View 沒有 onInterceptTouchEvent 這個方法)
  • onTouchEvent 這個方法主要是用來處理事件的
  • requestDisallowInterceptTouchEvent(true),這個方法能夠影響父View是否攔截事件,true 表示父 View 不攔截事件,false 表示父 View 攔截事件

我們先來看一張圖。

以下內容參考圖解 Android 事件分發機制這一篇部落格

  • 仔細看的話,圖分為3層,從上往下依次是Activity、ViewGroup、View
  • 事件從左上角那個白色箭頭開始,由 Activity 的 dispatchTouchEvent 進行分發
  • 箭頭的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是呼叫父類實現。)
  • dispatchTouchEvent和 onTouchEvent的框裡有個【true---->消費】的字,表示的意思是如果方法返回true,那麼代表事件就此消費,不會繼續往別的地方傳了,事件終止。
  • 目前所有的圖的事件是針對ACTION_DOWN的,對於ACTION_MOVE和ACTION_UP我們最後做分析。

當觸控事件發生時,首先 Activity 將 TouchEvent 傳遞給最頂層的 View,TouchEvent最先到達最頂層 view 的 dispatchTouchEvent ,然後由 dispatchTouchEvent 方法進行分發,

如果dispatchTouchEvent返回true 消費事件,事件終結。

如果dispatchTouchEvent返回 false ,則回傳給父View的onTouchEvent事件處理;

如果dispatchTouchEvent返回super的話,預設會呼叫自己的onInterceptTouchEvent方法。

  • 預設的情況下onInterceptTouchEvent回撥用super方法,super方法預設返回false,所以會交給子View的onDispatchTouchEvent方法處理
  • 如果 interceptTouchEvent 返回 true ,也就是攔截掉了,則交給它的 onTouchEvent 來處理,
  • 如果 interceptTouchEvent 返回 false ,那麼就傳遞給子 view ,由子 view 的 dispatchTouchEvent 再來開始這個事件的分發。

關於更多詳細分析,請檢視原部落格圖解 Android 事件分發機制,真心推薦,寫得很好。


View 滑動事件衝突

在開發當中,View 的滑動衝突時經常遇到的,比如 ViewPager 巢狀 ViewPager,ScrollView 巢狀 ViewPager。下面讓我們一起來看看怎麼解決。

常見的三種情況

第一種情況,滑動方向不同

第二種情況,滑動方向相同

第三種情況,上述兩種情況的巢狀

解決思路

看了上面三種情況,我們知道他們的共同特點是父View 和子View都想爭著響應我們的觸控事件,但遺憾的是我們的觸控事件 同一時刻只能被某一個View或者ViewGroup攔截消費,所以就產生了滑動衝突。

那既然同一時刻只能由某一個 View 或者 ViewGroup 消費攔截,那我們就只需要 決定在某個時刻由這個 View 或者 ViewGroup 攔截事件,另外的 某個時刻由 另外一個 View 或者 ViewGroup 攔截事件,不就 OK了嗎?

綜上,正如 在 《Android開發藝術》 一書提出的,總共 有兩種解決方案

以下解決思路來自於 《Android開發藝術》 書籍

下面的兩種方法針對第一種情況(滑動方向不同),父View是上下滑動,子View是左右滑動的情況。

外部解決法

從父View著手,重寫onInterceptTouchEvent方法,在父View需要攔截的時候攔截,不要的時候返回false,為程式碼大概 如下

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final float x = ev.getX();
    final float y = ev.getY();

    final int action = ev.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            mDownPosX = x;
            mDownPosY = y;

            break;
        case MotionEvent.ACTION_MOVE:
            final float deltaX = Math.abs(x - mDownPosX);
            final float deltaY = Math.abs(y - mDownPosY);
            // 這裡是夠攔截的判斷依據是左右滑動,讀者可根據自己的邏輯進行是否攔截
            if (deltaX > deltaY) {
                return false;
            }
    }

    return super.onInterceptTouchEvent(ev);
}

內部解決法

從子View著手,父View先不要攔截任何事件,所有的事件傳遞給 子View,如果子View需要此事件就消費掉,不需要此事件的話就交給 父View處理。

實現思路 如下,重寫子 View的dispatchTouchEvent方法,在Action_down 動作中通過方法 requestDisallowInterceptTouchEvent(true) 先請求 父 View不要攔截事件,這樣保證子 View 能夠接受到 Action_move 事件,再在 Action_move 動作中根據自己的邏輯是否要攔截事件,不需要攔截事件的話再交給 父 View 處理。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    int x = (int) ev.getRawX();
    int y = (int) ev.getRawY();
    int dealtX = 0;
    int dealtY = 0;

    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            dealtX = 0;
            dealtY = 0;
            // 保證子View能夠接收到Action_move事件
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            dealtX += Math.abs(x - lastX);
            dealtY += Math.abs(y - lastY);
            Log.i(TAG, "dealtX:=" + dealtX);
            Log.i(TAG, "dealtY:=" + dealtY);
            // 這裡是夠攔截的判斷依據是左右滑動,讀者可根據自己的邏輯進行是否攔截
            if (dealtX >= dealtY) {
                getParent().requestDisallowInterceptTouchEvent(true);
            } else {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_CANCEL:
            break;
        case MotionEvent.ACTION_UP:
            break;

    }
    return super.dispatchTouchEvent(ev);
}

更多細節,可以檢視我的這篇文章,裡面有詳細介紹哦 ViewPager,ScrollView 巢狀ViewPager滑動衝突解決


View 雙擊,多擊事件是怎麼實現的

實現之前,我們首先來闡述一下思路,怎樣實現雙擊事件,正所謂,授人以魚不如授人以漁。

單擊:使用者點選一次之後,一段時間之內不再點選

雙擊;使用者點選一次之後,一段時間之內再次點選

實現思路

  1. 我們監聽 onTouch 事件,在 ACTION_DOWN 的時候,點選次數 clickCount +1;
  2. 同時,在 ACTION_DOWN 的時候,延時一段時間,執行相應的 Runnable 任務,這裡我們用 handler 的 postDelayed 實現
  3. 在延時任務執行的時候,我們根據點選的次數,進行單擊或者多級的回撥,最後,記得重置點選次數,以及移除延時任務
open class MyDoubleTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener {

    private var clickCount = 0 //記錄連續點選次數
    private val handler: Handler = Handler()

    interface MyClickCallBack {
        fun oneClick() //點選一次的回撥
        fun doubleClick() //連續點選兩次的回撥
    }

    override fun onTouch(v: View, event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_DOWN) {
            clickCount++
            handler.postDelayed({
                if (clickCount == 1) {
                    myClickCallBack.oneClick()
                } else if (clickCount == 2) {
                    myClickCallBack.doubleClick()
                }
                handler.removeCallbacksAndMessages(null)
                //清空handler延時,並防記憶體洩漏
                clickCount = 0 //計數清零
            }, timeout.toLong()) //延時timeout後執行run方法中的程式碼
        }
        return false //讓點選事件繼續傳播,方便再給View新增其他事件監聽
    }

    companion object {
        private const val TAG = "MyClickListener"
        private val timeout = ViewConfiguration.getDoubleTapTimeout() //雙擊間四百毫秒延時

        init {
            Log.i(TAG, "timeout is $timeout ")
        }
    }

}

三擊事件

三級事件呢,其實也很簡單,我們直接判斷在指定時間間隔內點選的次數即可

open class MyMultiTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener {

    private var clickCount = 0 //記錄連續點選次數
    private val handler: Handler = Handler()

    interface MyClickCallBack {
        fun oneClick() //點選一次的回撥
        fun doubleClick() //連續點選兩次的回撥
        fun threeClick() // 連續點選三次的回撥
    }

    override fun onTouch(v: View, event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_DOWN) {
            clickCount++
            handler.postDelayed({
                if (clickCount == 1) {
                    myClickCallBack.oneClick()
                } else if (clickCount == 2) {
                    myClickCallBack.doubleClick()
                } else if (clickCount == 3) {
                    myClickCallBack.threeClick()
                }
                handler.removeCallbacksAndMessages(null)
                //清空handler延時,並防記憶體洩漏
                clickCount = 0 //計數清零
            }, timeout.toLong()) //延時timeout後執行run方法中的程式碼
        }
        return false //讓點選事件繼續傳播,方便再給View新增其他事件監聽
    }

    companion object {
        private const val TAG = "MyClickListener"
        private val timeout = 600 //雙擊間四百毫秒延時

        init {
            Log.i(TAG, "timeout is $timeout ")
        }
    }
}

手勢識別

在 Android 開發當中,幾乎所有的事件都會與使用者進行互動,而我們用得的最多的就是手勢了。

我們知道當我們觸控螢幕的時候,會產生很多事件,比如 down,move,up, fling 事件等等。一些簡單的處理,我們可以直接重寫 View 的 onTouchEvent 方法,根據 View 的 MotionEvent 事件進行處理。

而 Google 為了方便開發者方便接入,提供了幾個預設處理類,那就是 GestureDetector 和 ScaleGestureDetector。

GestureDetector這個類對外提供了兩個介面和一個外部類 。 介面:OnGestureListener,OnDoubleTapListener

內部類:SimpleOnGestureListener,同時實現了 OnGestureListener,OnDoubleTapListener 介面,如果只想使用介面裡面的某個方法,可以直接使用它,方便快捷。

講解之前,我們向來看一下怎麼使用

GestureDetector(Context context, GestureDetector.OnGestureListener listener)

GestureDetector 基本使用

第一步,初始化 GestureDetector 物件

 mDetector = GestureDetectorCompat(this, MyGestureListener())

可以看到有兩個引數,第一個引數 context,第二個引數 OnGestureListener,我們可以直接實現 OnGestureListener 介面,也可以直接使用 GestureDetector.SimpleOnGestureListener

    private class MyGestureListener : GestureDetector.OnGestureListener {

        private val TAG = "GestureDemoActivity"

        override fun onShowPress(e: MotionEvent?) {
            Log.d(TAG, "onShowPress: e is $e")
        }

        override fun onSingleTapUp(e: MotionEvent?): Boolean {
            Log.d(TAG, "onSingleTapUp: e is $e")
            return false
        }

        override fun onDown(event: MotionEvent): Boolean {
            Log.d(TAG, "onDown: $event")
            return true
        }

        override fun onFling(
                event1: MotionEvent,
                event2: MotionEvent,
                velocityX: Float,
                velocityY: Float
        ): Boolean {
            Log.d(TAG, "onFling: $event1 $event2")
            return false
        }

        override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
            Log.d(TAG, "onScroll: distanceX is $distanceX,distanceY is $distanceY ")
            return false
        }

        override fun onLongPress(e: MotionEvent?) {
            Log.d(TAG, "onLongPress: e is $e")
        }
    }

第二步:設定雙擊監聽


// 設定雙擊監聽
mDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
            override fun onDoubleTap(e: MotionEvent?): Boolean {
                Log.d(TAG, "onDoubleTap: e is e")
                return false
            }

            override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
                Log.d(TAG, "onDoubleTapEvent: e is e")
                return false
            }

            override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
                Log.d(TAG, "onSingleTapConfirmed: e is e")
                return false
            }

        })

最後,重寫 Activity 或者 View 的 onTouchEvent ,將事件交給 mDetector 處理。

通常會有兩種寫法,第一種是如果手勢處理器處理了,直接返回 true,進行消費。否則,進行預設處理

override fun onTouchEvent(event: MotionEvent): Boolean {
        return if (mDetector.onTouchEvent(event)) {
            true
        } else {
            super.onTouchEvent(event)
        }
    }

第二種寫法是直接在 onTouchEvent 方法中,直接呼叫 mDetector.onTouchEvent(event) 方法

override fun onTouchEvent(event: MotionEvent): Boolean {
        mDetector.onTouchEvent(event)
        return super.onTouchEvent(event)
    }

第二種寫法,一般不會影響當前 View 或者 Activity 事件的傳遞,在開發當中,有時候為了減少一些觸控事件的衝突,經常這樣寫。

ScaleGestureDetector 這裡暫時不展開描述了了,寫著寫著,發現好多呀,一個週末就這樣過去,賊快,覺得對你有幫助的,請來個三連,點贊,收藏,轉發?


小結

這篇文章,其實不難。主要是將 View 的事件分發機制,滑動衝突,以及開發當中經常用到的一些知識點,總結一下。

面試複習筆記:

文末分享一份Android中高階面試複習筆記

這份資料我從2020年春招開始,就會將各部落格、論壇。網站上等優質的Android開發中高階面試題收集起來,然後全網尋找最優的解答方案。每一道面試題都是百分百的大廠面經真題+最優解答。包知識脈絡 + 諸多細節。節省大家在網上搜尋資料的時間來學習,也可以分享給身邊好友一起學習。

由於篇幅原因,下面以截圖展示部分內容。如有需要以下完整學習筆記PDF,可以點贊+評論支援下,點選這裡即可免費自取

正文總共分為6個部分:

Java 基礎(★★)
Java 高階(★★)
Android 基礎(★★★)
Android 高階(★★★)
Android 專案(★★★)
專案面試常見問題(★★★)

一、Java 基礎(★★)

物件導向思想
多型
異常處理
資料型別
Java 的 IO
集合
Java 多執行緒

Java 高階(★★)

Java 中的反射
Java 中的動態代理
Java 中的設計模式&回收機制
Java 的類載入器

Android 基礎(★★★)

Android 基本常識
Activity
Service
BroadCastReceiver
ContentProvider&資料庫

Android 中的佈局
ListView
JNI & NDK
Android 中的網路訪問
Intent
Fragment

Android 高階(★★★)

Android 效能優化
Android 螢幕適配
AIDL
自定義控制元件
Android 中的事件處理
Android 簽名
Android 中的動畫
網路協議
其他

專案面試常見問題(★★★)

  • 開發週期
  • 專案中遇到的難題
  • 專案中最大的收穫
  • 專案是如何上線的
  • 專案是如何盈利的
  • 繪製專案架構圖
  • 專案開發流程
  • 你在專案中的角色
  • 你負責專案中的哪些模組
  • 講講你負責模組的具體實現
  • 專案中都用到了哪些第三發框架
  • 有沒有自己寫過框架
  • 業餘時間你是如何提高自己(學習)的
  • 有沒有自己的技術 blog
  • 你的職業規劃
  • 為什麼離職
  • 為什麼選擇我們公司
  • 說說你們專案的亮點和不足
  • 你們的專案是如何保持風格一致的
  • 專案架構是如何搭建的
  • 螢幕適配是如何解決的
  • 都看過哪些原始碼
  • 專案版本是如何升級的
  • 用的什麼版本控制工具
  • 你能獨立開發嗎
  • App 跟伺服器是如何互動的
  • 需求文件寫過嗎
  • 介面文件寫過嗎
  • 雲伺服器都用過哪些
  • 第三方平臺都用過哪些

簡歷+社招解答+經典HR面試解析

以上是整理總結的Android中高階面試遇到的真題解析,希望對大家有幫助;同時很多人經常也會遇到很多關於簡歷製作,職業困惑、HR經典面試問題回答等有關面試的問題。同樣我也蒐集整理了全套簡歷製作、金三銀四社招困惑、HR面試等問題解析,有疑問,可以提供專業的解答。

對於Android開發的朋友來說應該是最全面最完整的面試資料,為了更好地整理每個模組,我參考了很多網上的優質博文和專案,力求不漏掉每一個知識點。很多朋友靠著這些內容進行復習,拿到了BATJ等大廠的offer,這個資料也已經幫助了很多的安卓開發者,希望也能幫助到你。

由於篇幅原因,如有需要以上完整學習筆記PDF,可以點贊+評論支援一下,點選這裡免費自取

感謝您閱讀這篇文章,如果可以收到您的點贊,那是將使我非常榮幸,希望我們可以成為朋友,一起分享交流Android技術。

相關文章