非嵌套滑動 | 嵌套滑動
Android 系統(tǒng)的觸摸事件分發(fā)總是從父布局開始分發(fā),從最頂層的子 View 開始處理窗看,這種特性有時(shí)候會限制了我們一些很復(fù)雜的交互設(shè)計(jì)潘鲫。
TouchEventBus
致力于解決非嵌套的滑動沖突,比如多個(gè) 在同一層級 的Fragment
對觸摸事件的處理:觸摸事件會先到達(dá)頂層Fragment
的onTouch
方法窃植,然后逐層判斷是否消費(fèi)辖试,在都不消費(fèi)的情況下才到達(dá)底層的Fragment
辜王。而且這些層級互不嵌套,沒有形成 parent 和 child 的關(guān)系罐孝,意味著想通過onInterceptTouchEvent()
或者requestDisallowInterceptTouchEvent()
方法來調(diào)整事件分發(fā)都是不可能的。
同級視圖的觸摸事件
下面是手機(jī)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ù)邏輯的層級順序。

每個(gè)手勢的處理就是一個(gè) TouchEventHandler
花沉,比如鏡頭的縮放是 CameraZoomHandler 柳爽,鏡頭的聚焦點(diǎn)擊是 CameraClickHandler ,ViewPager
滑動是 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的前面:

在直播間模版切換的時(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)四敞。

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

- 單指左右滑動切換選項(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。
配置
-
在項(xiàng)目 build.gradle 添加倉庫地址
allprojects { repositories { maven { url 'https://jitpack.io' } } }
-
對應(yīng)模塊添加依賴
dependencies { compile 'com.github.YvesCheung.TouchEventBus:toucheventbus:1.4.3' }