一種非嵌套滑動衝突的解決方案


非嵌套滑動 | 嵌套滑動

Android 系統的觸摸事件分發老是從父佈局開始分發,從最頂層的子 View 開始處理,這種特性有時候會限制了咱們一些很複雜的交互設計。git

TouchEventBus 致力於解決非嵌套的滑動衝突,好比多個 在同一層級Fragment 對觸摸事件的處理:觸摸事件會先到達頂層 FragmentonTouch 方法,而後逐層判斷是否消費,在都不消費的狀況下才到達底層的 Fragment 。並且這些層級互不嵌套,沒有造成 parent 和 child 的關係,意味着想經過 onInterceptTouchEvent() 或者 requestDisallowInterceptTouchEvent() 方法來調整事件分發都是不可能的。github

同級視圖的觸摸事件

下面是手機YY的開播預覽頁:maven

YY預覽頁

在這個頁面上有不少對觸摸事件的處理,包括且不限於:ide

  • 在屏幕上點擊,會觸發攝像頭的聚焦(黃色框出現的地方)
  • 雙指縮放,會觸發攝像頭的縮放
  • 左右滑動,能夠切換 ViewPager ,從「直播」和「玩遊戲」兩個選項卡之間切換
  • 「玩遊戲」選項卡上的列表能夠滑動
  • 「直播」選項卡上的控件能夠點擊(開播按鈕,添加圖片…)
  • 因爲預覽頁和開播頁是同一個 Activity ,因此這個 Activity 上還有不少開播後的 Fragment,好比公屏等等也有觸摸事件

從視覺上能夠判斷出View Tree的層級以及對觸摸處理的層級:佈局

處理順序

圖左側是 UI 的層級,上層是一些按鈕控件和 ViewPager ,下層是視頻流展現的 Fragment。右邊是觸摸事件處理的層級,雙指縮放/View點擊/聚焦點擊須要在 ViewPager上面,不然都會被 ViewPager 消費掉,可是 ViewPager 的 UI 層級又比視頻的 Fragment 要高。這就是非嵌套的滑動衝突的核心矛盾:gradle

業務邏輯的層級用戶看到的UI層級 不一致ui

對觸摸事件的從新分發

手機YY直播間中的 Fragment 很是多,並且由於插件化的緣由,各個業務插件能夠動態地往直播間添加/移除本身業務的 Fragment ,這些 Fragment 層級相同互不嵌套,有本身比較獨立的業務邏輯,也會有點擊/滑動等事件處理的需求。但因爲業務場景複雜,Fragment 的上下層級順序也會動態改變,這就很容易致使一些 Fragment 一直收不到觸摸事件或者在切換業務模板的時候觸摸事件被其餘業務消費。this

TouchEventBus 用於這種場景下對觸摸事件進行從新分發,咱們能夠爲所欲爲地決定業務邏輯的層級順序。url

TouchEventBus從新分發觸摸事件

每一個手勢的處理就是一個 TouchEventHandler,好比鏡頭的縮放是 CameraZoomHandler ,鏡頭的聚焦點擊是 CameraClickHandlerViewPager 滑動是 PreviewSlideHandler ,而後爲這些 Handler 從新排序,按照業務的須要來傳遞 MotionEvent 。而後是 TouchEventHandler 和ui的對應關係:經過Handler的 attach / dettach 方法來綁定/解綁對應的 ui 。而 ui 能夠是一個具體的 Fragment,也能夠是一個抽象的接口,一個對觸摸事件做出響應的業務。spa

好比開播預覽頁的聚焦點擊處理,先是定義ui的接口:

public interface CameraClickView {
    /** * 在指定位置爲中心顯示一個黃色矩形的聚焦框 * * @param x 手指觸摸座標x * @param y 手指觸摸座標y */
    void showVideoClickFocus(float x, float y);

    /** * 給VideoSdk傳遞觸摸事件,讓其在指定座標進行攝像頭聚焦 * * @param e 觸摸事件 */
    void onTouch(MotionEvent e);
}
複製代碼

而後是 TouchEventHandler 的定義:

public class CameraClickHandler extends TouchEventHandler<CameraClickView> {
    
    private boolean performClick = false;
    //...
    
    @Override
    public boolean onTouch(@NonNull CameraClickView v, MotionEvent e, boolean hasBeenIntercepted) {
        super.onTouch(v, e, hasBeenIntercepted);
        if (!isCameraFocusEnable()) { //一些特殊業務須要禁止攝像頭聚焦
            return false;
        }
        //經過MotionEvent判斷performClick是否爲true
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //...
                break;
            case MotionEvent.ACTION_MOVE:
                //...
                break;
            case MotionEvent.ACTION_UP: 
                //...
                break;
            default:
                break;
        }

