HarmonyOS非侵入式事件分發(fā)設(shè)計(jì)
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
在鴻蒙的Java UI框架中的交互中,是只存在消費(fèi)機(jī)制,并沒有分發(fā)機(jī)制。消費(fèi)事件是從子控件向父控件傳遞,而分發(fā)事件是從父控件向子控件傳遞。消費(fèi)機(jī)制雖然可以滿足大部分單一化的場(chǎng)景,但是隨著業(yè)務(wù)和UI設(shè)計(jì)的復(fù)雜化,僅靠消費(fèi)機(jī)制是無法滿足實(shí)際需求的。下面簡(jiǎn)單介紹下鴻蒙目前的消費(fèi)機(jī)制流程:
首先自定義一個(gè)CustomContainer和CustomChild,然后都增加TouchEventListener的監(jiān)聽,下面打印出父控件和子控件的onTouchEvent設(shè)置不同返回值時(shí)候的事件消費(fèi)日志:
- CustomContainer:true CustomChild:false
- 07-12 10:15:29.785 28923-28923/? W 0006E/seagazer: com.testbug.widget.CustomContainer # init[Line:33]: onTouchEvent: DOWN 1, MOVE 3, UP 2
- 07-12 10:15:33.103 28923-28923/? D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->1
- 07-12 10:15:33.103 28923-28923/? D 0006E/seagazer: com.testbug.widget.CustomContainer$1 # onTouchEvent[Line:37]: ------->1
- 07-12 10:15:33.652 28923-28923/? D 0006E/seagazer: com.testbug.widget.CustomContainer$1 # onTouchEvent[Line:37]: ------->3
- 07-12 10:15:34.344 28923-28923/? D 0006E/seagazer: com.testbug.widget.CustomContainer$1 # onTouchEvent[Line:37]: ------->2
- CustomContainer:true CustomChild:true
- 07-12 10:16:02.501 5438-5438/? D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->1
- 07-12 10:16:03.050 5438-5438/? D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->3
- 07-12 10:16:03.532 5438-5438/? D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->3
- 07-12 10:16:03.970 5438-5438/? D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->2
- CustomContainer:false CustomChild:true
- 07-12 10:16:54.300 5441-5441/ D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->1
- 07-12 10:16:54.555 5441-5441/ D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->3
- 07-12 10:16:54.881 5441-5441/ D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->3
- 07-12 10:16:55.269 5441-5441/ D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->2
- CustomContainer:false CustomChild:false
- 07-12 10:17:29.362 10847-10847/? D 0006E/seagazer: com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->1
- 07-12 10:17:29.362 10847-10847/? D 0006E/seagazer: com.testbug.widget.CustomContainer$1 # onTouchEvent[Line:37]: ------->1
因?yàn)椴淮嬖诜职l(fā)和攔截機(jī)制,不論什么情況,down事件永遠(yuǎn)是子控件優(yōu)先觸發(fā),根據(jù)子控件是否消費(fèi)down事件來判斷后續(xù)的move,up事件是否傳遞給它。
何為事件分發(fā)
這里簡(jiǎn)單介紹下事件分發(fā):客戶端的視圖框架一般都是設(shè)計(jì)成樹結(jié)構(gòu),視圖樹會(huì)有根節(jié)點(diǎn)。事件的源頭就是從根節(jié)點(diǎn)開始,一般通過深度遍歷傳遞給各個(gè)子節(jié)點(diǎn),然后根據(jù)各個(gè)子節(jié)點(diǎn)是否攔截,繼續(xù)下發(fā)給各個(gè)子子節(jié)點(diǎn),以此類推。這就是事件分發(fā)模型。事件消費(fèi)模型則是從子節(jié)點(diǎn)開始,根據(jù)該子節(jié)點(diǎn)是否消費(fèi),繼續(xù)把事件回溯給父節(jié)點(diǎn)或者同級(jí)子節(jié)點(diǎn),看其是否消費(fèi)。分發(fā)消費(fèi)機(jī)制可以理解為一種典型的責(zé)任鏈的設(shè)計(jì)模式。
主流的事件分發(fā)設(shè)計(jì)
鴻蒙目前其實(shí)也已經(jīng)存在一些分發(fā)的框架,但是多數(shù)都是屬于侵入式的設(shè)計(jì),需要自定義控件繼承或者實(shí)現(xiàn)其接口,再在onTouchEvent代理其事件。這種方式在絕大部分場(chǎng)景的確可以滿足需求,并且如果是從framework層設(shè)計(jì),這種方式也是最優(yōu)的。畢竟都是通過頂層接口或者抽象類對(duì)外暴露的方式,說白點(diǎn)就是把所有原生控件完全自主可控化,需要外界繼承或?qū)崿F(xiàn)其進(jìn)行統(tǒng)一化的邏輯處理。
何為侵入式以及其缺陷
但是有些開發(fā)場(chǎng)景,里面涉及到第三方提供的控件,第三方提供的控件肯定不會(huì)去實(shí)現(xiàn)我們的頂層接口或抽象類,這種場(chǎng)景就不是太合適了,畢竟我們是從應(yīng)用層的角度去增加一個(gè)分發(fā)機(jī)制。如果我們按照這種方式去設(shè)計(jì),就需要把第三方源碼全部拷貝到我們項(xiàng)目,自行對(duì)其進(jìn)行修改適配我們的規(guī)則,暫且稱之為侵入式設(shè)計(jì)。能夠保證在不需要修改第三方源碼的前提下去實(shí)現(xiàn),稱之為非侵入式設(shè)計(jì)。
舉個(gè)例子,你的項(xiàng)目中用到一個(gè)第三方提供的自定義CustomView組件,它并沒有繼承你的頂層接口,但是它把所有事件都消費(fèi)掉了,因?yàn)樗陨聿⒉粫?huì)考慮太多復(fù)雜的場(chǎng)景,那假設(shè)你需要CustomView插入到一個(gè)自定義的滑動(dòng)列表使用,它都完全消費(fèi)掉了事件,你的自定義滑動(dòng)列表還能處理消費(fèi)事件么?答案是肯定不能的。那有什么辦法可以讓第三方組件不消費(fèi)事件呢,并且讓其加入我們自定義的攔截機(jī)制中呢?可以通過邏輯托管方式。
事件溯源托管
在當(dāng)前鴻蒙提供的消費(fèi)機(jī)制中,我們要想自定義父控件能夠接受到事件,子控件必須保證不能消費(fèi)事件。因此,我們必須將子控件的消費(fèi)邏輯暫時(shí)屏蔽(或者onTouchEvent中返回false),這樣,我們就能將所有事件一級(jí)級(jí)的回溯到頂層父控件:
- private final WeakHashMap<Component, Component.TouchEventListener> observers = new WeakHashMap<>();
- ...
- // 遍歷所有子控件,如果子控件有自己的touch事件處理邏輯,加入緩存列表,并重置子控件的touch監(jiān)聽
- // 這樣,所有子控件的touch事件處理邏輯都被托管至緩存列表,實(shí)際上所有子控件并不消費(fèi)事件,事件消費(fèi)回到了頂層控件,也就是我們所說的事件源
- for (int i = 0; i < childCount; i++) {
- Component child = rootComponent.getComponentAt(i);
- Component.TouchEventListener childListener = child.getTouchEventListener();
- if (childListener != null) {
- observers.put(child, childListener);
- child.setTouchEventListener(null);
- }
通過上面的邏輯,我們把所有子控件的事件處理都托管到一個(gè)緩存列表,并且重置子控件的事件監(jiān)聽,這樣一來,事件就會(huì)溯源到了我們頂層控件,而一般情況下頂層控件都是屬于布局容器,因此我們就只需要處理好該容器的事件流程:
- private Component touchTarget = null;
- @Override
- public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
- int action = touchEvent.getAction();
- boolean isIntercepted = false;
- // down事件,判斷當(dāng)前是否需要攔截
- if (action == TouchEvent.PRIMARY_POINT_DOWN) {
- touchTarget = null;
- isIntercepted = interceptTouchEvent(component, touchEvent);
- }
- if (isIntercepted) {
- // 攔截的話,自己處理掉
- return processTouchEvent(component, touchEvent);
- } else {
- if (action == TouchEvent.PRIMARY_POINT_DOWN) {
- // down事件,查找touch目標(biāo)子控件
- // 當(dāng)前控件為布局容器時(shí),遍歷子控件查找,符合目標(biāo)如果需要消費(fèi)down事件,則后續(xù)事件都交給其處理
- if (component instanceof ComponentContainer) {
- ComponentContainer root = (ComponentContainer) component;
- int childCount = root.getChildCount();
- for (int i = childCount - 1; i >= 0; i--) {
- Component child = root.getComponentAt(i);
- if (isTouchInTarget(child, touchEvent)) {
- Component.TouchEventListener listener = observers.get(child);
- if (listener != null) {
- boolean handled = listener.onTouchEvent(child, touchEvent);
- if (handled) {
- touchTarget = child;
- return true;
- }
- }
- }
- }
- } else {
- if (isTouchInTarget(component, touchEvent)) {
- Component.TouchEventListener listener = observers.get(component);
- if (listener != null) {
- boolean handled = listener.onTouchEvent(component, touchEvent);
- if (handled) {
- touchTarget = component;
- return true;
- }
- }
- }
- }
- }
- }
- // 沒有找到touch目標(biāo)子控件,自己處理
- if (touchTarget == null) {
- return processTouchEvent(component, touchEvent);
- }
- // 如果touchTarget不為null,說明down事件時(shí)候已經(jīng)找到了需要消費(fèi)的目標(biāo)控件,直接將其余事件交給它處理
- Component.TouchEventListener listener = observers.get(touchTarget);
- if (listener != null) {
- return listener.onTouchEvent(touchTarget, touchEvent);
- }
- // 上述條件都不符合,自己處理
- return processTouchEvent(component, touchEvent);
- }
這樣一來,自定義控件對(duì)事件的監(jiān)聽回調(diào)的onTouchEvent邏輯就被托管了,具體是否會(huì)執(zhí)行該消費(fèi)邏輯,不再由系統(tǒng)進(jìn)行處理,而是由我們的ExTouchListener根據(jù)布局容器是否攔截,以及子控件是否消費(fèi)共同進(jìn)行決策。下面列列舉一個(gè)demo,里面有2個(gè)自定義控件,一個(gè)自定義父布局包裹一個(gè)自定義子控件:
<ExTouchListener.java>
- public abstract class ExTouchListener implements Component.TouchEventListener, Component.LayoutRefreshedListener {
- private final WeakHashMap<Component, Component.TouchEventListener> observers = new WeakHashMap<>();
- private final ComponentContainer rootComponent;
- private Component touchTarget = null;
- public ExTouchListener(ComponentContainer root) {
- this.rootComponent = root;
- this.rootComponent.setLayoutRefreshedListener(this);
- }
- @Override
- public void onRefreshed(Component component) {
- int childCount = rootComponent.getChildCount();
- if (childCount != observers.size()) {
- for (int i = 0; i < childCount; i++) {
- Component child = rootComponent.getComponentAt(i);
- Component.TouchEventListener childListener = child.getTouchEventListener();
- if (childListener != null) {
- observers.put(child, childListener);
- child.setTouchEventListener(null);
- }
- }
- }
- }
- @Override
- public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
- int action = touchEvent.getAction();
- boolean isIntercepted = false;
- if (action == TouchEvent.PRIMARY_POINT_DOWN) {
- touchTarget = null;
- isIntercepted = interceptTouchEvent(component, touchEvent);
- }
- if (isIntercepted) {
- // intercepted
- return processTouchEvent(component, touchEvent);
- } else {
- if (action == TouchEvent.PRIMARY_POINT_DOWN) {
- // down, find touch target
- if (component instanceof ComponentContainer) {
- ComponentContainer root = (ComponentContainer) component;
- int childCount = root.getChildCount();
- for (int i = childCount - 1; i >= 0; i--) {
- Component child = root.getComponentAt(i);
- if (isTouchInTarget(child, touchEvent)) {
- Component.TouchEventListener listener = observers.get(child);
- if (listener != null) {
- boolean handled = listener.onTouchEvent(child, touchEvent);
- if (handled) {
- touchTarget = child;
- return true;
- }
- }
- }
- }
- } else {
- if (isTouchInTarget(component, touchEvent)) {
- Component.TouchEventListener listener = observers.get(component);
- if (listener != null) {
- boolean handled = listener.onTouchEvent(component, touchEvent);
- if (handled) {
- touchTarget = component;
- return true;
- }
- }
- }
- }
- }
- }
- // not find touch target, handle self
- if (touchTarget == null) {
- return processTouchEvent(component, touchEvent);
- }
- // move, up ...
- Component.TouchEventListener listener = observers.get(touchTarget);
- if (listener != null) {
- return listener.onTouchEvent(touchTarget, touchEvent);
- }
- return processTouchEvent(component, touchEvent);
- }
- public abstract boolean interceptTouchEvent(Component component, TouchEvent touchEvent);
- public abstract boolean processTouchEvent(Component component, TouchEvent touchEvent);
- private boolean isTouchInTarget(Component target, TouchEvent touchEvent) {
- MmiPoint pointer = touchEvent.getPointerScreenPosition(touchEvent.getIndex());
- float touchX = pointer.getX();
- float touchY = pointer.getY();
- int[] location = target.getLocationOnScreen();
- int targetX = location[0];
- int targetY = location[1];
- int targetWidth = target.getWidth();
- int targetHeight = target.getHeight();
- boolean result = touchX >= targetX && touchX <= targetX + targetWidth && touchY >= targetY && touchY <= targetY + targetHeight;
- return result;
- }
- }
<CustomContainer.java>
- public class CustomContainer extends DirectionalLayout {
- public CustomContainer(Context context, AttrSet attrSet) {
- super(context, attrSet);
- setTouchEventListener(new ExTouchListener(this) {
- @Override
- public boolean interceptTouchEvent(Component component, TouchEvent touchEvent) {
- return false;
- }
- @Override
- public boolean processTouchEvent(Component component, TouchEvent touchEvent) {
- switch (touchEvent.getAction()) {
- case TouchEvent.PRIMARY_POINT_DOWN:
- Logger2.w("--->down");
- return true;
- case TouchEvent.POINT_MOVE:
- Logger2.w("--->move");
- return true;
- case TouchEvent.PRIMARY_POINT_UP:
- Logger2.w("--->up");
- return true;
- }
- return true;
- }
- });
- }
- }
<CustomComponent.java>
- public class CustomComponent extends Text {
- public CustomComponent(Context context, AttrSet attrSet) {
- super(context, attrSet);
- setTouchEventListener(new TouchEventListener() {
- @Override
- public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
- switch (touchEvent.getAction()) {
- case TouchEvent.PRIMARY_POINT_DOWN:
- Logger2.e( "--->down");
- return true;
- case TouchEvent.POINT_MOVE:
- Logger2.e( "--->move");
- return true;
- case TouchEvent.PRIMARY_POINT_UP:
- Logger2.e( "--->up");
- return true;
- }
- return false;
- }
- });
- }
- }
下面看下3種常見場(chǎng)景的處理打印日志(上面已經(jīng)貼出全部源碼,可以復(fù)制進(jìn)自己的項(xiàng)目運(yùn)行):
- // 父控件不攔截,子控件down事件不消費(fèi),父控件的processTouchEvent進(jìn)行處理
- CustomContainer interceptTouchEvent:false CustomComponent onTouchEvent down:false
- 08-02 14:42:53.754 17396-17396/? E 0006E/seagazer: com.example.touch.CustomComponent$1 # onTouchEvent[Line:35]: --->down
- 08-02 14:42:53.754 17396-17396/? D 0006E/seagazer: com.example.touch.CustomContainer$1 # processTouchEvent[Line:42]: --->down
- 08-02 14:42:53.824 17396-17396/? D 0006E/seagazer: com.example.touch.CustomContainer$1 # processTouchEvent[Line:48]: --->up
- // 父控件不攔截,子控件down事件消費(fèi),子控件onTouchEvent處理
- CustomContainer interceptTouchEvent:false CustomComponent onTouchEvent down:true
- 08-02 14:43:29.132 17661-17661/com.example.touch E 0006E/seagazer: com.example.touch.CustomComponent$1 # onTouchEvent[Line:35]: --->down
- 08-02 14:43:29.218 17661-17661/com.example.touch E 0006E/seagazer: com.example.touch.CustomComponent$1 # onTouchEvent[Line:41]: --->up
- // 父控件攔截,父控件的processTouchEvent進(jìn)行處理
- 08-02 14:42:13.409 13918-13918/? W 0006E/seagazer: com.example.touch.CustomContainer$1 # processTouchEvent[Line:41]: --->down
- 08-02 14:42:13.533 13918-13918/? W 0006E/seagazer: com.example.touch.CustomContainer$1 # processTouchEvent[Line:47]: --->up
結(jié)語
通過上面的事件托管、事件溯源再傳遞,就已經(jīng)能夠?qū)崿F(xiàn)簡(jiǎn)單的分發(fā)攔截機(jī)制,并且兼容第三方庫(kù)的控件。當(dāng)然,這里主要是提供一種設(shè)計(jì)的簡(jiǎn)化模型,包括disptach機(jī)制,touchTarget的復(fù)用,nestScroll機(jī)制本文都沒考慮,如果本質(zhì)上能夠理解透徹事件的分發(fā)機(jī)制,在此基礎(chǔ)上進(jìn)行擴(kuò)展也不是什么難事。但是回歸當(dāng)下,從個(gè)人角度去評(píng)判,這類理應(yīng)該由系統(tǒng)提供的機(jī)制,畢竟應(yīng)用層更多的精力應(yīng)該放在業(yè)務(wù)的實(shí)現(xiàn),用戶界面交互,應(yīng)用性能方面,而不是把一些框架層機(jī)制自己去實(shí)現(xiàn)一遍。
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)