안드로이드 개발, 터치는 어디에나 있습니다. 소스 코드에 대해 잘 모르는 일부 학생의 경우 이에 대해 약간의 의구심을 가질 수 있습니다. View 이벤트의 배포 메커니즘은 비즈니스 요구 사항을 충족할 때 이러한 문제에 직면할 뿐만 아니라 일부 서면 인터뷰 질문에서도 자주 묻는 문제라고 할 수 있습니다. 예전에 이 분야 사람들이 쓴 글을 많이 읽어본 적이 있는데, 내용이 너무 장황하거나 모호하고, 세부 내용에 논란이 있는 부분도 있어서 10분 뒤에 내용을 다시 정리해서 알려드리겠습니다. 이벤트 배포 메커니즘을 확인하세요.
직설적으로 말하면 이러한 터치 이벤트의 이벤트 배포 메커니즘은 3가지 메소드(dispatchTouchEvent(), OnInterceptTouchEvent(), onTouchEvent())를 파악하고 이 3가지 메소드를 n개의 ViewGroup으로 스택하는 문제를 파악하는 것입니다. 및 Views 는 아무리 복잡한 구조라도 1개의 ViewGroup + 1개의 View로 나눌 수 있습니다.
사실 ViewGroup과 View는 비슷합니다. View에는 하위 컨테이너가 없기 때문에 가로채기 문제가 없습니다. Dispatch도 매우 간단하므로 ViewGroup을 이해하시면 거의 이해가 되실 것입니다.
세 가지 주요 방법
public boolean dispatchTouchEvent(MotionEvent ev)
View/ViewGroup은 이벤트 배포의 시작자를 처리합니다. View/ViewGroup은 터치 이벤트를 수신하고 이를 먼저 전달합니다. 이것이 시작되는 메소드이고, 이 메소드에서 차단을 처리할지 아니면 이벤트를 하위 컨테이너에 배포할지 판단합니다.
public boolean onInterceptTouchEvent(MotionEvent ev)
Special for ViewGroup, 이 방법을 사용할 수 있습니다. 일반적으로 이 방법은 이벤트를 ViewGroup에만 제공할지 아니면 하위 컨테이너에 계속 전달할지 결정하는 데 사용할 수 있습니다. 이벤트 충돌 처리
public boolean onTouchEvent(MotionEvent event)
터치 이벤트의 실제 핸들러, 각 이벤트는 결국 여기에서 처리됩니다
핵심 문제
시간 분배 메커니즘의 어려움은 무엇입니까? 포인트: 이벤트를 처리하는 객체를 결정하는 규칙을 호출하는 세 가지 방법과 이벤트 충돌의 해결 방법은 무엇입니까?
이벤트 전달 규칙
일반적으로 한 번의 클릭에 대한 일련의 MotionEvents가 있으며 간단히 다음과 같이 나눌 수 있습니다: 아래->이동->….->이동- >up, one일 때 이벤트가 ViewGroup에 배포될 때 위의 세 메서드 사이의 ViewGroup의 호출 순서는 간단한 코드
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; }
로 표현할 수 있습니다. 결과가 true이면 이벤트가 처리되었음을 의미하고, false를 반환하면 이벤트가 처리되지 않았음을 의미합니다. 위의 코드는 이해하기 쉽고 매우 간단해 보입니다. ViewGroup은 이벤트를 수신한 후 Dispatch를 호출하고, 가로챌 경우 먼저 ViewGroup이 이벤트를 먹습니다. 그렇지 않으면 하위 컨테이너 처리 능력을 갖춘 사람에게 넘겨집니다.
그렇지만 단순함은 이해를 돕기 위해 이렇게 작성했습니다. 물론 ViewGroup의 이벤트 처리 과정은 그다지 간단하지 않습니다. 여기서는 계속해서 추가하겠습니다. 위에서 말한 내용으로 돌아가서, 우리가 자주 처리하는 일련의 이벤트는 일반적으로 하나의 아래, 여러 개의 이동 및 하나의 위의 의사 코드만으로는 이러한 문제를 완벽하게 해결할 수 없습니다. ViewGroup의 dispatchTouchEvent를 직접 살펴보겠습니다.
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; }
위 코드를 설명하면 아주 간단해 보이지만 정말 간단할까요? . 설명하기 전에, 가로채기 == false는 상위 컨테이너 ViewGroup이 이벤트를 일시적으로 가로채지 않는다는 의미이며, 이벤트가 처리를 위해 하위 보기로 전달될 수 있다는 의미입니다. 컨테이너는 일련의 이벤트를 직접 가로채서 나중에 자식 뷰에 전달하지 않습니다. 하위 뷰가 이벤트를 얻으려는 경우 false 값만 만들 수 있습니다
onInterceptTouchEvent 호출은 false를 반환합니다(false를 반환하면 의사의 else 콘텐츠에 해당하는 하위 뷰에 전달될 수 있음). 위의 코드를 하위 컨테이너에 전달하려면 다음 요구 사항을 충족해야 합니다. 내용을 더 잘 이해할 수 있습니다. 두 가지 조건 중 하나라도 충족되면 트리거될 수 있습니다(물론 가능합니다). 🎜>
하나는 다운되었을 때이고, 다른 하나는 mFirstTouchTarget입니다! =null, mFirstTouchTarget이 비어 있지 않은 경우는 언제입니까? 관심 있는 학생은 ViewGroup에서 addTouchTarget 메소드를 호출하는 타이밍을 살펴보세요. mFirstTouchTarget에 값이 할당되어 있으므로 소스 코드는 게시하지 않겠습니다. mFirstTouchTarget은 ACTION_DOWN 이벤트를 소비한 ViewGroup에 하위 뷰를 저장하는 데 사용됩니다. 즉, 위의 의사 코드에서 child.dispatchTouchEvent(ev)는 디스패치가 있는 한 ACTION_DOWN일 때 true를 반환합니다. 하위 뷰가 ACTION_DOWN에 있음 true가 반환되면 null이 아닙니다(이 할당 프로세스는 ACTION_DOWN에서만 발생합니다. 하위 ViewACTION__DOWN이 값을 할당하지 않으면 후속 이벤트 시퀀스가 다시 발생하지 않습니다). 반대로 하위 보기 처리가 없으면 개체는 null이 됩니다. 물론, 위의 두 가지 조건을 만족하는 것만으로는 충분하지 않으며, !disallowIntercept도 충족해야 합니다. disallowIntercept 변수의 값은 주로 FLAG_DISALLOW_INTERCEPT 표시의 영향을 받습니다. 이 값은 ViewGroup의 하위 View가 requestDisallowInterceptTouchEvent 메서드를 호출하면 설정될 수 있습니다. FLAG_DISALLOW_INTERCEPT를 변경하면 disallowIntercept가 발생합니다. 이 경우 차단이 건너뛰어 차단이 실패합니다.但这事还没了,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,处理起来一点难度都木有呀。
一些没说到但也很重要的点
위에서 설명한 내용은 매우 간단합니다. 이벤트 배포 실행자는 ViewGroup입니다. 그러나 실제 개발에서는 그렇게 간단하지 않습니다. 하지만 두려워하지 마세요. 아무리 복잡해도 상황은 이 모드로 나눌 수 있지만 수준은 더 재귀적이고 복잡하며 원칙은 여전히 동일합니다.
몇 가지 추가 사항:
사용자가 화면을 클릭하면 일련의 클릭 이벤트가 트리거될 때 실제 이벤트 전달 프로세스는 Activity(PhoneWindow)-> DecorView->ViewGroup->View, ViewGroup에 도달하기 전에 DecorView가 있습니다. 이벤트는 Activity에서 전달되지만 이는 실제로 ViewGroup과 동일합니다. DecorView에 포함된 자식 View의 누구도 이벤트를 사용할 수 없으면(이렇게 말하는 데 허점이 있습니다. 무슨 뜻인지 이해하세요) 결국 처리를 위해 Activity로 넘겨지게 됩니다.
이벤트 충돌 해결은 위의 원칙에 따라 여러 지점에서 처리될 수 있습니다. 처리 시간을 가장 쉽게 생각할 수 있는 시간은 onInterceptTouchEvent입니다. 예를 들어 수직 슬라이딩 ViewGroup이 수평 슬라이딩 ViewGroup에 중첩된 경우 여기에서 ACTION_MOVE를 사용하여 처리를 위해 후속 이벤트를 전달할 사람을 결정할 수 있습니다. 위에서 언급한 플래그 비트 FLAG_DISALLOW_INTERCEPT에 따르면 하위 뷰의 dispatchTouchEvent와 함께 이벤트 흐름을 제어하는 것이 더 쉽다고 생각됩니다. 그러나 다른 전문가가 MotionEvent 메서드를 공유하여 이벤트 흐름을 제어하는 것을 본 적이 있습니다. 즉, MotionEvent를 사용하고 적절한 시간에 하위 뷰의 사용자 정의 이벤트 처리 방법을 전달하여 이벤트를 공유하는 것도 가능합니다.
일련의 이벤트에서 뷰가 ACTION_DOWN을 거부(false 반환)하는 한 후속 이벤트는 전달되지 않습니다. 그러나 다른 이벤트가 거부되면 후속 이벤트는 계속 전달될 수 있습니다. 예를 들어 View의 특정 ACTION_MOVE는 처리되지 않습니다. 계속해서 뷰에 전달하세요.
ACTION_CANCEL을 올바르게 사용하면 일련의 이벤트 수명 주기를 제어하여 이벤트 처리를 더욱 유연하게 만들 수 있습니다.
이벤트 배포 메커니즘을 이해하려면 기본적으로 위의 원칙을 이해하는 것만으로도 충분합니다. 이를 기반으로 수많은 멋진 마스터들이 작성한 다양한 멋진 사용자 정의 컨트롤의 배포도 볼 수 있습니다. 이해도 볼 수 있습니다. 물론 공간 제한으로 인해 여기서 다루지 않을 확장 기능과 더 자세한 내용이 있습니다. 더 중요한 것은 소스 코드를 읽는 것입니다.
마지막으로 저는 여러분에게 고전적인 명언을 전하고 싶습니다. 종이에서 보는 것은 결국 깨닫게 되지만, 그렇게 해야 한다는 것을 확실히 알게 될 것입니다!
위 내용은 Android View 이벤트 배포 메커니즘에 대한 정보 모음입니다. 앞으로도 관련 정보를 계속 추가하겠습니다.
Android 뷰 이벤트 배포 메커니즘에 대한 자세한 내용은 PHP 중국어 웹사이트를 참고하세요!