Développement Android, le tactile est partout. Pour certains étudiants qui ne connaissent pas grand-chose au code source, ils auront des doutes à ce sujet. Le mécanisme de distribution des événements View rencontrera non seulement ces problèmes lorsqu'il répondra aux exigences commerciales, mais il sera également souvent posé dans certaines questions d'entretien écrit. Cela peut être considéré comme un cliché. J'ai déjà lu de nombreux articles écrits par des personnes dans ce domaine. Ils sont soit trop longs, soit trop vagues, et certains détails sont controversés, je vais donc réorganiser à nouveau ce contenu et vous le faire comprendre dans dix minutes. mécanisme de répartition.
Pour parler franchement, le mécanisme de distribution d'événements de ces événements tactiles consiste à comprendre les trois méthodes, dispatchTouchEvent(), OnInterceptTouchEvent(), onTouchEvent(), et le problème de l'empilement de ces trois méthodes avec n ViewGroups et Views , quelle que soit la complexité de la structure, elle peut être divisée en 1 ViewGroup et 1 View.
En fait, ViewGroup et View sont très similaires. View n'a tout simplement pas de sous-conteneurs, il n'y a donc pas de problème d'interception. Dispatch est également très simple, donc une fois que vous aurez compris ViewGroup, vous le comprendrez presque.
Trois méthodes clés
public boolean dispatchTouchEvent(MotionEvent ev)
View/ViewGroup gère l'initiateur de la distribution d'événements View/ViewGroup reçoit l'événement tactile et le distribue en premier. C'est la méthode qui démarre, puis dans cette méthode, on juge s'il faut traiter l'interception ou distribuer l'événement au sous-conteneur
public boolean onInterceptTouchEvent(MotionEvent ev)
Spécial pour ViewGroup, cette méthode peut être réalisée. Le sens de distribution des événements de contrôle. Généralement, cette méthode peut être utilisée pour déterminer s'il faut donner l'événement au ViewGroup seul ou continuer à le transmettre au sous-conteneur. gérer les conflits d'événements
public boolean onTouchEvent (événement MotionEvent)
Le véritable gestionnaire des événements tactiles, chaque événement sera traité ici à la fin
Le problème principal
Quelles sont les difficultés du mécanisme de distribution du temps ? Je pense que les difficultés sont les suivantes Point : Trois règles d'appel de méthode pour déterminer l'objet qui gère l'événement et la résolution du conflit d'événement.
Règles de livraison des événements
Généralement, il y aura une série de MotionEvents pour un clic, qui peuvent être simplement divisés en : bas->move->….->move- >up, quand un Lorsque l'événement est distribué au ViewGroup, la séquence d'appel dans le ViewGroup entre les trois méthodes ci-dessus peut être exprimée par un simple morceau de code
MotionEvent ev;//down or move or up or others... viewgroup.dispatchTouchEvent(ev); public boolean dispatchTouchEvent(MotionEvent ev){ boolean isConsumed = false; if(onInterceptTouchEvent(ev)){ isCousumed = this.onTouchEvent(ev); }else{ isConsumed = childView.dispatchTouchEvent(ev); } return isConsumed; }
Renvoyer le résultat true signifie que l'événement a été traité, renvoyer false signifie aucun traitement. Le code ci-dessus est facile à comprendre et semble très simple.Il peut être résumé en une phrase.Une fois que le ViewGroup a reçu l'événement, il appelle dispatch, il vérifie d'abord s'il est intercepté, le ViewGroup mange l'événement. Sinon, il est remis à une personne disposant de capacités de traitement en sous-conteneur.
Cependant, la simplicité est la simplicité. C'est écrit comme ceci juste pour faciliter la compréhension. Bien sûr, le processus de traitement des événements de ViewGroup n'est pas si simple. De nombreux détails sont ignorés ici. Pour revenir à ce qui a été dit ci-dessus, une série d'événements que nous traitons souvent sont généralement un vers le bas, plusieurs mouvements et un vers le haut. Le pseudocode ci-dessus ne peut pas à lui seul résoudre parfaitement ces problèmes. Regardons directement le dispatchTouchEvent de ViewGroup.
Condition d'appel onInterceptTouchEvent
final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
Expliquez le code ci-dessus, cela semble très simple, mais est-ce vraiment simple. . Avant d'expliquer, parlons de la signification de intercepted == false signifie que le conteneur parent ViewGroup n'intercepte pas temporairement l'événement et que l'événement a la possibilité d'être transmis à la vue enfant pour traitement. Le conteneur a directement intercepté la série d'événements et ne les transmettra pas plus tard. Donnez-la à la vue enfant. Si la sous-vue veut obtenir l'événement, elle ne peut que rendre la valeur false
l'appel onInterceptTouchEvent renvoie false (le renvoi de false peut être transmis à la sous-vue, ce qui correspond au contenu else du pseudo code ci-dessus. Afin de transmettre l'événement au sous-conteneur, les conditions suivantes doivent être remplies : Le contenu est mieux compris) Il peut être déclenché si l'une des deux conditions est remplie (bien sûr, cela n'est que possible) :
L'un est lorsqu'il est en panne, et l'autre est mFirstTouchTarget ! =null, quand mFirstTouchTarget n'est-il pas vide ? Les étudiants intéressés peuvent consulter le moment de l'appel de la méthode addTouchTarget dans ViewGroup. mFirstTouchTarget se voit attribuer une valeur ici. Le code source est trop long et je ne le publierai pas.
mFirstTouchTarget est utilisé pour enregistrer la vue enfant dans le ViewGroup qui a consommé l'événement ACTION_DOWN, c'est-à-dire que dans le pseudocode ci-dessus, child.dispatchTouchEvent(ev) renvoie true lorsque ACTION_DOWN est dans ACTION_DOWN, tant que le l'envoi de la vue enfant est dans ACTION_DOWN Si true est renvoyé, il ne sera pas nul (ce processus d'affectation ne se produit que dans ACTION_DOWN. Si la sous-ViewACTION__DOWN ne lui attribue pas de valeur, la séquence d'événements suivante ne se reproduira plus). Au contraire, s'il n'y a pas de traitement sous-Vue, l'objet sera nul. Bien sûr, il ne suffit pas de remplir les deux conditions ci-dessus, vous devez également remplir !disallowIntercept.
La variable disallowIntercept est très intéressante. Sa valeur est principalement affectée par le flag FLAG_DISALLOW_INTERCEPT. Cette valeur peut être définie par la sous-View de ViewGroup si la sous-View de ViewGroup appelle la méthode requestDisallowInterceptTouchEvent. modifiez FLAG_DISALLOW_INTERCEPT, ce qui entraîne disallowIntercept. La valeur est vraie. Dans ce cas, l'interception sera ignorée, provoquant l'échec de l'interception.
但这事还没了,FLAG_DISALLOW_INTERCEPT这个标记有一个重置的机制,查看ViewGroup源码可以看到,在处理MotionEvent.ACTION_DOWN的时候会重置这个标记导致disallowIntercept失效,是不是丧心病狂,上面的一段这么简单的代码有这么多幺蛾子,这里还能得到一个结论,ACTION_DOWN的时候肯定可以执行onInterceptTouchEvent的。
所以拦截的intercepted很重要,能影响到底是让ViewGroup还子View处理这个事件。
上面的两个有可能触发拦截的条件说完了,那么当两个条件都不满足的话就不会再调用拦截了(拦截很重要,一般ViewGroup都返回false这样能把事件传递给子View,如果在ACTION_DOWN时不能走到OnInterceptTouchEvent并返回false告诉ViewGroup不要拦截,则事件再也不能传给子View了,所以拦截一般都是要走到的,而且一般都是返回false这样能让子View有机会处理),这种情况一般都是在ACTION_DOWN处理完之后没有子View当接盘侠消费ACTION_DOWN以及后续事件,从上面的伪代码可以看出来,这时候ViewGroup自己就很被动了,需要自己来调用onTouchEvent来处理,这锅就自己背了。
再继续说一下mFirstTouchTarget和intercepted是怎么影响事件方向的。看源码:
if (!canceled && !intercepted) { .... if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { .... for(child : childList){ if(!child satisfied condition....){ continue; } newTouchTarget = addTouchTarget(child, idBitsToAssign);//在这里给mFirstTouchTarget赋值 } } }
可以在这里看到intercepted为false在ACTION_DOWN里才能给上面说过的mFirstTouchTarget赋值,只有mFirstTouchTarget不为空才能让后续事件传递给子View,否则根据上上面说的代码后续事件只能给父容器处理了。
mFirstTouchTarget就是我们后续事件传递的对象,很容易理解,如果在ACTION_DOWN中没有确定这个对象,则后续事件不知道传递给谁自然就交给父容器ViewGroup处理了,真正处理事件传递的方法是dispatchTransformedTouchEvent,再看源码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Canceling motions is a special case. We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } }
看到没,只要参数里传的child为空,则ViewGroup调用super.dispatchTouchEvent(event),super是谁,ViewGroup继承自View,当然是View咯,View的dispatch调用的谁?当然是自己的onTouchEvent(后面会说),所以这个最后还是调用了ViewGroup自己的onTouchEvent。
那么当child!=null的时候呢,调用的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup则继续按照上面的伪代码执行事件分发,如果也是View则调用自己的onTouchEvent。
所以,说到底事件到底给谁处理,还是和传进来的child有关,那这个方法在哪里调用的呢,继续看:
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { ... dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits) }
这就是为什么mFirstTouchTarget能影响事件分发的方向的原因。就这样,整个伪代码的流程是不是很清楚了。
这里需要多说两句,在上上面代码流程中,intercepted决定了这个事件会不会调用ViewGroup的onTouchEvent,当intercepted为true则后续流程会调用ViewGroup的onTouchEvent,仔细看上面的代码能发现,只有两种情况为ture:一是调用了InterceptTouchEvent把事件拦截下来,另一个就是没有一个子View能够消费ActionDown。只有这两种情况父容器ViewGroup才会自己处理
那么问题来了,思考一个问题:如果子View处理了ACTION_DOWN但后续事件都返回false,这些没有被处理的事件最后传给谁处理了?各位思考之,后面再说这个问题。
孩子是谁的
继续来扩展我们的伪代码,拦截条件判断完之后,决定把事件继续传递给子View的时候,会调用childView.dispatchTouchEvent(ev),问题来了,child是哪来的,继续看源码、
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; }
ViewGroup通过判断所有的子View是否可见是否在播放动画和是否在点击范围内来决定它是否能够有资格接受事件。只有满足条件的child才能够调用dispatch。
再看伪代码,最后dispatch返回ViewGroup的isConsumed,若isConsume == true,说明ViewGroup处理了这个点击事件(ViewGroup自身或者子View处理的),并且这个系列的点击事件会继续传到这个ViewGroup来处理,若isConsume == false(ACTION_DOWN时),ViewGroup没办法处理这个点击事件,那么这个系类的点击事件就和该ViewGroup无缘了。会把这个事件上抛给自己的父容器或者Activity处理。
伪代码说完了,ViewGroup的事件传递规则也就差不多说完了,这么看是不是很简单了。View相对于ViewGroup来说就更简单了,没有拦截方法,dispatch基本上是直接调用了自身的onTouchEvent,处理起来一点难度都木有呀。
一些没说到但也很重要的点
Les choses expliquées ci-dessus sont très simples. Cela commence avec un ViewGroup et une View. L'exécuteur de la distribution des événements est ViewGroup. Le sous-conteneur n'a qu'une seule View. Mais bien sûr, ce n'est pas si simple dans le développement réel, mais. n'ayez pas peur, aussi compliqué soit-il. La situation peut également être divisée dans ce mode, mais les niveaux sont plus récursifs et plus compliqués, et le principe est toujours le même.
Quelques points supplémentaires en passant :
Lorsqu'une série d'événements de clic est déclenchée par le clic de l'utilisateur sur l'écran, le processus réel de transmission de l'événement est : Activité (PhoneWindow) -> DecorView->ViewGroup->View, il y a un DecorView avant d'atteindre le ViewGroup. L'événement est transmis à partir de l'activité, mais ces éléments sont en fait les mêmes que le ViewGroup. L'activité peut être considérée comme un grand ViewGroup. les enfants contenus dans son DecorView Lorsque personne dans la vue ne peut consommer l'événement (il y a une faille à dire cela, comprenez simplement ce que je veux dire), il sera finalement transmis à l'activité pour traitement.
La résolution des conflits d'événements peut être traitée en plusieurs points selon les principes ci-dessus. Le moment le plus simple pour penser au temps de traitement est dans onInterceptTouchEvent. Par exemple, lorsqu'un ViewGroup coulissant verticalement est imbriqué dans un ViewGroup coulissant horizontalement, vous pouvez utiliser ACTION_MOVE ici pour déterminer à qui doit transmettre l'événement suivant pour le traitement. peut également Selon le bit indicateur FLAG_DISALLOW_INTERCEPT mentionné ci-dessus, il est plus facile de penser à contrôler le flux d'événements en conjonction avec le dispatchTouchEvent de la vue enfant. Cependant, j'ai vu d'autres experts contrôler le flux d'événements en partageant la méthode MotionEvent. , c'est-à-dire en l'enregistrant dans le conteneur parent. Il est également possible d'utiliser MotionEvent et de transmettre la méthode de gestion des événements personnalisée de la vue enfant au moment approprié pour partager les événements.
Tant qu'une vue rejette ACTION_DOWN (renvoie false) dans une série d'événements, les événements suivants ne seront pas ignorés. Mais si d'autres événements sont rejetés, les événements suivants peuvent toujours être transmis. Par exemple, un certain ACTION_MOVE de View n'est pas traité. Cet événement non traité sera finalement consommé par Activity (pas le conteneur parent de View), mais les événements suivants le seront toujours. Continuez à le transmettre à la vue.
Une utilisation appropriée de ACTION_CANCEL peut contrôler le cycle de vie d'une série d'événements, rendant le traitement des événements plus flexible.
Pour comprendre le mécanisme de distribution des événements, il suffit fondamentalement de comprendre les principes ci-dessus. Vous pouvez également voir la distribution de divers contrôles personnalisés sympas écrits par de nombreux maîtres géniaux sur github. vous pouvez également voir Comprendre, bien sûr, il y a beaucoup d'extensions et de contenu plus approfondi que je n'aborderai pas ici en raison du manque d'espace. Le plus important est de regarder le code source.
Enfin, j'aimerais vous citer une citation classique : Ce que vous voyez sur papier finira par vous le faire réaliser, mais vous saurez certainement que vous devez le faire !
Ce qui précède est la collecte d'informations sur le mécanisme de distribution d'événements Android View. Nous continuerons à ajouter des informations pertinentes à l'avenir. Merci pour votre soutien à ce site !
Pour des articles plus détaillés sur le mécanisme de distribution d'événements Android View, veuillez faire attention au site Web PHP chinois !