banner
fwrite

fwrite

好好生活
twitter
github
email

Android View イベント配信

イベントの伝達#

イベント分配.drawio

イベントは、Activity から View に段階的に伝達されます。

イベント分配の概念#

主に 3 つのメソッドで実現されます。

分配の重点メソッド機能戻り値の意味
タッチイベントのディスパッチ:ブール値上層の View によってトリガーされ、ターゲット View に伝達される戻り値は、子 View の影響を受ける(true:処理された)onTouchEvent``dispatchTouchEvent
onInterceptTouchEvent:boolean現在の View 内で、特定のイベントをインターセプトするかどうかを判断戻り値は、そのイベントがインターセプトされたかどうかを示す(true:インターセプトされた)
タッチイベント:ブール値現在の View がインターセプトし、処理を開始することを示す戻り値は、そのイベントが消費されたかどうかを示す(true:処理された)
public boolean dispatchTouchEvent(MotionEvent e) {
    bool isEventConsume = false;

    if(onInterceptTouchEvent(e)) {     
        isEventConsume = onTouchEvent(e);
    } else {
        isEventConsume = child.dispatchTouchEvent(e);
    }

    return isEventConsume;
}

分配の順序は dispatchTouchEvent -> onInterceptTouchEvent であり、消費されたかどうかに基づいてその後の進行を決定します。dispatchTouchEvent の再帰呼び出しは、消費イベントの View が見つかるまで続きます。View は木構造であり、もしその View がクリックイベントをインターセプトした場合、onTouch がトリガーされます。onTouch がそのイベントを消費しなければ、onTouchEvent メソッドに渡されます。onTouchEvent 内で onClick イベントが発生します。

Activity がイベントを受信#

//Activity.java
 
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // ここでのgetWindowはPhoneWindowクラスです
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}


//PhoneWindow.java
    
private DecorView mDecor;

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

//DecorView.java
//DecorViewはFrameLayoutを継承しており、FrameLayoutはdispatchTouchEventメソッドをオーバーライドしていないため、
//親クラスを探す必要があります。
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

// ViewGroup.java
// ViewGroupはViewを継承しています。
// ViewGroupはdispatchTouchEventをオーバーライドしているため、Viewに進む必要はありません。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
}

イベント処理#

上記の分析を通じて、クリックイベントがどのようにDecorView#ViewGroupに伝達されるかがわかります。ここでは、ViewGroup のクリックイベントをさらに分析します。
まず、いくつかの MotionEvent を理解します。

MotionEvent イベントアクションその他
ACTION_DOWN指が下に押されるインターセプト、非インターセプトに分かれる
ACTION_UP指が上に上がるイベントが終了
ACTION_MOVE画面上をスライド複数回トリガーされる
ACTION_CANCELイベントがキャンセルされる上層によってインターセプトされたときにトリガーされる

ViewGroup 処理 - ChildView がACTION_DOWNをインターセプト#

Down イベントは一度だけトリガーされます(単一タッチ、マルチタッチでは複数回トリガーされます)。インターセプトとは、その ViewGroup が自分でイベントを処理することであり、ChildView には分配されません。
各クリックイベントは Action Down から始まります。詳細は以下の注釈に注意してください。主に行うことは(ここでは主な処理項目を列挙します)。
以前のイベントをクリアします:resetTouchState メソッドに注意してください。これは ViewGroup#dispatchTouchEvent イベントが ACTION_DOWN のときに実行されます。

Note

以前のイベントをクリアします:resetTouchState メソッドに注意してください。これは ViewGroup#dispatchTouchEvent イベントが ACTION_DOWN のときに実行されます。

// ViewGroup.java
    
// このViewGroupから始まり、クリックのターゲットに接続します。
private TouchTarget mFirstTouchTarget;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {

        // 現在のアクションを取得
        final int action = ev.getAction();
        // マスクとAND操作を行い、実際のアクションを取得
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Action Downのときにのみresetを実行
            
            // @ cancelAndClearTouchTargetsメソッドを追跡
            cancelAndClearTouchTargets(ev);
            
            // @ resetTouchStateメソッドを追跡
            resetTouchState();
        }

    }

    ...

    return handled;
}

