一篇文章搞定《Android事件分发》

什么是事件分发

事件分发是将屏幕触控信息分发给控件树的一个套机制。 当我们触摸屏幕时,会产生一系列的MotionEvent事件对象,经过控件树的管理者ViewRootImpl,调用view的dispatchPointerEvnet方法进行分发。
深入学习事件分发机制,是为了解决在Android开发中遇到的滑动冲突问题做准备。事件分发机制描述了用户的手势一系列事件是如何被Android系统传递并消费的。

MotionEvent事件

在MotionEvent.java中我们可以看到这些事件。(有很多事件这里只列举重要常用的几个)

事件 作用
ACTION_DOWN 第一个手指初次接触到屏幕时触发
ACTION_MOVE 手指 在屏幕上滑动 时触发,会多次触发
ACTION_UP 最后一个手指离开屏幕时触发
ACTION_CANCEL 事件被取消或被覆盖(事件被上层拦截了,由父View发送,不是用户自己触发的),也就是非人为触发
ACTION_POINTER_DOWN 主ではない指が押されています(つまり、押す前に指がすでに画面上にありました)
ACTION_POINTER_UP 主要ではない指のリフトがある (つまり、指を持ち上げた後も画面上に指が残っている)

イベントが画面からアプリに送信される仕組み

当面はこの部分を複雑にする必要はありませんが、プロセス全体を知っておく必要があります。当面はページのイベント開発に集中します。後でさらに詳しく知りたい場合は、詳細な調査を行うことができます。

入力マネージャーサービス

まず、Android システムが起動すると、SystemServer プロセス内で AMS や WMS などの一連のシステム サービスが起動されます。その 1 つが
イベント入力を管理する InputManagerService (IMS) です。
InputManagerService: このサービスは、ハードウェアと通信し、画面入力イベントを受け入れるために使用されます。システムが受信したハードウェア ポイントの InputDispatcher スレッドを読み取り、統合されたイベントの配信とスケジューリングを実行します。

ウィンドウマネージャーサービス

Android でのビューの描画とイベント配布はビュー ツリーに基づいています。各ビュー ツリーはウィンドウです。
各ビュー ツリーには ViewRootImpl と呼ばれるルートがあり、ビュー ツリー全体の描画とイベント配布の管理を担当します。
1. したがって、最終的にはイベントの View ツリーの ViewRootImpl に通知する必要があります。
2. Window との通信を管理するのは WindowManagerService (WMS) です
3. 各 viewRootImpl は wms 内に対応する windowState を持ち、wms は管理のために windowState を通じて対応する viewRootImpl を見つけることができます。
4. WMS は、windowState を介して Binder 通信を提供し、関連する必要な Windows 情報を提供し、IMS によって APP 関連イベントに送信されます。 5. InputEventReceiver は入力イベントの受信を担当し、
それを Handler を介して処理するために ViewRootImpl クラスに送信します
。内部クラス ViewRootHandler。Handler から継承され、メインスレッドで動作し、主にタッチ イベント、キー イベントなどのさまざまな入力イベントを処理するために使用されます。
7. 最後にアクティビティに送信されます

1. ウィンドウ メカニズムは、画面上のビューの表示とタッチ イベントの送信を管理します。
2. ウィンドウとは Android のウィンドウの仕組みでは、各ビュー ツリーをウィンドウとみなすことができます。
3. ビューツリーとは何ですか? たとえば、レイアウト内のアクティビティのレイアウト XML を設定すると、LinearLayout などの最上位のレイアウトがビュー ツリーのルートとなり、そこに含まれるすべてのビューがビュー ツリーのノードとなるため、ビュー ツリーは対応します。窓に。

