ホームページ  >  記事  >  Java  >  Android Viewのイベント配信仕組みの詳細説明

Android Viewのイベント配信仕組みの詳細説明

高洛峰
高洛峰オリジナル
2017-01-16 16:33:331044ブラウズ

Android 向けに開発されており、タッチはどこにでもあります。ソースコードについてあまり知らない学生にとっては、これについて疑問を持つ人もいるでしょう。 View イベントの配布メカニズムは、ビジネス要件を満たすときにこれらの問題に遭遇するだけでなく、面接のいくつかの質問でもよく聞かれる決まり文句であると言えます。この分野の人々が書いた多くの記事を以前に読んだことがありますが、内容が長すぎたり、曖昧すぎたりするため、この内容をもう一度整理して、10 分で理解できるようにします。配布メカニズム。

率直に言うと、これらのタッチ イベントのイベント分散メカニズムは、dispatchTouchEvent()、OnInterceptTouchEvent()、onTouchEvent() の 3 つのメソッドを理解することと、これら 3 つのメソッドを n 個の ViewGroups および View とスタックする問題を解決することです。どれだけ複雑か 構造は 1 ViewGroup + 1 View に分割できます。

実際、ViewGroup と View は非常に似ています。View にはサブコンテナがないだけで、Dispatch も非常に単純なので、ViewGroup を理解すればほぼ理解できるでしょう。

3つの主要なメソッド

public booleandispatchTouchEvent(MotionEvent ev)

View/ViewGroupがタッチイベントを受信すると、最初にこのメソッドが呼び出され、その後このメソッド内でイベント配信の開始かどうかが判断されます。インターセプトを処理するか、サブコンテナにイベントを配布する

public boolean onInterceptTouchEvent(MotionEvent ev)

ViewGroup のみ、このメソッドでイベントを ViewGroup に渡すかどうかを判断できます。単独で、または子コンテナに渡され続けます。これは、イベントの競合を処理するのに最適な場所です

public boolean onTouchEvent(MotionEvent event)

タッチ イベントの実際のハンドラ、そして最終的にすべてのイベントがここで処理されます

中核問題

時間 分散メカニズムの難しさは何ですか? 難しさは次のとおりだと思います: 3 つのメソッド呼び出しルール、イベントを処理するオブジェクトの決定、およびイベントの競合の解決策。

イベント配信ルール

通常、ワンクリックで一連の MotionEvent が発生します。これは、イベントが発生した場合、down->move->up に単純に分割できます。 ViewGroup に配布される、上記 3 つのメソッド間の 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 がイベントを受信した後、ディスパッチの際に、インターセプトするかどうかを最初に確認します。それ以外の場合は、サブコンテナ処理能力を持つ誰かに引き渡されます。

ただし、単純さは単純さです。もちろん、ViewGroup のイベント処理プロセスはそれほど単純ではありません。ここでは引き続き説明します。上で述べたことに戻りますが、私たちがよく扱う一連のイベントは、通常、1 つのダウン、複数の移動、1 つのアップです。上記の疑似コードだけでは、これらの問題を完全に解決することはできません。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;
}

上記のコードを説明すると、とても簡単そうに見えますが、本当に簡単なのでしょうか? 。説明する前に、intercepted == false は、親コンテナー ViewGroup がイベントを一時的にインターセプトしないことを意味し、イベントが処理のために子 View に渡される機会があることを意味します。 true を返すことは、親コンテナーが処理することを意味します。コンテナは一連のイベントを直接インターセプトし、後でそれを子ビューに渡しません。サブビューがイベントを取得したい場合、値を false にすることしかできません

onInterceptTouchEvent 呼び出しは false を返します (false を返すことは、上記の疑似コードの else のコンテンツに対応するサブビューに渡すことができます。満たす必要があるサブコンテナにイベントを渡す方が良いです。何かを理解してください) 2 つの条件のいずれかが満たされた場合にトリガーされる可能性があります (もちろん、それは可能です):

1 つは、ダウンしたときです。もう 1 つは mFirstTouchTarget! =null、mFirstTouchTarget が空でない場合は、ViewGroup で addTouchTarget メソッドを呼び出すタイミングを参照してください。ソース コードは長すぎるため、投稿しません。