private void resetTouchState() {
    // @ clearTouchTargetsメソッドを確認
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    
    // FLAG_DISALLOW_INTERCEPTをクリアし、ViewGroupがインターセプトできるようにする
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

// 最初のクリックViewのすべてのイベントをクリアします
private void clearTouchTargets() {
    // TouchTargetは単方向リストです
    TouchTarget target = mFirstTouchTarget;
    if (target != null) {
        do {
            TouchTarget next = target.next;
            // TouchTargetを再利用できるように回収します
            target.recycle();
            target = next;
        } while (target != null);
        
        // メンバーmFirstTouchTargetをnullに設定します
        mFirstTouchTarget = null;
    }
}

ViewGroup#onInterceptTouchEventメソッドが呼び出されるかどうか:2 つの条件があり、さらに 1 つの FLAG の判断があります:

  • 現在は ACTION_DOWN イベントです
  • すでに ChildView がこのクリックイベントを処理しています(子 View がイベントを処理すると、mFirstTouchTarget に値が設定されます)
  • 現在の ViewGroup がインターセプトを禁止されているかどうか(一般的に ViewGroup が受信したとき、禁止されていなければ、onInterceptTouchEvent メソッドが実行されます)ACTION_DOWN
// ViewGroup.java
    
// このViewGroupから始まり、クリックのターゲットに接続します。
private TouchTarget mFirstTouchTarget;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {

        // 現在のアクションを取得
        final int action = ev.getAction();
        // マスクとAND操作を行い、実際のアクションを取得
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Action downのとき、FLAG_DISALLOW_INTERCEPTを初期化します

        final boolean intercepted;
        // ここには2つの判断があり、ViewGroup自身のonInterceptTouchEventメソッドを呼び出すかどうかを決定します
        // 1. 現在はACTION_DOWNです 
        // 2. すでにChildViewがこのクリックイベントを処理しています 
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // インターセプトが禁止されているかどうかを判断します
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {

                // 自身のonInterceptTouchEventメソッドを呼び出します
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // 変更された場合に備えてアクションを復元します
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

    }

    ...

    return handled;
}

もし ViewGroup 自身がインターセプトしなければ、ChildView に再帰的に分配されます:それぞれの ChildView に対してViewGroup#dispatchTransformedTouchEventを実行し、ChildView がイベントを消費すれば true を返し、ループを抜けます。そうでなければ、次の ChildView に問い合わせます。

// ViewGroup
    
// このViewGroupから始まり、クリックのターゲットに接続します。
private TouchTarget mFirstTouchTarget;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {

        final int action = ev.getAction();
        // マスクとAND操作を行い、実際のアクションを取得
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Action downのとき、FLAG_DISALLOW_INTERCEPTを初期化します

        final boolean intercepted;
        // 自身のViewGroupがイベントをインターセプトできるかどうかを判断します

        // 現在、ViewGroupはインターセプトしないと仮定します
        if (!canceled && !intercepted) {
            ...

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                ...

                if (newTouchTarget == null && childrenCount != 0) {
                    ...
                    // このViewGroup内のChildViewの順序を再配置します
                    final ArrayList<View> preorderedList = 
                                    buildTouchDispatchChildList();
                    ...

                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);

                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        ...

                        // アニメーション中かどうか canReceivePointerEvents
                        // クリックがchild要素内にあるかどうか isTransformedTouchPointInView 
                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // クリック中のViewを取得します 
                        // 新しいViewイベントは非nullで返されます
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // それが処理しているものに加えて新しいポインタを与えます。
                            // ChildViewがクリックを受け取った
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }


                        // 重点はdispatchTransformedTouchEventメソッドです
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // ChildViewがイベントを処理しました

                            ...

                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();

                            // addTouchTargetメソッド
                            // 内部でmFirstTouchTargetに値を設定します
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            // ループを抜けます
                            break;
                        }


                    }
                }

            }
        }

    }

    ...

    return handled;
}

ViewGroup#dispatchTransformedTouchEventメソッド:ViewGroup はこのメソッドを通じてイベントを ChildView に渡します。もし渡された child が null であれば、ViewGroup の親の dispatchTouchEvent を呼び出し、null でなければ指定された View の dispatchTouchEvent を呼び出します(現在の状況では View オブジェクトが渡されるため、指定されたView#dispatchTouchEventメソッドにジャンプします)。