各ビューツリーはウィンドウに対応しており、ビューツリーはウィンドウの存在形式であり、ウィンドウはビューツリーの伝達者であり、上で説明したアプリケーションインターフェイス、ダイアログ、popupWindow、およびフローティングウィンドウはすべてウィンドウの表現です。
いくつかの具体的な例を挙げると、次のようになります。

  • ダイアログを追加するときは、そのビューを設定する必要があります。このビューはアクティビティのレイアウトに属さず、WindowManager を通じて画面に追加され、アクティビティのビュー ツリーには属しません。したがって、このダイアログは独立したビュー ツリーであるため、ウィンドウになります。
  • PopupWindow も windowManager を通じて追加され、Activity のビュー ツリーに属さないため、ウィンドウに対応します。
  • windowManager を使用すると、画面上に追加されたビューは、ボタンのみを追加した場合でも、アクティビティのレイアウト ビュー ツリーに属しません。
    ウィンドウの仕組みを理解する上で重要な理由は、イベントの配布は Activity ではなく、配布用のシステム サービス ドライバー viewRootImpl によって行われるためであり、フレームワーク層の観点から見ると、Activity とはまったく関係がないとさえ言えます。これは、イベント配布の性質を理解するのに役立ちます。

まとめ

1. 指が画面に触れると、位置、圧力、その他の情報を含むタッチ ポイント情報が生成されます。このタッチ情報は、画面のハードウェアによって生成され、システムの基礎となるドライバーによって取得され、Android の入力システム サービス、InputManagerService (IMS) に渡されます。
2. 入力システムは、ウィンドウ マネージャー (WMS) の API を呼び出して、タッチ イベントを配布するウィンドウと対応するビューを決定します。つまり、WMS は View 情報を提供します。
3. IMS は、WMS によって提供された情報を取得し、それを View に対応する ViewRootImpl に送信します。ここでは、InputChannel が双方向通信のための SocketPair の確立を支援していますが、興味があれば、InputChannel の関連内容を確認していただければ、ここでは説明しません。

ここに画像の説明を挿入

APPからイベントが対応するページに到達する仕組み

ステップ 1: 分類

次に、イベントが到着しました。InputEventReceiver は、何をしたかを確認するために ViewRootImpl に通知しました。
いくつかの重要なメソッドを次に示します。

//ViewRootImpl.java ::WindowInputEventReceiver
//1、接收到事件
final class WindowInputEventReceiver extends InputEventReceiver {
    
    
    public void onInputEvent(InputEvent event) {
    
    
        enqueueInputEvent(event, this, 0, true);
    }
}

//ViewRootImpl.java
//2、简单处理掉用doProcessInputEvents进行分类
void enqueueInputEvent(InputEvent event,
                       InputEventReceiver receiver, int flags, boolean processImmediately) {
    
    
    adjustInputEventForCompatibility(event);
    .....
    .....
    .....
    if (processImmediately) {
    
    
        doProcessInputEvents();
    } else {
    
    
        scheduleProcessInputEvents();
    }
}

//3、维护了输入事件队列
void doProcessInputEvents() {
    
    
   .....
    while (mPendingInputEventHead != null) {
    
    
        QueuedInputEvent q = mPendingInputEventHead;
        mPendingInputEventHead = q.mNext;
        deliverInputEvent(q);
    }
    ......
}

//调用InputStage责任链处理分类
private void deliverInputEvent(QueuedInputEvent q) {
    
    
    InputStage stage;
    ....
    //stage赋值操作
    ....
    if (stage != null) {
    
    
        stage.deliver(q);
    } else {
    
    
        //事件分发完成后会调用finishInputEvent,告知SystemServer进程的InputDispatcher线程,
        //最终将该事件移除,完成此次事件的分发消费。
        finishInputEvent(q);
    }
}

abstract class InputStage {
    
    
    .....
    public final void deliver(QueuedInputEvent q) {
    
    
        if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
    
    
            forward(q);
        } else if (shouldDropInputEvent(q)) {
    
    
            finish(q, false);
        } else {
    
    
            traceEvent(q, Trace.TRACE_TAG_VIEW);
            final int result;
            try {
    
    
                result = onProcess(q);
            } finally {
    
    
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            apply(q, result);
        }
    }
}

