Android 系統的觸摸事件分發老是從父佈局開始分發,從最頂層的子 View 開始處理,這種特性有時候會限制了咱們一些很複雜的交互設計。git
TouchEventBus
致力於解決非嵌套的滑動衝突,好比多個 在同一層級 的Fragment
對觸摸事件的處理:觸摸事件會先到達頂層Fragment
的onTouch
方法,而後逐層判斷是否消費,在都不消費的狀況下才到達底層的Fragment
。並且這些層級互不嵌套,沒有造成 parent 和 child 的關係,意味着想經過onInterceptTouchEvent()
或者requestDisallowInterceptTouchEvent()
方法來調整事件分發都是不可能的。github
下面是手機YY的開播預覽頁:maven
在這個頁面上有不少對觸摸事件的處理,包括且不限於: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
每一個手勢的處理就是一個 TouchEventHandler
,好比鏡頭的縮放是 CameraZoomHandler ,鏡頭的聚焦點擊是 CameraClickHandler ,ViewPager
滑動是 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 均可以動態地添加到這個圖當中,也能夠從這個圖中隨時移除,不會影響其餘業務的正常進行。
互不嵌套的 Fragment
層級才須要使用 TouchEventBus
,Fragment
內部用 Android 默認的觸摸事件分發。以下圖:紅色箭頭部分爲 TouchEventBus
的分發,按 Handler 的拓撲順序進行逐層調用。藍色箭頭部分爲 Fragment
內部 ViewTree 的分發,徹底依照 Android 系統的分發順序,即從父佈局向子視圖分發,子視圖向父佈局逐層決定是否消費。
運行本工程的 TouchSample 模塊,是一個使用 TouchEventBus
的簡單 Demo 。
ui的層級:Activity -> 背景圖 -> 側邊面板 -> 選項卡 -> 文本框
觸摸處理的順序:側邊面板 -> 文本縮放 -> 背景圖滑動 -> 底部導航點擊 -> 選項卡滑動
這裏還作了一個操做是:讓底部導航點擊不消費觸摸事件。因此你能夠在底部的導航欄區域上左右滑動,切換的是一級Tab。而在背景圖區域左右滑動,切換的是二級Tab。
在項目 build.gradle 添加倉庫地址
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
複製代碼
對應模塊添加依賴
dependencies {
compile 'com.github.YvesCheung.TouchEventBus:toucheventbus:1.4.3'
}
複製代碼