mFirstTouchTarget は、ACTION_DOWN イベントを消費した ViewGroup に子 View を保存するために使用されます。つまり、上記の疑似コードでは、ACTION_DOWN が使用されている限り、child.dispatchTouchEvent(ev) は true を返します。 ACTION_DOWN が使用されている場合は true を返しますが、null にはなりません (この割り当てプロセスは ACTION_DOWN でのみ発生します。サブ ViewACTION__DOWN がそれに値を割り当てない場合、逆に、後続のイベントは再度発生しません)。サブビュー処理がない場合、オブジェクトは null になります。もちろん、上記 2 つの条件を満たすだけでは十分ではなく、!disallowIntercept も満たす必要があります。

変数 disallowIntercept は、主にフラグ FLAG_DISALLOW_INTERCEPT の影響を受けます。この値は、ViewGroup のサブビューによって requestDisallowInterceptTouchEvent メソッドが呼び出されることで、FLAG_DISALLOW_INTERCEPT が変更されます。 disallowIntercept の値を true に設定すると、インターセプトがスキップされ、インターセプトが失敗します。

但这事还没了,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 + View から始まります。サブコンテナには View が 1 つしかありません。ただし、実際の開発ではそう単純ではありません。状況がどれほど複雑であっても心配する必要はありません。このモードに分割することもできますが、レベルはより再帰的でより複雑になります。原則は同じです。

ちなみに、いくつかの追加ポイント:

ユーザーが画面をクリックすることで一連のクリック イベントがトリガーされる場合、実際のイベント配信プロセスは次のようになります: Activity (PhoneWindow)->DecorView->ViewGroup->View ViewGroup に到達する前に DecorView もあり、Activity からイベントが渡されますが、これらは実際には ViewGroup と同じであり、その DecorView に含まれるすべてのサブビューに誰もアクセスできない場合に、大きな ViewGroup と見なすことができます。イベントを消費します (これを言うには抜け穴があります。私が言いたいことを理解してください。) 最終的には、処理のためにアクティビティに渡されます。

イベントの競合解決は、上記の原則に従っていくつかのポイントで処理できます。処理時間を考えるのに最も簡単なのは onInterceptTouchEvent です。たとえば、垂直方向にスライドする ViewGroup が水平方向にスライドする ViewGroup にネストされている場合、ここで ACTION_MOVE を使用して、後続のイベントを処理のために誰に渡すかを決定できます。前述のフラグ ビット FLAG_DISALLOW_INTERCEPT によれば、子 View のdispatchTouchEvent と組み合わせてイベント フローを制御することを考えるのが簡単ですが、他の専門家が MotionEvent メソッドを共有することでイベント フローを制御しているのを見てきました。つまり、MotionEvent を使用して、適切なタイミングで子 View のカスタム イベント処理メソッドを渡してイベントを共有することも可能です。

いずれかの View が一連のイベントで ACTION_DOWN を拒否する (false を返す) 限り、後続のイベントは引き継がれません。ただし、他のイベントが拒否された場合でも、後続のイベントは引き続き渡されます。たとえば、この未処理のイベントは最終的に (View の親コンテナーではなく) アクティビティによって消費されます。引き続きビューに渡します。

ACTION_CANCEL を適切に使用すると、一連のイベントのライフサイクルを制御でき、イベント処理がより柔軟になります。

イベント配布の仕組みを理解するには、基本的に上記の原則を理解するだけで十分です。もちろん、github には数多くの素晴らしいマスターが作成したさまざまなカスタム コントロールの配布も理解できます。詳細 スペースの制約のため、ここでは拡張された内容やより詳細な内容は詳しく説明しません。さらに重要なのは、ソース コードを読むことです。
最後に、古典的な言葉を贈りたいと思います。紙に書いたものはいずれ薄っぺらいものになりますが、それをやらなければならないことは必ずわかります。

上記は Android View イベント配信メカニズムに関する情報をまとめたものです。今後も関連情報を追加していきます。

Android View イベント配信メカニズムに関する詳細な記事については、PHP 中国語 Web サイトに注目してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。