ここで、ViewRootImpl がビュー入力イベント、入力メソッド イベント、ナビゲーション パネル イベントなどの時間をさらに分類していることがわかります。では、InputStage の責任の連鎖は具体的にどこで生成され、どのような種類があるのでしょうか?
setView メソッドで答えを得ることができます

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    
    
    synchronized (this) {
    
    
       ...
        mSyntheticInputStage = new SyntheticInputStage();
        InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
        InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
                "aq:native-post-ime:" + counterSuffix);
        InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
        InputStage imeStage = new ImeInputStage(earlyPostImeStage,
                "aq:ime:" + counterSuffix);
        InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
        InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
                "aq:native-pre-ime:" + counterSuffix);
     ....
    }
}

setView メソッド内で、この入力イベント処理に対する一連の責任が完了していることがわかります。

責任連鎖のメンバー 効果
合成入力ステージ ナビゲーション パネル、ジョイスティックなどのイベントを処理します。
ViewPostImeInputStage キーストローク、指のタッチ、その他のモーション イベントなどのビュー入力処理段階。共通のビュー イベントの配布はこの段階で発生します。
NativePostImeInputStage ローカルメソッド処理段階では、主に遅延キューが構築されます。
EarlyPostImeInputStage 入力方法の初期処理段階
ImeInputStage インプットメソッドイベント処理ステージ、インプットメソッド文字の処理
ViewPreImeInputStage 前処理入力メソッドのイベントフェーズを表示する
NativePreImeInputStage ネイティブメソッド前処理入力メソッドイベントフェーズ

したがって、最初のステップは、InputStage を通じてイベントを分類して配布することです。View タッチ イベントはViewPostImeInputStageステージで発生します。そこで、イベントを ViewPostImeInputStage に分類します。

ステップ 2: アクティビティに送信する

ビューの階層は次のとおりです。Activity -> PhoneWindow -> DecorView
通常、setContentView を呼び出してレイアウトを描画すると、DecorView の ContentView にも描画されます。
ここに画像の説明を挿入
次に、イベントがページ イベントの最初の Activety にどのように渡されるかを確認する必要があります。
まず第一に、上で述べたように、すべてのイベントは ViewPostImeInputStage の責任チェーンで処理さます

//ViewRootImpl.java ::ViewPostImeInputStage 
final class ViewPostImeInputStage extends InputStage {
    
    
    .....
    .....
    private int processPointerEvent(QueuedInputEvent q) {
    
    
        final MotionEvent event = (MotionEvent)q.mEvent;
        boolean handled = mView.dispatchPointerEvent(event)
        return handled ? FINISH_HANDLED : FORWARD;
    }

    //View.java
    public final boolean dispatchPointerEvent(MotionEvent event) {
    
    
        if (event.isTouchEvent()) {
    
    
            return dispatchTouchEvent(event);
        } else {
    
    
            return dispatchGenericMotionEvent(event);
        }
    }


最終的には、まだ mView.dispatchPointerEvent(event) に到達し、ViewRootImpl の mView は DecorView であることがわかります。
これで、イベントがインターフェイスのルート レイアウトである DecorView に渡されました。

//DecorView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    
    
    //Callback是我们的Activity和Dialog
    final Window.Callback cb = mWindow.getCallback();
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
            ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

上のcb は、レイアウトを作成したときに作成したアクティビティであり、
その後、そのアクティビティ内で、dispatchTouchEvent を見つけることができます。これについては誰もがよく知っています。
getCallback を通じてアクティビティに時間を渡し、アクティビティをページ イベントの始まりにする大きな裏切り者 DecorView です。

その後の配送

アクティビティでのdispatchTouchEventとその後の配信を見てみましょう

//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    
    
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    
    
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
    
    
        return true;
    }
    return onTouchEvent(ev);
}

//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    
    
    return mDecor.superDispatchTouchEvent(event);
}

//DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
    
    
    return super.dispatchTouchEvent(event);
}

最終的に、プロセス全体がViewRootImpl -> DecorView -> Activity -> PhoneWindow -> DecorView としてDecorView に戻されることがわかりました。

