HarmonyOS非侵入式事件分发设计

开发 前端 OpenHarmony
在鸿蒙的Java UI框架中的交互中,是只存在消费机制,并没有分发机制。消费事件是从子控件向父控件传递,而分发事件是从父控件向子控件传递。

[[418480]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

在鸿蒙的Java UI框架中的交互中,是只存在消费机制,并没有分发机制。消费事件是从子控件向父控件传递,而分发事件是从父控件向子控件传递。消费机制虽然可以满足大部分单一化的场景,但是随着业务和UI设计的复杂化,仅靠消费机制是无法满足实际需求的。下面简单介绍下鸿蒙目前的消费机制流程:

首先自定义一个CustomContainer和CustomChild,然后都增加TouchEventListener的监听,下面打印出父控件和子控件的onTouchEvent设置不同返回值时候的事件消费日志:

  1. CustomContainer:true CustomChild:false 
  2. 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 
  3. 07-12 10:15:33.103 28923-28923/? D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->1 
  4. 07-12 10:15:33.103 28923-28923/? D 0006E/seagazer:  com.testbug.widget.CustomContainer$1 # onTouchEvent[Line:37]: ------->1 
  5. 07-12 10:15:33.652 28923-28923/? D 0006E/seagazer:  com.testbug.widget.CustomContainer$1 # onTouchEvent[Line:37]: ------->3 
  6. 07-12 10:15:34.344 28923-28923/? D 0006E/seagazer:  com.testbug.widget.CustomContainer$1 # onTouchEvent[Line:37]: ------->2 
  7.  
  8. CustomContainer:true CustomChild:true 
  9. 07-12 10:16:02.501 5438-5438/? D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->1 
  10. 07-12 10:16:03.050 5438-5438/? D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->3 
  11. 07-12 10:16:03.532 5438-5438/? D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->3 
  12. 07-12 10:16:03.970 5438-5438/? D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->2 
  13.  
  14. CustomContainer:false CustomChild:true 
  15. 07-12 10:16:54.300 5441-5441/ D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->1 
  16. 07-12 10:16:54.555 5441-5441/ D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->3 
  17. 07-12 10:16:54.881 5441-5441/ D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->3 
  18. 07-12 10:16:55.269 5441-5441/ D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->2 
  19.  
  20. CustomContainer:false CustomChild:false  
  21. 07-12 10:17:29.362 10847-10847/? D 0006E/seagazer:  com.testbug.widget.CustomChild$1 # onTouchEvent[Line:33]: ------->1 
  22. 07-12 10:17:29.362 10847-10847/? D 0006E/seagazer:  com.testbug.widget.CustomContainer$1 # onTouchEvent[Line:37]: ------->1 

因为不存在分发和拦截机制,不论什么情况,down事件永远是子控件优先触发,根据子控件是否消费down事件来判断后续的move,up事件是否传递给它。

何为事件分发

这里简单介绍下事件分发:客户端的视图框架一般都是设计成树结构,视图树会有根节点。事件的源头就是从根节点开始,一般通过深度遍历传递给各个子节点,然后根据各个子节点是否拦截,继续下发给各个子子节点,以此类推。这就是事件分发模型。事件消费模型则是从子节点开始,根据该子节点是否消费,继续把事件回溯给父节点或者同级子节点,看其是否消费。分发消费机制可以理解为一种典型的责任链的设计模式。

主流的事件分发设计

鸿蒙目前其实也已经存在一些分发的框架,但是多数都是属于侵入式的设计,需要自定义控件继承或者实现其接口,再在onTouchEvent代理其事件。这种方式在绝大部分场景的确可以满足需求,并且如果是从framework层设计,这种方式也是最优的。毕竟都是通过顶层接口或者抽象类对外暴露的方式,说白点就是把所有原生控件完全自主可控化,需要外界继承或实现其进行统一化的逻辑处理。

何为侵入式以及其缺陷

但是有些开发场景,里面涉及到第三方提供的控件,第三方提供的控件肯定不会去实现我们的顶层接口或抽象类,这种场景就不是太合适了,毕竟我们是从应用层的角度去增加一个分发机制。如果我们按照这种方式去设计,就需要把第三方源码全部拷贝到我们项目,自行对其进行修改适配我们的规则,暂且称之为侵入式设计。能够保证在不需要修改第三方源码的前提下去实现,称之为非侵入式设计。

举个例子,你的项目中用到一个第三方提供的自定义CustomView组件,它并没有继承你的顶层接口,但是它把所有事件都消费掉了,因为它自身并不会考虑太多复杂的场景,那假设你需要CustomView插入到一个自定义的滑动列表使用,它都完全消费掉了事件,你的自定义滑动列表还能处理消费事件么?答案是肯定不能的。那有什么办法可以让第三方组件不消费事件呢,并且让其加入我们自定义的拦截机制中呢?可以通过逻辑托管方式。

事件溯源托管

在当前鸿蒙提供的消费机制中,我们要想自定义父控件能够接受到事件,子控件必须保证不能消费事件。因此,我们必须将子控件的消费逻辑暂时屏蔽(或者onTouchEvent中返回false),这样,我们就能将所有事件一级级的回溯到顶层父控件:

  1. private final WeakHashMap<Component, Component.TouchEventListener> observers = new WeakHashMap<>(); 
  2.   ... 
  3.   // 遍历所有子控件,如果子控件有自己的touch事件处理逻辑,加入缓存列表,并重置子控件的touch监听 
  4.   // 这样,所有子控件的touch事件处理逻辑都被托管至缓存列表,实际上所有子控件并不消费事件,事件消费回到了顶层控件,也就是我们所说的事件源 
  5.   for (int i = 0; i < childCount; i++) { 
  6.       Component child = rootComponent.getComponentAt(i); 
  7.       Component.TouchEventListener childListener = child.getTouchEventListener(); 
  8.       if (childListener != null) { 
  9.           observers.put(child, childListener); 
  10.           child.setTouchEventListener(null); 
  11.        
  12.   } 

通过上面的逻辑,我们把所有子控件的事件处理都托管到一个缓存列表,并且重置子控件的事件监听,这样一来,事件就会溯源到了我们顶层控件,而一般情况下顶层控件都是属于布局容器,因此我们就只需要处理好该容器的事件流程:

  1. private Component touchTarget = null
  2.  
  3. @Override 
  4. public boolean onTouchEvent(Component component, TouchEvent touchEvent) { 
  5.     int action = touchEvent.getAction(); 
  6.     boolean isIntercepted = false
  7.     // down事件,判断当前是否需要拦截 
  8.     if (action == TouchEvent.PRIMARY_POINT_DOWN) { 
  9.         touchTarget = null
  10.         isIntercepted = interceptTouchEvent(component, touchEvent); 
  11.     } 
  12.     if (isIntercepted) { 
  13.         // 拦截的话,自己处理掉 
  14.         return processTouchEvent(component, touchEvent); 
  15.     } else { 
  16.         if (action == TouchEvent.PRIMARY_POINT_DOWN) { 
  17.             // down事件,查找touch目标子控件 
  18.             // 当前控件为布局容器时,遍历子控件查找,符合目标如果需要消费down事件,则后续事件都交给其处理 
  19.             if (component instanceof ComponentContainer) { 
  20.                 ComponentContainer root = (ComponentContainer) component; 
  21.                 int childCount = root.getChildCount(); 
  22.                 for (int i = childCount - 1; i >= 0; i--) { 
  23.                     Component child = root.getComponentAt(i); 
  24.                     if (isTouchInTarget(child, touchEvent)) { 
  25.                         Component.TouchEventListener listener = observers.get(child); 
  26.                         if (listener != null) { 
  27.                             boolean handled = listener.onTouchEvent(child, touchEvent); 
  28.                             if (handled) { 
  29.                                 touchTarget = child; 
  30.                                 return true
  31.                             } 
  32.                         } 
  33.                     } 
  34.                 } 
  35.             } else { 
  36.                 if (isTouchInTarget(component, touchEvent)) { 
  37.                     Component.TouchEventListener listener = observers.get(component); 
  38.                     if (listener != null) { 
  39.                         boolean handled = listener.onTouchEvent(component, touchEvent); 
  40.                         if (handled) { 
  41.                             touchTarget = component; 
  42.                             return true
  43.                         } 
  44.                     } 
  45.                 } 
  46.             } 
  47.         } 
  48.     } 
  49.     // 没有找到touch目标子控件,自己处理 
  50.     if (touchTarget == null) { 
  51.         return processTouchEvent(component, touchEvent); 
  52.     } 
  53.     // 如果touchTarget不为null,说明down事件时候已经找到了需要消费的目标控件,直接将其余事件交给它处理 
  54.     Component.TouchEventListener listener = observers.get(touchTarget); 
  55.     if (listener != null) { 
  56.         return listener.onTouchEvent(touchTarget, touchEvent); 
  57.     } 
  58.     // 上述条件都不符合,自己处理 
  59.     return processTouchEvent(component, touchEvent); 

这样一来,自定义控件对事件的监听回调的onTouchEvent逻辑就被托管了,具体是否会执行该消费逻辑,不再由系统进行处理,而是由我们的ExTouchListener根据布局容器是否拦截,以及子控件是否消费共同进行决策。下面列列举一个demo,里面有2个自定义控件,一个自定义父布局包裹一个自定义子控件:

<ExTouchListener.java>

  1. public abstract class ExTouchListener implements Component.TouchEventListener, Component.LayoutRefreshedListener { 
  2.     private final WeakHashMap<Component, Component.TouchEventListener> observers = new WeakHashMap<>(); 
  3.     private final ComponentContainer rootComponent; 
  4.     private Component touchTarget = null
  5.  
  6.     public ExTouchListener(ComponentContainer root) { 
  7.         this.rootComponent = root; 
  8.         this.rootComponent.setLayoutRefreshedListener(this); 
  9.     } 
  10.  
  11.     @Override 
  12.     public void onRefreshed(Component component) { 
  13.         int childCount = rootComponent.getChildCount(); 
  14.         if (childCount != observers.size()) { 
  15.             for (int i = 0; i < childCount; i++) { 
  16.                 Component child = rootComponent.getComponentAt(i); 
  17.                 Component.TouchEventListener childListener = child.getTouchEventListener(); 
  18.                 if (childListener != null) { 
  19.                     observers.put(child, childListener); 
  20.                     child.setTouchEventListener(null); 
  21.                 } 
  22.             } 
  23.         } 
  24.     } 
  25.  
  26.     @Override 
  27.     public boolean onTouchEvent(Component component, TouchEvent touchEvent) { 
  28.         int action = touchEvent.getAction(); 
  29.         boolean isIntercepted = false
  30.         if (action == TouchEvent.PRIMARY_POINT_DOWN) { 
  31.             touchTarget = null
  32.             isIntercepted = interceptTouchEvent(component, touchEvent); 
  33.         } 
  34.         if (isIntercepted) { 
  35.             // intercepted 
  36.             return processTouchEvent(component, touchEvent); 
  37.         } else { 
  38.             if (action == TouchEvent.PRIMARY_POINT_DOWN) { 
  39.                 // down, find touch target 
  40.                 if (component instanceof ComponentContainer) { 
  41.                     ComponentContainer root = (ComponentContainer) component; 
  42.                     int childCount = root.getChildCount(); 
  43.                     for (int i = childCount - 1; i >= 0; i--) { 
  44.                         Component child = root.getComponentAt(i); 
  45.                         if (isTouchInTarget(child, touchEvent)) { 
  46.                             Component.TouchEventListener listener = observers.get(child); 
  47.                             if (listener != null) { 
  48.                                 boolean handled = listener.onTouchEvent(child, touchEvent); 
  49.                                 if (handled) { 
  50.                                     touchTarget = child; 
  51.                                     return true
  52.                                 } 
  53.                             } 
  54.                         } 
  55.                     } 
  56.                 } else { 
  57.                     if (isTouchInTarget(component, touchEvent)) { 
  58.                         Component.TouchEventListener listener = observers.get(component); 
  59.                         if (listener != null) { 
  60.                             boolean handled = listener.onTouchEvent(component, touchEvent); 
  61.                             if (handled) { 
  62.                                 touchTarget = component; 
  63.                                 return true
  64.                             } 
  65.                         } 
  66.                     } 
  67.                 } 
  68.             } 
  69.         } 
  70.         // not find touch target, handle self 
  71.         if (touchTarget == null) { 
  72.             return processTouchEvent(component, touchEvent); 
  73.         } 
  74.         // move, up ... 
  75.         Component.TouchEventListener listener = observers.get(touchTarget); 
  76.         if (listener != null) { 
  77.             return listener.onTouchEvent(touchTarget, touchEvent); 
  78.         } 
  79.         return processTouchEvent(component, touchEvent); 
  80.     } 
  81.  
  82.     public abstract boolean interceptTouchEvent(Component component, TouchEvent touchEvent); 
  83.  
  84.     public abstract boolean processTouchEvent(Component component, TouchEvent touchEvent); 
  85.  
  86.     private boolean isTouchInTarget(Component target, TouchEvent touchEvent) { 
  87.         MmiPoint pointer = touchEvent.getPointerScreenPosition(touchEvent.getIndex()); 
  88.         float touchX = pointer.getX(); 
  89.         float touchY = pointer.getY(); 
  90.         int[] location = target.getLocationOnScreen(); 
  91.         int targetX = location[0]; 
  92.         int targetY = location[1]; 
  93.         int targetWidth = target.getWidth(); 
  94.         int targetHeight = target.getHeight(); 
  95.         boolean result = touchX >= targetX && touchX <= targetX + targetWidth && touchY >= targetY && touchY <= targetY + targetHeight; 
  96.         return result; 
  97.     } 

<CustomContainer.java>

  1. public class CustomContainer extends DirectionalLayout { 
  2.     public CustomContainer(Context context, AttrSet attrSet) { 
  3.         super(context, attrSet); 
  4.         setTouchEventListener(new ExTouchListener(this) { 
  5.  
  6.             @Override 
  7.             public boolean interceptTouchEvent(Component component, TouchEvent touchEvent) { 
  8.                 return false
  9.             } 
  10.  
  11.             @Override 
  12.             public boolean processTouchEvent(Component component, TouchEvent touchEvent) { 
  13.                 switch (touchEvent.getAction()) { 
  14.                     case TouchEvent.PRIMARY_POINT_DOWN: 
  15.                         Logger2.w("--->down"); 
  16.                         return true
  17.                     case TouchEvent.POINT_MOVE: 
  18.                         Logger2.w("--->move"); 
  19.                         return true
  20.                     case TouchEvent.PRIMARY_POINT_UP: 
  21.                         Logger2.w("--->up"); 
  22.                         return true
  23.                 } 
  24.                 return true
  25.             } 
  26.         }); 
  27.     } 

<CustomComponent.java>

  1. public class CustomComponent extends Text { 
  2.     public CustomComponent(Context context, AttrSet attrSet) { 
  3.         super(context, attrSet); 
  4.         setTouchEventListener(new TouchEventListener() { 
  5.             @Override 
  6.             public boolean onTouchEvent(Component component, TouchEvent touchEvent) { 
  7.                 switch (touchEvent.getAction()) { 
  8.                     case TouchEvent.PRIMARY_POINT_DOWN: 
  9.                         Logger2.e( "--->down"); 
  10.                         return true
  11.                     case TouchEvent.POINT_MOVE: 
  12.                         Logger2.e( "--->move"); 
  13.                         return true
  14.                     case TouchEvent.PRIMARY_POINT_UP: 
  15.                         Logger2.e( "--->up"); 
  16.                         return true
  17.                 } 
  18.                 return false
  19.             } 
  20.         }); 
  21.     } 

下面看下3种常见场景的处理打印日志(上面已经贴出全部源码,可以复制进自己的项目运行):

  1. // 父控件不拦截,子控件down事件不消费,父控件的processTouchEvent进行处理 
  2. CustomContainer interceptTouchEvent:false  CustomComponent onTouchEvent down:false 
  3. 08-02 14:42:53.754 17396-17396/? E 0006E/seagazer:  com.example.touch.CustomComponent$1 # onTouchEvent[Line:35]: --->down 
  4. 08-02 14:42:53.754 17396-17396/? D 0006E/seagazer:  com.example.touch.CustomContainer$1 # processTouchEvent[Line:42]: --->down 
  5. 08-02 14:42:53.824 17396-17396/? D 0006E/seagazer:  com.example.touch.CustomContainer$1 # processTouchEvent[Line:48]: --->up 
  6.  
  7. // 父控件不拦截,子控件down事件消费,子控件onTouchEvent处理 
  8. CustomContainer interceptTouchEvent:false  CustomComponent onTouchEvent down:true 
  9. 08-02 14:43:29.132 17661-17661/com.example.touch E 0006E/seagazer:  com.example.touch.CustomComponent$1 # onTouchEvent[Line:35]: --->down 
  10. 08-02 14:43:29.218 17661-17661/com.example.touch E 0006E/seagazer:  com.example.touch.CustomComponent$1 # onTouchEvent[Line:41]: --->up 
  11.  
  12. // 父控件拦截,父控件的processTouchEvent进行处理 
  13. 08-02 14:42:13.409 13918-13918/? W 0006E/seagazer:  com.example.touch.CustomContainer$1 # processTouchEvent[Line:41]: --->down 
  14. 08-02 14:42:13.533 13918-13918/? W 0006E/seagazer:  com.example.touch.CustomContainer$1 # processTouchEvent[Line:47]: --->up 

结语

通过上面的事件托管、事件溯源再传递,就已经能够实现简单的分发拦截机制,并且兼容第三方库的控件。当然,这里主要是提供一种设计的简化模型,包括disptach机制,touchTarget的复用,nestScroll机制本文都没考虑,如果本质上能够理解透彻事件的分发机制,在此基础上进行扩展也不是什么难事。但是回归当下,从个人角度去评判,这类理应该由系统提供的机制,毕竟应用层更多的精力应该放在业务的实现,用户界面交互,应用性能方面,而不是把一些框架层机制自己去实现一遍。

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2020-04-10 10:36:20

网络通信框架

2024-02-21 15:30:56

2023-07-08 00:12:26

框架结构组件

2021-08-11 14:29:20

鸿蒙HarmonyOS应用

2010-08-06 10:24:56

Flex事件分发

2023-10-08 08:23:44

Android事件逻辑

2021-07-27 06:51:53

Istio 微服务Service Mes

2019-07-15 08:43:23

开源技术 工具

2016-12-08 10:19:18

Android事件分发机制

2023-12-25 15:40:37

数据治理大数据GenAI

2010-08-06 10:03:42

Flex事件

2012-05-29 10:44:17

WebApp

2018-06-29 13:24:48

沙箱容器解决方案

2017-06-14 10:12:19

SophixAndroid热修复

2021-12-26 23:34:00

微服务Istio压缩

2017-03-14 13:51:23

AndroidView事件分发和处理

2017-02-21 12:20:20

Android事件分发机制实例解析

2022-06-02 10:35:20

架构驱动

2021-07-23 08:57:32

鸿蒙HarmonyOS应用

2021-07-22 10:20:21

鸿蒙HarmonyOS应用
点赞
收藏

51CTO技术栈公众号