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


非嵌套滑動 | 嵌套滑動

Android 系統(tǒng)的觸摸事件分發(fā)總是從父布局開始分發(fā),從最頂層的子 View 開始處理窗看,這種特性有時(shí)候會限制了我們一些很復(fù)雜的交互設(shè)計(jì)潘鲫。

TouchEventBus 致力于解決非嵌套的滑動沖突,比如多個(gè) 在同一層級Fragment 對觸摸事件的處理:觸摸事件會先到達(dá)頂層 FragmentonTouch 方法窃植,然后逐層判斷是否消費(fèi)辖试,在都不消費(fèi)的情況下才到達(dá)底層的 Fragment 辜王。而且這些層級互不嵌套,沒有形成 parent 和 child 的關(guān)系罐孝,意味著想通過 onInterceptTouchEvent() 或者 requestDisallowInterceptTouchEvent() 方法來調(diào)整事件分發(fā)都是不可能的。

同級視圖的觸摸事件

下面是手機(jī)YY的開播預(yù)覽頁:

YY預(yù)覽頁
YY預(yù)覽頁

在這個(gè)頁面上有很多對觸摸事件的處理肥缔,包括且不限于:

  • 在屏幕上點(diǎn)擊莲兢,會觸發(fā)攝像頭的聚焦(黃色框出現(xiàn)的地方)
  • 雙指縮放,會觸發(fā)攝像頭的縮放
  • 左右滑動续膳,可以切換 ViewPager 改艇,從“直播”和“玩游戲”兩個(gè)選項(xiàng)卡之間切換
  • “玩游戲”選項(xiàng)卡上的列表可以滑動
  • “直播”選項(xiàng)卡上的控件可以點(diǎn)擊(開播按鈕,添加圖片…)
  • 由于預(yù)覽頁和開播頁是同一個(gè) Activity 坟岔,所以這個(gè) Activity 上還有很多開播后的 Fragment,比如公屏等等也有觸摸事件

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

處理順序
處理順序

圖左側(cè)是 UI 的層級谒兄,上層是一些按鈕控件和 ViewPager ,下層是視頻流展示的 Fragment社付。右邊是觸摸事件處理的層級承疲,雙指縮放/View點(diǎn)擊/聚焦點(diǎn)擊需要在 ViewPager上面,否則都會被 ViewPager 消費(fèi)掉鸥咖,但是 ViewPager 的 UI 層級又比視頻的 Fragment 要高燕鸽。這就是非嵌套的滑動沖突的核心矛盾:

業(yè)務(wù)邏輯的層級用戶看到的UI層級 不一致

對觸摸事件的重新分發(fā)

手機(jī)YY直播間中的 Fragment 非常多,而且因?yàn)椴寮脑蛱淅保鱾€(gè)業(yè)務(wù)插件可以動態(tài)地往直播間添加/移除自己業(yè)務(wù)的 Fragment 啊研,這些 Fragment 層級相同互不嵌套,有自己比較獨(dú)立的業(yè)務(wù)邏輯,也會有點(diǎn)擊/滑動等事件處理的需求党远。但由于業(yè)務(wù)場景復(fù)雜削解,Fragment 的上下層級順序也會動態(tài)改變,這就很容易導(dǎo)致一些 Fragment 一直收不到觸摸事件或者在切換業(yè)務(wù)模板的時(shí)候觸摸事件被其他業(yè)務(wù)消費(fèi)沟娱。

TouchEventBus 用于這種場景下對觸摸事件進(jìn)行重新分發(fā)氛驮,我們可以隨心所欲地決定業(yè)務(wù)邏輯的層級順序。

TouchEventBus重新分發(fā)觸摸事件
TouchEventBus重新分發(fā)觸摸事件

每個(gè)手勢的處理就是一個(gè) TouchEventHandler花沉,比如鏡頭的縮放是 CameraZoomHandler 柳爽,鏡頭的聚焦點(diǎn)擊是 CameraClickHandlerViewPager 滑動是 PreviewSlideHandler 碱屁,然后為這些 Handler 重新排序磷脯,按照業(yè)務(wù)的需要來傳遞 MotionEvent 。然后是 TouchEventHandler 和ui的對應(yīng)關(guān)系:通過Handler的 attach / dettach 方法來綁定/解綁對應(yīng)的 ui 娩脾。而 ui 可以是一個(gè)具體的 Fragment赵誓,也可以是一個(gè)抽象的接口,一個(gè)對觸摸事件作出響應(yīng)的業(yè)務(wù)柿赊。