質問: そこで質問は、なぜ ViewRootImpl がアクティビティから直接イベントの処理を開始しないのかということです。
回答: ViewRootImpl は、Activity のようなものが存在することを知りません。DecorView を保持するだけです。したがって、タッチイベントを Activity.dispatchTouchEvent() に直接送信することはできません。

質問: では、CB がない場合、つまりアクティビティがない場合はどうすればよいでしょうか。else が super.dispatchTouchEvent(ev) を呼び出していることがわかります;
回答: したがって、トップレベルの viewGroup が DecorView でない場合は、対応するビューのdispatchTouchEvent メソッドをディスパッチします。たとえば、トップレベルのビューが Button の場合、Button のdispatchTouchEvent メソッドが直接呼び出されます。トップレベルの viewGroup サブクラスがdispatchTouchEvent メソッドをオーバーライドしない場合は、ViewGroup のデフォルトのdispatchTouchEvent メソッドが直接呼び出されます。

質問: なぜアクティビティは DecorView を直接呼び出さないのですか
? 回答: PhoneWindow によって維持される DecorView をアクティビティが維持しないためです。

まとめ:

1. まず、イベントは、 InputEventReceiverによって受信され、処理のためにViewRootImplに送信され、最初に分類されます
2.
Viewの処理クラス
として、 ViewRootImplがイベントの処理と View の管理を担当します3. イベントはViewPostImeInputStageに分類されます4. mView は、
レイアウトの DecorView ルート レイアウトです
5. ViewRootImpl -> DecorView -> Activity -> PhoneWindow -> DecorView を介して、最後にページ ViewGroup のイベント配布に渡されます
6. PhoneWindow には、単純な最後の説明

ページイベント配信

DecorViewはFrameLayoutを継承していますが、FrameLayoutはdispatchTouchEventメソッドを書き換えないため、ViewGroupクラスのメソッドが呼び出されます。したがって、ここでは、イベントが ViewGroup に渡されて、コントロール ツリーに配布されます。
もちろん、ViewGroup には多数のビューが含まれています。

プロセス全体

上記のプロセスを組み合わせると、プロセス全体は次のようになります。
ViewRootImpl -> DecorView -> Activity ->PhoneWindow -> DecorView -> ViewGroup -> View
ただし、アプリケーション開発で最後に焦点を当てるのは、Activity から開始することです。または ViewGroup のイベント処理から。
以下では、アクティビティを例として説明します。

アクティビティから始める

Activity -> PhoneWindow -> DecorView -> ViewGroup -> View
このうち、ページ上にいるときに PhoneWindow -> DecorView を処理する必要がないため、プロセスは
Activity -> ViewGroup -> View
に簡略化できます。すぐに馴染みますか?昔ながらのアクティビティ -> ビューグループ -> ビュー

一連の出来事

指が最初に画面に触れた瞬間から指が画面を離れるまでの過程で生成される一連のイベントを指します。通常、このシーケンスはダウン イベントで始まり、途中に複数の移動イベントが含まれ、最後にダウン イベントで終了します。アップイベント

ソースコード分析

ここではソース コードの簡単な分析を示します。読むのは難しくなく、コードもそれほど多くありません。主にプロセスの重要なメソッドを理解するためのものです。

アクティビティによるイベントの処理

//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    
      
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    
      
            onUserInteraction();  
        }  
        if (getWindow().superDispatchTouchEvent(ev)) {
    
      
            return true;  
        }  
        return onTouchEvent(ev);  
}

ソースコードを見ると、Activityに付属しているWindowにイベントが渡されて配信されていることが分かりますが、trueを返すとイベントの配信が終了します、それ以外の場合は、すべてのViewのonTouchEventがfalseを返します(どれも処理されません)このとき、ActivityのonTouchEventを扱います。

ウィンドウによるイベントの処理

Window の唯一の実装クラスは PhoneWindow です

//PhoneWindow.java
@Override  
public boolean superDispatchTouchEvent(MotionEvent event) {
    
      
    return mDecor.superDispatchTouchEvent(event);  
}