// ViewGroup.java

private boolean dispatchTransformedTouchEvent(MotionEvent event, 
                              boolean cancel,
                              View child, 
                              int desiredPointerIdBits) {

    final boolean handled;

    ...

    if (child == null) {
        // Child viewがnullであればViewGroupのParentに戻ります
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        ...

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    return handled;
}

ViewGroup#addTouchTarget:メンバーmFirstTouchTargetに値を設定します。
ViewGroup#addTouchTargetmFirstTouchTargetメンバーを装備します。

// ViewGroup.java

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        // 再利用可能なTouchTargetオブジェクトを取得します

        final TouchTarget target = TouchTarget
                    .obtain(child, pointerIdBits);
        
        // 前のイベントのViewに接続します
        target.next = mFirstTouchTarget;
        
        // クリックイベントを処理する最初のView
        mFirstTouchTarget = target;
        return target;
    }

ChildView がイベントを受信し、インターセプトした後(メソッドを通じて)、ViewGroup#mFirstTouchTargetに値が設定され、ループを抜けます。dispatchTouchEventChildView がイベントを処理した後、ViewGroup に戻って処理を続けます。

// ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    ...

    if (onFilterTouchEventForSecurity(ev)) {


        if (mFirstTouchTarget == null) {
            ...
        } else {
            ... 

            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;

            while (target != null) {
                final TouchTarget next = target.next;
                
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    
                    // イベントを分配します
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }

        }

    }

    ...

    return handled;
}

ViewGroup 処理 - ChildView がACTION_DOWNをインターセプトしない#

ViewGroup ChildView がイベントをインターセプトする:処理方法は同じです(前のセクション)。ここでは異なる部分を見ていきます。このイベントは ViewGroup 自身が処理します(dispatchTransformedTouchEvent メソッド)。

// ViewGroup.java

    // ViewGroupから始まります
    private TouchTarget mFirstTouchTarget;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {

          
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;


            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Action Downのときにのみresetを実行

                cancelAndClearTouchTargets(ev);

                resetTouchState();
            }

        }

        ...

        return handled;
    }

以前のイベントをクリアします:resetTouchState メソッドに注意してください。これはViewGroup#dispatchTouchEvent、イベントがACTION_DOWNのときに実行されます。 ViewGroup#onInterceptTouchEventメソッドが呼び出されるかどうか:2 つの条件があり、さらに 1 つの FLAG の判断があります。現在はイベントACTION_DOWN、すでに ChildView がこのクリックイベントを処理しています(子 View がイベントを処理すると、mFirstTouchTarget に値が設定されます)。現在の ViewGroup がインターセプトを禁止されているかどうか(一般的に ViewGroup が受信したとき、禁止されていなければ、onInterceptTouchEvent メソッドが実行されます)ACTION_DOWN。もし ViewGroup 自身がインターセプトしなければ、ChildView に再帰的に分配されます:それぞれの ChildView に対してViewGroup#dispatchTransformedTouchEventを実行し、ChildView がイベントを消費すれば true を返し、ループを抜けます。

// ViewGroup
    
    // ViewGroupから始まります
    private TouchTarget mFirstTouchTarget;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            ...
                
            if (!canceled && !intercepted) {
                // 現在、ChildViewがこのイベントを処理する必要はありません
            }
        }
        ...
    }

ChildView が受信しますが、すべてのイベントをインターセプトしない場合、ViewGroup#mFirstTouchTarget は null になります。

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ...

        if (onFilterTouchEventForSecurity(ev)) {

            ...

            // 現在の状況では、ChildViewがイベントを処理する必要はありません
            if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                ...
            }

        }

        ...

        return handled;
    }


    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        ...

        if (child == null) {
            // ViewGroupのParent dispatchTouchEventに戻ります
            handled = super.dispatchTouchEvent(transformedEvent);

        } else {
            ...
        }
        ...
        return handled;
    }

View がイベントを受信 - ACTION_UPを処理#

