HarmonyOS自定义控件的触摸事件及事件分发通过Listener的方式:setTouchEventListener(newTouchEventListener(){@OverridepublicbooleanonTouchEvent(Componentcomponent,TouchEventtouchEvent){returnfalse;}});注意:setTouchEventListener会被常见类型的触摸事件覆盖这里我们将其他主流系统中的MotionEvent与HarmonyOS中的TouchEvent进行比较,便于理解和记忆。MotionEvent的常用的事件类型与HarmonyOS中的TouchEvent类型基本可以对应起来:MotionEvent.ACTION_CANCEL->TouchEvent.CANCELMotionEvent.ACTION_HOVER_ENTER->TouchEvent.HOVER_POINTER_ENTERMotionEvent.ACTION_HOVER_EXIT->TouchEvent.HOVER_POINTER_EXITMotionEvent.ACTION_HOVER_MOVE->TouchEvent.HOVER_POINTER_MOVEMotionEvent.ACTION_POINTER_DOWN->TouchEvent.OTHER_POINT_DOWNMotionEvent.ACTION_POINTER_UP->TouchEvent.OTHER_POINT_UPMotionEvent.ACTION_MOVE->TouchEvent.POINT_MOVEMotionEvent.ACTION_DOWN->TouchEvent.PRIMARY_POINT_DOWNMotionEvent.ACTION_UP->TouchEvent.PRIMARY_POINT_DOWNMotionEvent.ACTION_UP->TouchEvent.PRIMARY_POINT_UP常用应用程序getApi获取的Api获取==TouchEvent.PRIMARY_POINT_DOWN获取手指相对于屏幕的x、y坐标手指相对于父控件的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,则后续事件不会冒泡,直接被目标控件消费。如果某个控件在按下事件中返回false,则后续事件将不会传递给该控件。如果某个控件收到按下事件并返回true,则后续事件将直接传递给该控件,其他控件将不会收到该事件。HarmonyOS中的事件传递更像是冒泡,而不是分发,down事件一旦被某个控件消费,其他控件就不会再收到后续事件了。这样的机制比较难实现一些复杂的嵌套效果。比如子控件响应水平滑动,父控件响应垂直滑动。如果子控件要接收后续的move事件,只能在down的时候返回true,这样父控件就根本接收不到touch事件了。如果子控件在移动时要判断滑动方向,而down事件返回false,则子控件将不再接收后续事件。HarmonyOS的事件冒泡比较简单。一旦达成协议,就没有反悔的机会。那么如何像其他主流系统一样从顶层控件分发和拦截事件呢?这里只是一个思路,具体代码可以参考:Eventdistribution实现事件分发我们概念中的事件分发应该是这样的:事件先到顶层parent控件再向下层分发通过dispatchTouchEvent层。ComponentContainer可以通过onInterceptTouchEvent拦截事件,交给自己的onTouchEvent处理。如果ComponentContainer没有处理事件,它会继续向下分发,直到最终的Component控件。这样的机制意味着每一层都有机会获取事件,那么在HarmonyOS中如何实现呢?我们可以将事件分发相关的函数和代码提取出来,移植到HarmonyOS中,在onTouchEvent中通过某种方式应用到HarmonyOS中。摘要HarmonyOS中没有dispatchTouchEvent、onInterceptTouchEvent等函数。如何将它们应用于组件?抽象接口,将事件分发相关的函数抽象为两个接口:View/***事件分发基础接口。需要分发和处理事件的组件需要实现这个接口*/publicinterfaceView{/***将屏幕的触摸事件传递给目标控件或者自己消费**@paramevent传递的触摸事件*@return如果事件被消费自己,返回true,否则返回false*/booleandispatchTouchEvent(TouchEventevent);/***处理触摸事件的方法*@paramevent要消费的事件*@return是否消费事件*/booleanonTouchEvent(TouchEventevent);/***事件是否被自己消费,结果只能获取一次,获取后会重置为false*@return事件是否被消费*/booleanisConsumed();}ViewGroup/***包含事件分发接口子控件的,需要拦截或者分发事件的ComponentContainer需要实现这个接口*/publicinterfaceViewGroupextendsView{/***当子控件不希望父控件通过{@link#o拦截事件nInterceptTouchEvent(TouchEvent)},调用这个方法*@paramdisallowIntercept如果子控件不想让父控件拦截事件,传true*/voidrequestDisallowInterceptTouchEvent(booleandisallowIntercept);/***当需要拦截所有touch时实现这个方法事件。*注意:如果后面需要拦截事件,down事件不要返回true,否则由于事件冒泡机制,子控件在down后收不到事件。**当此方法返回true时,事件将传递给onTouchEvent()方法。如果onTouchEvent返回true,*后续事件会继续到控件的onTouchEvent()方法,不会传递到onInterceptTouchEvent()中。**当该方法返回false时,事件会先传递给onInterceptTouchEvent(),再传递给子控件的onTouchEvent()。*一旦该方法返回true,子控件就会收到最后一个CANCEL事件,该事件将不再传递给onInterceptTouchEvent,*而是直接传递给自己的onTouchEvent。**@paramev向下传递触摸事件*@return当需要拦截子控件的触摸事件时,返回true,事件将传递给{@linkView#onTouchEvent(TouchEvent)}。*子控件会收到CANCEL事件,后续事件不会传递给控件。*/booleanonInterceptTouchEvent(TouchEventev);}实现然后使用两个辅助类实现两个接口中的相关功能。将View中事件分发的具体代码封装成ViewHelper,将ViewGroup中事件分发的具体代码封装成ViewGroupHelper。代码参考了ViewHelper和ViewGroupHelper分布。最后使用一个分发助手类DispatchHelper,根据ViewGroupHelper中的dispatchTouchEvent,从顶层分发HarmonyOS中的事件。DispatchHelper主要做了以下事情:在缓存当前事件时,视图树中所有实现了View和ViewGroup接口的控件从最顶层控件开始,调用它的dispatchTouchEvent函数过滤掉可能因为事件冒泡而传递的重复事件code:/***事件分发助手类,协助{@linkView}和{@linkViewGroup}分发事件。**在{@linkComponent.TouchEventListener#onTouchEvent(Component,TouchEvent)}*调用{@link#dispatch(Component,TouchEvent)}以分派事件。*@paramcomponent控件需要分发的事件*@paramtouchEvent需要分发的事件*@returnEvent处理结果*/publicstaticbooleandispatch(Componentcomponent,TouchEventtouchEvent){//过滤由于bottom-up事件冒泡和top-down事件产生的重复分发分配机制if(isSameEvent(touchEvent)){returntrue;}//修正getPointerPosition得到的y坐标的偏移量compact.correct(touchEvent);lastEvent=convertEvent(touchEvent);intaction=touchEvent.getAction();if(action==TouchEvent.PRIMARY_POINT_DOWN){clearNodes();}if(nodes.size()<=0)createNodes(component);dispatch(nodes.size(),1,touchEvent);//collectRecords();//booleanresult=findRecord(component);if(action==TouchEvent.PRIMARY_POINT_UP){clearNodes();}returntrue;}/***当子控件不想让父控件拦截事件时,调用这个方法**@paramcomponent不想事件被拦截的控件*@paramdisallowIntercepttrue不拦截*/publicstaticvoidrequestDisallowInterceptTouchEvent(Componentcomponent,booleandisallowIntercept){if(component.getComponentParent()instanceofViewGroup){((ViewGroup)component.getComponentParent()).requestDisallowInterceptTouchEvent(;allow}拦截***当子控件不想父控件拦截事件,在{@linkEv在entHandler中调用这个方法#postTask(Runnable)}**@paramcomponent不希望事件被拦截*@paramdisallowIntercepttrue不被拦截*/publicstaticvoidpostRequestDisallowInterceptTouchEvent(Componentcomponent,booleandisallowIntercept){EventHandlerhandler=newEventHandler(EventRunner.getMainEventRunner.p)(()->requestDisallowInterceptTouchEvent(component,disallowIntercept));}publicstaticTouchEventCompactgetTouchEventCompact(){returncompact;}/***自上而下的事件分发,如果最顶层的父控件没有实现{@linkViewGroup},则找下??一个A实现{@linkViewGroup}分发事件的控件**@paramsize{@link#nodes}的大小*@parami求实现{@linkViewGroup}的控件个数,初值为1,自增*@paramtouchEvent传递的事件*结果@return事件分布*/privatestaticbooleandispatch(intsize,inti,TouchEventtouchEvent){booleanresult=false;if(size>0){Componentnode=nodes.get(size-i);if(nodeinstanceofViewGroup){ViewGroupgroup=(ViewGroup)node;结果=group.dispatchTouchEvent(touchEvent);}elseif(nodeinstanceofView){Viewview=(View)node;result=view.dispatchTouchEvent(touchEvent);}else{if(i