ソース コードからわかるように、イベントは処理のために DecorView に渡されます。引き続き DecorView を見てみましょう。

DecorView によるイベントの処理

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker{
    
    }
 
public boolean superDispatchTouchEvent(MotionEvent event) {
    
    
    return super.dispatchTouchEvent(event);
}

DecorView は FrameLayout から継承されており、FrameLayout が ViewGroup を継承していることは誰もが知っているため、次のレベルは ViewGroup です。

ViewGroup によるイベントの処理

DispatchTouchEvent のコア コードは次のとおりです:
Activity と View と比較して、ViewGroup には追加の onInterceptTouchEvent() イベント インターセプト メソッドがあり、イベントは ViewGroup に渡されます。onInterceptTouchEvent が true を返した場合、イベントは ViewGroup によって処理されます。falseが返された場合、子ViewのdispatchTouchEventが呼び出されます。

// 检查是否进行事件拦截  
final boolean intercepted;  
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    
      
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (!disallowIntercept) {
    
      
    //回调onInterceptTouchEvent(),返回false表示不拦截touch,否则拦截touch事件 
    intercepted = onInterceptTouchEvent(ev);  
    ev.setAction(action);
  } else {
    
      
      intercepted = false;  
     }  
} else {
    
      
   //没有touch事件的传递对象,同时该动作不是初始动作down,ViewGroup继续拦截事件  
   intercepted = true;
}

ViewGroup の onInterceptTouchEvent が false を返すと、最初にすべての子要素を走査して、子要素がクリック イベントを受信できるかどうかを判断します。子要素にイベントを受信する条件がある場合、そのdispatchTouchEventが呼び出されます。すべての子要素が走査されてfalseが返された場合、ViewGroupは単独でイベントを処理することしかできません。子要素のこのメソッドから true を返すと、子要素のトラバースが停止します。

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;
    }

ビューによるイベントの処理

その後、最後の View のイベントが処理され、
View はイベントを引き継ぐことができなくなり、一連の判断の後、イベントは最終的に onTouchEvent を通じて消費されます。

public boolean dispatchTouchEvent(MotionEvent event) {
    
      
boolean result = false;
//...
    if (onFilterTouchEventForSecurity(event)) {
    
      
       //noinspection SimplifiableIfStatement  
       ListenerInfo li = mListenerInfo;  
       //会执行View的OnTouchListener.onTouch这个函数,若返回true,onTouchEvent便不会再被调用了。
       //可见OnTouchListener比onTouchEvent优先级更高。
       if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED  
                    && li.mOnTouchListener.onTouch(this, event)) {
    
      
         return true; }  
  
       if (onTouchEvent(event)) {
    
      
            return true; 
       }  
}
    //…
    return result;  
}

ちょっとした知識: 判定には li.mOnTouchListener.onTouch(this,event) も使用されます。したがって、OnTouchListener は onTouchEvent よりも優先されます。ここにもそれが反映されています。

ページ配布プロセス

くだらないことを言わないで、すぐに U 字型フローチャートに進みましょう。決まりきったフローチャートはすべて悪い使い方をしています。
ここに画像の説明を挿入注: 画像に惑わされないでください。ViewGroup には onTouchEvent はありません。ViewGroup は View を継承しているため、ViewGroup の onTouchEvent は View にあります。

dispatchTouchEvent() : クリックイベントを配布します。

dispatchTouchEvent()メソッドはイベント配信の中核となるメソッドであり、Activity、ViewGroup、Viewで実装されるメソッドです。イベント配信では、まずアクティビティのdispatchTouchEvent()メソッドにイベントが渡され、その後親コンテナと子ビューのdispatchTouchEvent()メソッドにイベントが渡され、状況に応じた処理が行われます。イベントが消費されると、イベント配信は直ちに停止されます。それ以外の場合は、最後のビューに配信されます。ビューがイベントを処理した場合は true を返し、それ以外の場合は false を返します。

