触摸事件
如何监听触摸事件
HarmonyOS中可以通过Listener的方式:
- setTouchEventListener(new TouchEventListener() {
- @Override
- public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
- return false;
- }
- });
注意:setTouchEventListener会被覆盖
常用的触摸事件的类型
这里我们对比其他主流系统中MotionEvent与HarmonyOS中TouchEvent来方便理解与记忆。
MotionEvent的常用的事件类型与HarmonyOS中的TouchEvent类型基本可以对应起来:
- MotionEvent.ACTION_CANCEL -> TouchEvent.CANCEL
- MotionEvent.ACTION_HOVER_ENTER -> TouchEvent.HOVER_POINTER_ENTER
- MotionEvent.ACTION_HOVER_EXIT -> TouchEvent.HOVER_POINTER_EXIT
- MotionEvent.ACTION_HOVER_MOVE -> TouchEvent.HOVER_POINTER_MOVE
- MotionEvent.ACTION_POINTER_DOWN -> TouchEvent.OTHER_POINT_DOWN
- MotionEvent.ACTION_POINTER_UP -> TouchEvent.OTHER_POINT_UP
- MotionEvent.ACTION_MOVE -> TouchEvent.POINT_MOVE
- MotionEvent.ACTION_DOWN -> TouchEvent.PRIMARY_POINT_DOWN
- MotionEvent.ACTION_UP -> TouchEvent.PRIMARY_POINT_UP
常用的Api
获取事件类型
- touchEvent.getAction() == TouchEvent.PRIMARY_POINT_DOWN
获取手指相对于屏幕的x、y坐标
- touchEvent.getPointerScreenPosition(touchEvent.getIndex()).getX();
- touchEvent.getPointerScreenPosition(touchEvent.getIndex()).getY();
获取手指相对于父控件的x、y坐标
- touchEvent.getPointerPosition(touchEvent.getIndex()).getX();
- touchEvent.getPointerPosition(touchEvent.getIndex()).getY();
getPointerScreenPosition与getPointerPosition的区别
前者是相对的屏幕的坐标,而后者是相对于父控件的坐标。如果在手指滑动过程中,对该控件做了位移,那么getPointerPosition获取的坐标将会是手指本身坐标加上控件的位移量,导致位移异常。
这里建议,如果需要根据坐标来计算,都使用getPointerScreenPosition比较保险。
总结
TouchEvent提供了基础api,但是没有MotionEvent内一些比较高阶的api,比如obtain等。接下来我们来关注更为重要的事件分发。
事件分发
事件分发是一套比较重要同时也比较复杂的机制,如果不熟悉这套机制,那么在遇到稍微复杂的滑动失效问题就会觉得手足无措。在这里通过打印日志的方式来摸索HarmonyOS上的事件的传递机制。
HarmonyOS中事件的传递机制
首先,我们通过打印日志的方式,来摸索触摸事件是如何在Component中传递的。经过实验,发现如下几条规律:
- 事件首先会传递到最底层的目标控件,而非顶层的父控件
- 如果目标控件不处理该事件,即onTouchEvent返回false,那么事件冒泡到父控件
- 如果目标控件处理了该事件,即onTouchEvent返回true,那么后续事件不会向上冒泡,而是直接被目标控件消费
- 如果一个控件在down事件中,返回了false,那么后续的事件也不会被传递到该控件中
- 如果一个控件接受到了down事件,并返回了true,那么后续的事件会直接被传递到该控件中,其他控件不会收到事件
HarmonyOS中的事件传递更像是冒泡,而非分发,down事件一旦被某一个控件消费了,那么其他控件将都收不到后续事件了。这样的机制比较难去实现一些复杂的嵌套效果。
比如子控件响应横向滑动,父控件响应垂直滑动这种情况。子控件如果要想收到后续的move事件,只能在down的时候返回true,这样就导致父控件完全收不到触摸事件。子控件如果像要在move时判断滑动方向而down事件返回了false,那么子控件将再也接收不到后续的事件了。
HarmonyOS的事件冒泡比较简单,一旦约定好就再也没有反悔的机会了。那么如何类似其他主流系统一样,从顶层控件分发并且可以拦截事件呢?
这里只提供思路,具体代码可以参考:事件分发
实现事件分发
我们构想中的事件分发应该是这样:事件是首先到顶层的父控件,然后经过dispatchTouchEvent一层层向下分发。ComponentContainer可以通过onInterceptTouchEvent拦截事件,并交给自己的onTouchEvent来处理。如果ComponentContainer不处理事件则继续向下分发,直到最终的Component控件。这样的机制意味着每一层都有机会能拿到事件,那么如何在HarmonyOS中实现呢?
我们可以将事件分发相关的函数与代码,抽取出来,移植到HarmonyOS中,并通过一些手段应用到HarmonyOS的onTouchEvent中。
抽象
HarmonyOS中没有dispatchTouchEvent、onInterceptTouchEvent等函数,如何应用到组件中呢?抽象接口,将事件分发相关的函数抽象成两个接口:
View
-
- public interface View {
-
- boolean dispatchTouchEvent(TouchEvent event);
-
-
- boolean onTouchEvent(TouchEvent event);
-
-
- boolean isConsumed();
- }
ViewGroup
-
- public interface ViewGroup extends View {
-
-
- void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
-
-
- boolean onInterceptTouchEvent(TouchEvent ev);
- }
实现
然后借助两个帮助类,来实现两个接口中的相关函数。将View中事件分发的具体代码封装到ViewHelper中,将ViewGroup中事件分发的具体代码封装到ViewGroupHelper中。
代码参考ViewHelper、ViewGroupHelper
分发
最后借助一个分发帮助类DispatchHelper,来将HarmonyOS中的事件,从顶层开始按照ViewGroupHelper中的dispatchTouchEvent来分发。
DispatchHelper主要做了下面几件事:
- 缓存当次事件中,视图树内所有实现了View、ViewGroup接口的控件
- 从最顶层的控件开始,调用其dispatchTouchEvent函数
- 过滤掉由于事件冒泡,而传递过来的可能的重复事件
代码:
-
- public class DispatchHelper {
-
-
- private static final List
nodes = new ArrayList<>(); -
- private static final HashMap<Integer, Boolean> records = new HashMap<>();
-
-
- private static String lastEvent = "";
- private final static TouchEventCompact compact = new TouchEventCompact(true);
-
-
- public static boolean dispatch(Component component, TouchEvent touchEvent) {
- // 过滤由于自下而上的事件冒泡 与 自上而下的事件分发机制而产生的重复分发
- if (isSameEvent(touchEvent)) {
- return true;
- }
-
- // 纠正通过getPointerPosition获取的y坐标的偏移
- compact.correct(touchEvent);
-
- lastEvent = convertEvent(touchEvent);
-
- int action = touchEvent.getAction();
- if (action == TouchEvent.PRIMARY_POINT_DOWN) {
- clearNodes();
- }
-
- if (nodes.size() <= 0) createNodes(component);
- dispatch(nodes.size(), 1, touchEvent);
- // collectRecords();
-
- // boolean result = findRecord(component);
-
- if (action == TouchEvent.PRIMARY_POINT_UP) {
- clearNodes();
- }
-
- return true;
- }
-
-
- public static void requestDisallowInterceptTouchEvent(Component component, boolean disallowIntercept) {
- if (component.getComponentParent() instanceof ViewGroup) {
- ((ViewGroup) component.getComponentParent()).requestDisallowInterceptTouchEvent(disallowIntercept);
- }
- }
-
-
- public static void postRequestDisallowInterceptTouchEvent(Component component, boolean disallowIntercept) {
- EventHandler handler = new EventHandler(EventRunner.getMainEventRunner());
- handler.postTask(() -> requestDisallowInterceptTouchEvent(component, disallowIntercept));
- }
-
- public static TouchEventCompact getTouchEventCompact() {
- return compact;
- }
-
-
- private static boolean dispatch(int size, int i, TouchEvent touchEvent) {
- boolean result = false;
- if (size > 0) {
- Component node = nodes.get(size - i);
- if (node instanceof ViewGroup) {
- ViewGroup group = (ViewGroup) node;
- result = group.dispatchTouchEvent(touchEvent);
- } else if (node instanceof View) {
- View view = (View) node;
- result = view.dispatchTouchEvent(touchEvent);
- } else {
- if (i < size) {
- i++;
- result = dispatch(size, i, touchEvent);
- }
- }
- }
-
- return result;
- }
-
- private static void collectRecords() {
- records.clear();
- for (int i = 0; i < nodes.size(); i++) {
- records.put(i, ((View) nodes.get(i)).isConsumed());
- }
- }
-
- private static boolean findRecord(Component component) {
- int i = nodes.indexOf(component);
- if (i < 0) return false;
- return records.get(i);
- }
-
- private static void clearNodes() {
- nodes.clear();
- }
-
- private static void createNodes(Component component) {
- if (component instanceof View) nodes.add(component);
- if (component.getComponentParent() != null) {
- createNodes((Component) component.getComponentParent());
- }
- }
-
- private static String convertEvent(TouchEvent event) {
- String split = ",";
- MmiPoint point = event.getPointerScreenPosition(event.getIndex());
- return event.getAction() + split + point.getX() + split +
- point.getY() + split + event.hashCode();
- }
-
- private static boolean isSameEvent(TouchEvent event) {
- return lastEvent.equals(convertEvent(event));
- }
- }
使用方式
参考文档
注意事项
- 虽然能使用事件分发了,但是由于底层机制的不同,在使用上还是会有一些差别:
- 如果根布局或者中间的ComponentContainer实现的是View而非ViewGroup,那么事件将不会继续往下传递。
- 视图树中间可以出现断层,即出现未实现View或ViewGroup的控件,事件会跳过并往下传递。
- 未实现View或ViewGroup的控件,如果设置了setTouchEventListener,那么事件将在回调返回true后直接被消费,而导致不会被分发。
- 如果遇到super.onTouchEvent或者super.onInterceptTouchEvent,需要去父类查看逻辑并移植进来,如果是普通的布局或者控件一般是可以忽略,或者返回false的。
- 如果遇到super.dispatchTouchEvent则可以直接使用ViewGroupHelper/ViewHelper的dispatchTouchEvent来替代。
- 暂时只支持单点触摸的分发