        if (performClick) { //認爲是點擊行爲,調用ui的接口
            v.showVideoClickFocus(e.getRawX(), e.getRawY());
            v.onTouch(e);
        }
        return performClick; //點擊的時候消費掉觸摸事件
    }
}
複製代碼

最後是 TouchEventHandler 與 ui 的對應的綁定

public class MobileLiveVideoComponent extends Fragment implements CameraClickView{
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        //...
        //CameraClickHandler與當前Fragment綁定
        TouchEventBus.of(CameraClickHandler.class).attach(this);
    }
    
    @Override
    public void onDestroyView() {
        //...
        //CameraClickHandler與當前Fragment解綁
        TouchEventBus.of(CameraClickHandler.class).dettach(this);
    }
    
    @Override
    public void showVideoClickFocus(float x, float y) {
        //todo: 展現一個黃色框ui
    }
    
    @Override
    public void onTouch(MotionEvent e) {
        //todo: 調用SDK的攝像頭聚焦
    }
}
複製代碼

當用戶對ui的進行手勢操做時,MotionEvent 就會沿着 TouchEventBus 裏面的順序進行分發。若是在 CameraClickHandler 以前沒有別的 Handler 把事件消費掉,那麼就能在 onTouch 方法進行處理,而後在 ui 做出響應。

事件的分發順序

多個 TouchEventHandler 之間須要定義一個分發的順序,最早接收到觸摸事件的 Handler 能夠攔截後面的 Handler。在順序的定義上,很難固定一條絕對的分發路線,由於隨着直播間模版的切換,Fragment 的層級可能會產生變化。 因此 TouchEventBus 使用相對的順序定義。每一個 Handler 能夠決定要攔截哪些其餘的 Handler。好比要把 CameraClickHandler 排在其餘幾個Handler前面:

public class CameraClickHandler extends AbstractTouchEventHandler<CameraClickView> {
    //...

    @Override
    public boolean onTouch(@NonNull CameraClickView v, MotionEvent e, boolean hasBeenIntercepted) {
        //...
    }

    /** * 定義哪些Handler須要排在個人後面 **/
    @Override
    protected void defineNextHandlers(@NonNull List<Class<? extends TouchEventHandler<?, ? extends TouchViewHolder<?>>>> handlers) {
        //下面的Handler都會在CameraClickHandler後面,但他們之間的順序還未定義
        handlers.add(CameraZoomHandler.class);
        handlers.add(MediaMultiTouchHandler.class);
        handlers.add(PreviewSlideHandler.class);
        handlers.add(VideoControlTouchEventHandler.class);
    }
}
複製代碼

每一個 Handler 都會指定排在本身後面的 Handler,從而造成一張圖。經過拓撲排序咱們就能動態地得到一條分發路徑。下圖的箭頭指向 「A->B」 表示A須要排在B的前面:

拓撲排序

在直播間模版切換的時候,任何一個 Handler 均可以動態地添加到這個圖當中,也能夠從這個圖中隨時移除,不會影響其餘業務的正常進行。

嵌套的視圖用 Android 系統的觸摸分發

互不嵌套的 Fragment 層級才須要使用 TouchEventBusFragment 內部用 Android 默認的觸摸事件分發。以下圖:紅色箭頭部分爲 TouchEventBus 的分發,按 Handler 的拓撲順序進行逐層調用。藍色箭頭部分爲 Fragment 內部 ViewTree 的分發,徹底依照 Android 系統的分發順序,即從父佈局向子視圖分發,子視圖向父佈局逐層決定是否消費。

觸摸事件分發

使用例子

運行本工程的 TouchSample 模塊,是一個使用 TouchEventBus 的簡單 Demo 。

TouchSample

  • 單指左右滑動切換選項卡
  • 雙指縮放中間的"Tab%_subTab%"文本框
  • 雙指左右滑動切換背景圖
  • 滑動屏幕左側拉出側邊面板

ui的層級:Activity -> 背景圖 -> 側邊面板 -> 選項卡 -> 文本框

觸摸處理的順序:側邊面板 -> 文本縮放 -> 背景圖滑動 -> 底部導航點擊 -> 選項卡滑動

這裏還作了一個操做是:讓底部導航點擊不消費觸摸事件。因此你能夠在底部的導航欄區域上左右滑動,切換的是一級Tab。而在背景圖區域左右滑動,切換的是二級Tab。

配置

  1. 在項目 build.gradle 添加倉庫地址

    allprojects {
        repositories {
            maven { url 'https://jitpack.io' }
    	}
    }
    複製代碼
  2. 對應模塊添加依賴

    dependencies {
        compile 'com.github.YvesCheung.TouchEventBus:toucheventbus:1.4.3'
    }
    複製代碼

項目地址

github.com/YvesCheung/…

相關文章
相關標籤/搜索