onInterceptTouchEvent(): ViewGroup レイヤーでクリック イベントをインターセプトします

onInterceptTouchEvent() メソッドは ViewGroup のメソッドであり、その主な機能は ViewGroup 内のサブ View の TouchEvent をインターセプトすること、つまり、サブ View の TouchEvent をインターセプトすることです。ViewGroup が TouchEvent をインターセプトすると、対応する子 View は TouchEvent を受信できません。

onTouchEvent(): クリック イベントを処理します。

onTouchEvent() メソッドは View のメソッドであり、その主な機能は、View がクリックされた、View がドラッグされたなど、View の TouchEvent を処理することです。View が TouchEvent を受信すると、onTouchEvent() メソッドを通じて TouchEvent に応答します。ビューがイベントを処理した場合は true を返し、それ以外の場合は false を返します。

まとめ

概要は画像に基づいています。画像と比較する必要があります(必ず画像を見てください。そうでないと文字を読みたくないでしょう) 1. まず、消費は、時間がここまでであることを意味し
ます
2. 制御メソッドをチェックして戻り値を書き換えたり変更したりせず、スーパーを直接使用して親クラスのデフォルト実装を呼び出す場合、イベント フロー全体はアクティビティから行われる必要があります-->ViewGroup->View を使用して、上から下にリーフ ノード (View) に至るまで、dispatchTouchEvent メソッドを呼び出します。その後、View->ViewGroup->Activity によって下から上に onTouchEvent メソッドを呼び出します。
3.dispatchTouchEvent、onTouchEventの場合、trueを返すとイベント配信が終了します。return false は、親ビューの onTouchEvent メソッドにバックトラックします。
5. onTouchEvent が false を返すことは、イベントを消費せず、イベントが親コントロールの方向に下から上に流れ続けることを意味します。
6. ViewGroup がそれ自体を独自の onTouchEvent に配布したい場合は、イベントをインターセプトするためにインターセプターの onInterceptTouchEvent メソッドが true を返す必要があります。
7. View にはインターセプターがありません View がイベントを独自の onTouchEvent に配布できるようにするために、View のdispatchTouchEvent のデフォルトの実装 (スーパー) では、イベントを独自の onTouchEvent に配布します。

ACTION_MOVE と ACTION_UP

onTouchEvent でイベントを消費する状況の場合: View の onTouchEvent が true を返す場合、ACTION_MOVEおよび ACTION_UP イベントはこの View に渡された後は渡されなくなり、独自の onTouchEvent に直接渡され、今回のイベント配信プロセスが終了します。 。
たとえば、
ViewGroup2 の onInterceptTouchEvent で true を返してこのイベントをインターセプトし、ViewGroup 1 の onTouchEvent で true を返してこのイベントを消費します。
赤い矢印は、ACTION_DOWN イベントのフローの方向を表します。
青い矢印は、ACTION_MOVE および ACTION_UP イベントのフローの方向を表します。ACTION_MOVE
ここに画像の説明を挿入
および ACTION_UP が、イベントが消費される ViewGroup1 に渡されなくなることがわかります。処理のために ViewGroup1 の onTouchEvent に直接返されます。

キャンセルイベントの説明

ACTION_CANCEL イベントはいつトリガーされますか?
まず、親 View から子 View に Cancel イベントが通知されます。
Cancel イベントをトリガーするには 2 つの方法があります:
1. ViewGroup がイベントをインターセプトして消費する
2. View が ViewGroup から削除される

上記 2 つのケースでは、View が必要なときに Down イベントが発生することを確認する必要があります。消費される。
2 つの例を挙げます:
例 1: ViewGroup はイベントをインターセプトして消費します。ScrollView

では、一般にジェスチャの優先順位はスクロール > クリックです。Scrollview
のようなスクロール可能なコントロールでは、操作を押した後に指がスライドし続けると、前の子コントロールがキャンセルイベントを送信するポイント