View は最初に onTouch に分配され、処理されていなければ onTouchEvent に分配されます。順序は次のとおりです。

  • 最初に onTouch インターフェースが実行され、すでに処理されていれば、onTouchEvent はイベントを受信しませんonTouch
  • onTouch がこのイベントを処理しなければ、その View の onTouchEvent がイベントを処理しますonTouchEvent
// View.java

    public boolean dispatchTouchEvent(MotionEvent event) {
        ...

        boolean result = false;

        ...

        if (onFilterTouchEventForSecurity(event)) {
            ...

            // イベントがonTouchに分配されます
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    // このViewが有効かどうかを判断します
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            
            // onTouchEventを分析します
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ...

        return result;
    }

View#onTouchEventメソッドでは、onClick、onLongClick インターフェースの呼び出しが見られます。

// View.java

    // PerformClickはこのViewのクリックイベントを表します
    private PerformClick mPerformClick;

    public boolean onTouchEvent(MotionEvent event) {

        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();


        // CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLEのいずれかを設定すれば、
        // このViewはクリック可能です
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;


        // クリックイベントの代理TouchDelegate
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }


        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {

                case MotionEvent.ACTION_UP:
                    ...

                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;

                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ...

                        // 長押しタスクmHasPerformedLongPress
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 長押しコールバックを削除します
                            removeLongPressCallback();
                            // 押された状態であった場合のみクリックアクションを実行します
                            if (!focusTaken) {

                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }

                                // performClickInternalを分析します
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                        ...
                    }
                    mIgnoreNextUpEvent = false;
                    break;
            }
        }
    }

ここでは、View のクリックイベントが Handler を通じて Click クリックタスク(Runnable)を送信することが見られます。これにより、クリックイベントのブロッキングが防止されます。

// View.java

    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        
        if (attachInfo != null) {
            // ClickタスクをHandlerに入れます
            return attachInfo.mHandler.post(action);
        }
        getRunQueue().post(action);
        return true;
    }

    private boolean performClickInternal() {
        notifyAutofillManagerOnClick();
        
        // performClickを分析します
        return performClick();
    }

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

ViewGroup - ACTION_DOWN#

  1. イベントが ViewGroup によってインターセプトされるかどうかを判断します:
    ViewGroup がイベントをインターセプトする -> 直接 3 に進みます
    ViewGroup がインターセプトしない -> まず 2 に進み、次に 3 に進みます
  2. ViewGroup がインターセプトせず、いずれかの ChildView がイベントをインターセプトして処理します
    ViewGroup#newTouchTargetに値が設定されます
    自身の View に分配します(親クラスを呼び出します)super.dispatchTouchEvent
// ViewGroup.java

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    ...

    mLastTouchDownX = ev.getX();
    mLastTouchDownY = ev.getY();
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
}
  1. イベントの分配、処理:2 つの状況があります。
    すべての ChildView がこのイベントをインターセプトしないため、mFirstTouchTarget は null であり、最後の View に相当します。
if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
        TouchTarget.ALL_POINTER_IDS);
}

すべての ChildView がインターセプトするため、mFirstTouchTarget は null ではなく、while ループは 1 回だけ実行されます。なぜなら、ChildView が処理したからです(分配時に処理されます)。

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
    handled = true;
}

ACTION_MOVE - 受信した MOVE を確認#

Move イベントは、最初に DecorView という ViewGroup を通過し、イベントを実行する際にいくつかのことを知る必要がありますACTION_MOVE。ViewGroup はクリックのフラグをクリアしません。TouchTarget 型のmFirstTouchTarget要素は空ではありません(すでに ChildView 要素が処理されています)。ACTION_MOVE イベントの判断フローチャートは以下の通りです。主に行うことは、ViewGroup がイベントをインターセプトするかどうかです(以下はデフォルトでインターセプトしないと仮定します)。インターセプトしない場合、Move イベントは分配されず、処理されません。同様に、ViewGroup#dispatchTouchEventメソッドを観察し、ここから分析を開始します。リセット段階には入らないでしょう。

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            ...
        }
    }

現在の ViewGroup がインターセプトを禁止されているかどうかを判断します。禁止されていなければ、自身のメソッドを呼び出してインターセプトを確認しますFLAG_DISALLOW_INTERCEPT onInterceptTouchEvent

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        ...

        // インターセプトを確認します。
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // ViewGroupがインターセプトを禁止しているかどうかを判断します
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

            // デフォルトではViewGroupはインターセプトしないと仮定します
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // 変更された場合に備えてアクションを復元します
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
    }