比如開播預(yù)覽頁的聚焦點(diǎn)擊處理俩功,先是定義ui的接口:

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

    /**
     * 給VideoSdk傳遞觸摸事件,讓其在指定坐標(biāo)進(jìn)行攝像頭聚焦
     *
     * @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()) { //一些特殊業(yè)務(wù)需要禁止攝像頭聚焦
            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) { //認(rèn)為是點(diǎn)擊行為碰声,調(diào)用ui的接口
            v.showVideoClickFocus(e.getRawX(), e.getRawY());
            v.onTouch(e);
        }
        return performClick; //點(diǎn)擊的時(shí)候消費(fèi)掉觸摸事件
    }
}

最后是 TouchEventHandler 與 ui 的對應(yīng)的綁定

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

當(dāng)用戶對ui的進(jìn)行手勢操作時(shí)诡蜓,MotionEvent 就會沿著 TouchEventBus 里面的順序進(jìn)行分發(fā)。如果在 CameraClickHandler 之前沒有別的 Handler 把事件消費(fèi)掉胰挑,那么就能在 onTouch 方法進(jìn)行處理蔓罚,然后在 ui 作出響應(yīng)。

事件的分發(fā)順序

多個(gè) TouchEventHandler 之間需要定義一個(gè)分發(fā)的順序瞻颂,最先接收到觸摸事件的 Handler 可以攔截后面的 Handler豺谈。在順序的定義上,很難固定一條絕對的分發(fā)路線贡这,因?yàn)殡S著直播間模版的切換茬末,Fragment 的層級可能會產(chǎn)生變化。
所以 TouchEventBus 使用相對的順序定義盖矫。每個(gè) Handler 可以決定要攔截哪些其他的 Handler丽惭。比如要把 CameraClickHandler 排在其他幾個(gè)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);
    }
}

每個(gè) Handler 都會指定排在自己后面的 Handler炼彪,從而形成一張圖吐根。通過拓?fù)渑判蛭覀兙湍軇討B(tài)地獲得一條分發(fā)路徑。下圖的箭頭指向 “A->B” 表示A需要排在B的前面:

拓?fù)渑判?><div   id=拓?fù)渑判?/div>

在直播間模版切換的時(shí)候辐马,任何一個(gè) Handler 都可以動態(tài)地添加到這個(gè)圖當(dāng)中拷橘,也可以從這個(gè)圖中隨時(shí)移除局义,不會影響其他業(yè)務(wù)的正常進(jìn)行。

嵌套的視圖用 Android 系統(tǒng)的觸摸分發(fā)

互不嵌套的 Fragment 層級才需要使用 TouchEventBus冗疮,Fragment 內(nèi)部用 Android 默認(rèn)的觸摸事件分發(fā)萄唇。如下圖:紅色箭頭部分為 TouchEventBus 的分發(fā),按 Handler 的拓?fù)漤樞蜻M(jìn)行逐層調(diào)用术幔。藍(lán)色箭頭部分為 Fragment 內(nèi)部 ViewTree 的分發(fā)另萤,完全依照 Android 系統(tǒng)的分發(fā)順序,即從父布局向子視圖分發(fā)诅挑,子視圖向父布局逐層決定是否消費(fèi)四敞。

觸摸事件分發(fā)
觸摸事件分發(fā)

使用例子

運(yùn)行本工程的 TouchSample 模塊,是一個(gè)使用 TouchEventBus 的簡單 Demo 拔妥。

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

ui的層級:Activity -> 背景圖 -> 側(cè)邊面板 -> 選項(xiàng)卡 -> 文本框

觸摸處理的順序:側(cè)邊面板 -> 文本縮放 -> 背景圖滑動 -> 底部導(dǎo)航點(diǎn)擊 -> 選項(xiàng)卡滑動

這里還做了一個(gè)操作是:讓底部導(dǎo)航點(diǎn)擊不消費(fèi)觸摸事件忿危。所以你可以在底部的導(dǎo)航欄區(qū)域上左右滑動,切換的是一級Tab没龙。而在背景圖區(qū)域左右滑動铺厨,切換的是二級Tab。

配置

  1. 在項(xiàng)目 build.gradle 添加倉庫地址

    allprojects {
        repositories {
            maven { url 'https://jitpack.io' }
        }
    }
    
  2. 對應(yīng)模塊添加依賴

    dependencies {
        compile 'com.github.YvesCheung.TouchEventBus:toucheventbus:1.4.3'
    }
    

項(xiàng)目地址

https://github.com/YvesCheung/TouchEventBus

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末硬纤,一起剝皮案震驚了整個(gè)濱河市解滓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌筝家,老刑警劉巖洼裤,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異溪王,居然都是意外死亡逸邦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門在扰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人雷客,你說我怎么就攤上這事芒珠。” “怎么了搅裙?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵皱卓,是天一觀的道長。 經(jīng)常有香客問我部逮,道長娜汁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任兄朋,我火速辦了婚禮掐禁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己傅事,他們只是感情好缕允,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蹭越,像睡著了一般障本。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上响鹃,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天驾霜,我揣著相機(jī)與錄音,去河邊找鬼买置。 笑死粪糙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的堕义。 我是一名探鬼主播猜旬,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼倦卖!你這毒婦竟也來了洒擦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤怕膛,失蹤者是張志新(化名)和其女友劉穎熟嫩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體褐捻,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡掸茅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了柠逞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昧狮。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖板壮,靈堂內(nèi)的尸體忽然破棺而出逗鸣,到底是詐尸還是另有隱情,我是刑警寧澤绰精,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布撒璧,位于F島的核電站,受9級特大地震影響笨使,放射性物質(zhì)發(fā)生泄漏卿樱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一硫椰、第九天 我趴在偏房一處隱蔽的房頂上張望繁调。 院中可真熱鬧萨蚕,春花似錦、人聲如沸涉馁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烤送。三九已至寒随,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間帮坚,已是汗流浹背妻往。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留试和,地道東北人讯泣。 一個(gè)月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓怎棱,卻偏偏與公主長得像木羹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子步咪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容