これは、指が押されたときにユーザーが上下にスライドし続けると、ScrollView のスクロール操作がトリガーされ、ScrollView も子コントロールのクリック イベントに応答するためです。この時点でユーザーが上または下にスワイプを続けると、競合が発生します。ScrollView はスクロールしたいのですが、子コントロールもクリック イベントを処理したいと考えています。

この競合を回避するために、ScrollView は、ユーザーが押してスライドを続けると、前にクリックした子コントロールに Cancel イベントを送信し、前のクリック操作をキャンセルするように通知します。このようにして、ScrollView はスムーズにスクロールでき、子コントロールが誤って処理することはありません。

つまり、ScrollView サブビューは ScrollView ViewGroup によってインターセプトされます。ScrollView のソース コードを見ると、

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    
    
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
    
    
        return true;
    }

    if (super.onInterceptTouchEvent(ev)) {
    
    
        return true;
    }
    ....
    ....
    switch (action & MotionEvent.ACTION_MASK) {
    
    
        case MotionEvent.ACTION_MOVE: {
    
    
            if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
    
    
                mIsBeingDragged = true;
                mLastMotionY = y;
                initVelocityTrackerIfNotExists();
                mVelocityTracker.addMovement(ev);
                mNestedYOffset = 0;
                if (mScrollStrictSpan == null) {
    
    
                    mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                }
                final ViewParent parent = getParent();
                if (parent != null) {
    
    
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }
            break;
        }
        ....
        ....    
    }
    return mIsBeingDragged;
}

ACTION_MOVEの処理の中で、If判定を見て解釈してみましょう。

指の垂直方向の移動距離がシステムのデフォルトの最小タッチ距離 (mTouchSlop) より大きく、入れ子になったスライド軸 (NestedScrolling) に垂直スクロールがない場合は、ScrollView をドラッグ中としてマークします (mIsBeingDragged = true)。
現在の指の位置 (mLastMotionY = y) を記録し、ユーザーのスワイプ速度を追跡するために速度トラッカーを初期化します (initVelocityTrackerIfNotExists())。さらに、NestedYOffset を 0 に設定して、ネストされたスライドを処理できるように準備します。

つまり、ACTION_MOVEがスライディング状態であると判定された後、mIsBeingDragged = trueとなる。つまり、onInterceptTouchEvent の戻り値は true になります。
onInterceptTouchEvent が true を返した場合、イベントはこのレイヤーの ViewGroup でインターセプトされ、消費のために現在の ViewGroup の onTouchEvent に送信されることを上で説明しました。つまり、イベントは ScrollView に消費されます。

概要: ScrollView はスライド時にイベントをインターセプトするため、子 View に Cancel イベントを送信します。

例 2: View が ViewGroup から削除される
自分でテストするのは比較的簡単です:

View を指で押して、3 秒後に ViewGroup から View を削除します。次に、onTouchEvent のイベントが返されることを観察します。

viewGroup.postDelayed(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            getWindowManager().removeView(getWindow().getDecorView());
        }
    }, 3000);

イベント配布に関する一般的な問題

入れ子になったスライド

複数のスライディング コントロールが同時に存在する場合、スライディング イベントが互いに干渉する可能性があるため、スライディングの競合の問題を解決するにはイベント分散メカニズムが必要です。
ネストされたスライディングについては、実際の使用例を交えて説明する別の記事を書く予定です。
例:
1. ScrollView + ListView (RecyclerView) ネストの競合
2. ScrollView + ViewPager ネストの問題
3. RecyclerView + RecyclerView のネストされた同じ方向と異なる方向のスライド
4. モール APP の共通ホームページには複数のリストがあり、タブがあります頂上で

マルチタッチ

マルチタッチでは指が絡み合い、無秩序なイベント配信が発生する可能性があるため、イベント配信メカニズムを通じてマルチタッチ イベントを処理する必要があります。

onTouch、onTouchEvent、onLongClick、onClick の関係