これはイベントではないため、最初のイベント分配段階ACTION_DOWNには入らないでしょう。

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

            ...

        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
      
        if (!canceled && !intercepted) {

            // Downイベントではありません
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
               ...

            }
        }

        ... 
    }

ACTION_MOVEの重点はイベントの分配です。

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ...

        if (mFirstTouchTarget == null) {
            ...

        } else {
          
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;

                // alreadyDispatchedToNewTouchTargetはfalseです
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;

                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;

                    // dispatchTransformedTouchEventメソッドを分析してChildViewに分配します
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    ...
                }
                predecessor = target;
                target = next;
            }
        }
    }

次に、dispatchTransformedTouchEvent メソッドを通じて、ChildView の dispatchTouchEventACTION_MOVEイベントを分配します。

// ViewGroup.java 

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) 

        ...

        if (child == null) {
            ...
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }
    }

ACTION_MOVE - ViewGroup が MOVE をインターセプト#

現在の状況を理解します:すでに ChildView が ACTION_DOWN イベントを処理しており、カスタム ViewGroup を作成し、ViewGroup#onInterceptTouchEventメソッドをコピーします。ACTION_MOVEのときに true を返してインターセプトします。

// カスタムViewGroup.java

public class MyViewGroup extends View {

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(ev.getAction() == MotionEvent.ACTION_MOVE) {
            // ViewGroupがイベントを消費します
            return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

}

ACTION_MOVE イベントは複数回発生します。最初の ACTION_MOVE(ParentView から入ってくる)の目的は:

  1. ChildView のイベントをキャンセルします(イベントが ACTION_CANCEL に変更されます)
  2. ViewGroup#mFirstTouchTargetを null に設定します。この時、ParentView はイベントを処理しません。
    結果:この時、ChildView は ACTION_CANCEL イベントを受け取ります(イベントがキャンセルされ、以降は ViewGroup がイベントを処理します)。
/**
 * ViewGroup.java
 * 
 * onInterceptTouchEventメソッドをオーバーライドすると、Moveイベントはtrueになるため、
 * cancelChildもtrueになります。
 * 
 * この時、イベントはACTION_CANCELに変更され、以下のChildViewはACTION_CANCELイベントを受け取ります。
 */ 
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ...

        if (mFirstTouchTarget == null) {
            ...

        } else {

            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // ACTION_MOVEが最初に入る
                    // interceptedはtrueなので、cancelChild = true
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;

                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }

                    // mFirstTouchTarget = next、nextはnull
                    // したがって、mFirstTouchTarget = null
                    if (cancelChild) {
                        if (predecessor == null) {
                            // nextがnullなので、mFirstTouchTargetがnullになります
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        ...
    }

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {

        final boolean handled;

        final int oldAction = event.getAction();
        // cancelがtrue
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {

            // イベントをACTION_CANCELに変更します
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                ...
            } else {
                // ChildViewに分配されるイベントはキャンセルされます
                // イベントが上層によってインターセプトされたときにトリガーされます
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        ...
            
        return handled;
    }

2 回目の ACTION_MOVE(ParentView から入ってくる)では、mFirstTouchTarget が null になるため、ParentView はイベントを分配しません。
結果:ParentView は自分自身でイベントを処理します。

//ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

        // 最初のACTION_MOVEのとき、mFirstTouchTargetがnullに設定されます
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            ...
        } else {
            intercepted = true;
        }

        // intercepted = true 
        if (!canceled && !intercepted) {
            ... 
        }


        if (mFirstTouchTarget == null) {
            // ChildViewがnull
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {

            ...
        }

        ...
}


private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {

    final boolean handled;

    ...
    
    
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // 完了。
    transformedEvent.recycle();
    return handled;
}

イベントの衝突#

なぜ衝突が発生するのか?それは、イベントは一つしか存在せず、応答するコンポーネントが自分が望む View オブジェクトでない場合、これをイベントの衝突と呼びます。
2 つの方法で処理されます。

  • 内部インターセプト法:ChildView#dispatchTouchEventが処理する必要があり、親コンテナ(ParentView)の onInterceptTouchEvent インターセプトと連携する必要があります。
  • 外部インターセプト法(一般的に使用される):親 View が処理するだけです。

内部インターセプト - ChildView が処理#

ChildView 内でrequestDisallowInterceptTouchEventメソッドを使用して、FLAG_DISALLOW_INTERCEPT要素を制御し、ParentView がイベントを分配しないようにします。
内部インターセプトの作業
ChildView は適切なタイミングでメソッドを通じて、ParentView にイベントをインターセプトしないよう要求しますrequestDisallowInterceptTouchEvent

public class MyView extends View {

    int dispatchX, dispatchY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // ParentViewにインターセプトしないよう要求します
                getParent().requestDisallowInterceptTouchEvent(true);
                break;

            case MotionEvent.ACTION_UP:
                break;

            case MotionEvent.ACTION_MOVE:
                int deltaX = x - dispatchX;
                int deltaY = y - dispatchY;
                
                // イベントをViewGroupに処理させます
                if(Math.abs(deltaX) > Math.abs(deltaY)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }

        dispatchX = x;
        dispatchY = y;

        return super.dispatchTouchEvent(event);
    }

}

親 View のメソッドをオーバーライドし、ACTION_DOWN のときにインターセプトしないように設定します。こうすることで、イベントが ChildView に渡ることができますonInterceptTouchEvent

public class MyViewGroup extends ViewGroup {

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(ev.getAction() == MotionEvent.ACTION_DOWN) {
            // ViewGroupはインターセプトしません
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }

}

requestDisallowInterceptTouchEventの詳細説明:なぜ requestDisallowInterceptTouchEvent が ACTION_DOWN イベントを制御できないのか。

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 初期のダウンを処理します。
            if (actionMasked == MotionEvent.ACTION_DOWN) {

                cancelAndClearTouchTargets(ev);
                // resetTouchStateを確認します
                resetTouchState();
            }

            // インターセプトを確認します。
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {    // mFirstTouchTarget != null

                // FLAG_DISALLOW_INTERCEPTを制御します
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // trueであれば、ParentViewは分配しません
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // 変更された場合に備えてアクションを復元します
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

            //
        }
    }


    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // すでにこの状態であれば、先祖もそうだと仮定します
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // 親に渡します
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

上記から、ViewGroup は ACTION_DOWN でresetTouchState()メソッドを使用し、ここでこのフラグをクリアするため、イベント ACTION_DOWN は必ず分配されますFLAG_DISALLOW_INTERCEPT

// ViewGroup.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        ...

        final boolean intercepted;

        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            // クリア後、FLAG_DISALLOW_INTERCEPTは空になります
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            
            if (!disallowIntercept) {
                // ViewGroup#onInterceptTouchEventを判断します
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // 変更された場合に備えてアクションを復元します
            } else {
                intercepted = false;
            }
        }
        ...
    }

Note

ViewGroup 内でonInterceptTouchEventをオーバーライドし、ViewGroup が ACTION_DOWN イベントのときに false を返すようにします(インターセプトしない)。こうすることで、ACTION_DOWN が分配され、ChildView に渡ることができます。他のアクションはインターセプトされます。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(ev.getAction() == ACTION_DOWN) {
            return false;
        }
        return true;
    }

外部インターセプト - ParentView が処理#

ParentView がイベントのインターセプトを処理するのは比較的簡単です。ParentView#onInterceptTouchEventをオーバーライドして、どのタイミング(条件)でイベントをインターセプトするかを決定します。

public class MyExternalViewGroup extends ViewGroup {

    int interceptX, interceptY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercept = false;
        boolean defaultIntercept = super.onInterceptTouchEvent(event);

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch(event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - interceptX;
                int deltaY = y - interceptY;
                
                // 水平スワイプでインターセプト
                if(Math.abs(deltaX) > Math.abs(deltaY)) {
                    intercept = true;
                } else {
                    intercept = false;
                }
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;
                
            default:
                intercept = defaultIntercept;
                break;
        }

        interceptX = x;
        interceptY = y;

        return intercept;
    }
}
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。