1. onTouch の優先度は、 onTouchEvent の優先度よりも高くなりますViewのdispatchTouchEventはonTouchListner.onTouch()で判定されるため、上記のソースコードはViewのイベント処理で説明しました。View の OnTouchListener.onTouch 関数が true を返した場合、onTouchEvent は再度呼び出されません。もちろん、onClick イベントと Long イベントは応答しなくなります。
2. OnClick の優先順位が最も低くなります。onClick は onTouchEvent のメソッド内にあります。次のコードがその証拠です。 MotionEvent.ACTION_UP では、クリックが PerformClick( によって実行されることがわかります) )
View の onTouchEvent 内。

case MotionEvent.ACTION_UP:
        .....
        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    
    
            // This is a tap, so remove the longpress check
            removeLongPressCallback();
            if (!focusTaken) {
    
    
                .....
                if (mPerformClick == null) {
    
    
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
    
    
                    performClickInternal();
                }
            }
        }

トレースバックできるperformClick()をクリックします。
li.mOnClickListener.onClick(this); がonClickイベントをトリガーすることがわかります
。そのため、onTouchEventはonClick()よりも優先されます。

 public boolean performClick() {
    
    
        notifyAutofillManagerOnClick();
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
    
    
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
    
    
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }

3. onLongClick > onClickonLongClick が true を返すと、onClick がブロックされることは誰もが知っています。何が起こっているのか、ソース コードを見てみましょう:
LongClick のトリガーは ACTION_DOWN から始まり、CheckForLongClick() メソッドによって完了します。onClick
() メソッドは View の PerformClick() メソッドでコールバックされます
。onLongClickListener の onLongClick が呼び出されます。 View の PerformLongClickInternal メソッドに戻ります。() メソッド

private final class CheckForLongPress implements Runnable {
    
    
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

        @Override
        public void run() {
    
    
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
    
    
                if (performLongClick(mX, mY)) {
    
    
                    mHasPerformedLongPress = true;
                }
            }
        }
        ...
    }

PerformLongClick が true を返すと、mHasPerformedLongPress = true であることがわかります; 注意深い学生は、私が mHasPerformedLongPress を見たことがわかるかもしれません。それは正しい!
これは、上記のビューの onTouchEvent で PerformClick() を呼び出す if 条件にあります。

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    
    
    // This is a tap, so remove the longpress check
    removeLongPressCallback();
    // Only perform take click actions if we were in the pressed state
    if (!focusTaken) {
    
    
        if (!post(mPerformClick)) {
    
    
            performClick();
        }
    }
}

つまり、performLongClick は true を返します。これが機能しない場合は、performClick() は呼び出されず、当然 onClick も呼び出すことができません。
4. 概要: onTouch > onTuchEvent > onLongClick > onClick

View イベント配信での有効化の影響

View に Enable = false を設定すると、View はイベントに応答しなくなります。つまり、onTouch を含め、上記の 4 つのイベントのいずれにも応答しません。
では、イベントはどのように処理されるのでしょうか? どのような影響を受けましたか?
ソースコードに直接アクセスしてください!

public boolean dispatchTouchEvent(MotionEvent event) {
    
    
		.........
		.........
        if (onFilterTouchEventForSecurity(event)) {
    
    
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
    
    
                result = true;
            }
           
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
    
    
                result = true;
            }

            if (!result && onTouchEvent(event)) {
    
    
                result = true;
            }
        }

見えますか?(mViewFlags & ENABLED_MASK) == ENABLED = false の場合、li.mOnTouchListener.onTouch(this,event) にはもう行かなくなります。
onTouch ボスは呼び出されなくなりました。その後、弟がおならをしました。

ここで皆さんに質問です。 View に Enable = false が設定されている場合。イベントはどこで消費されますか?
皆さん、宿題は取っておきましょう。

要約する

要約はありませんが、じっくり読んでいただければ幸いです。コンテンツが豊富なので、まとめて読むことができます。入れ子のスライドについては次の記事で説明します。
問題を見つけた場合は、以下にコメントしてください。一緒に進歩しましょう。

おすすめ

転載: blog.csdn.net/weixin_45112340/article/details/130863139