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 操作を行い、実際の Action を取得
        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 つの条件があり、さらに 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 操作を行い、実際の Action を取得
        final int actionMasked = action & MotionEvent.ACTION_MASK;

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

        final boolean intercepted;
        // ここに 2 つの判断があり、ViewGroup 自身の onInterceptTouchEvent メソッドを呼び出すかどうかを決定します
        // 1. 現在は ACTION_DOWN 
        // 2. すでに Child View がこのクリックイベントを処理しています 
        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 操作を行い、実際の Action を取得
        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) {
                            // それに加えて新しいポインタを与えます。
                            // Child View がすでにクリックを受け取っています
                            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 に値が設定され、dispatchTouchEvent ChildView がイベントを処理した後、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 つの条件があり、さらに FLAG の判断があります。現在はイベント ACTION_DOWN であり、すでに ChildView がこのクリックイベントを処理しています(子 View がイベントを処理すると、mFirstTouchTarget に値が設定されます)。現在 ViewGroup が遮断を禁止されているかどうか(一般的に ViewGroup が受信したとき、遮断が禁止されていなければ、onInterceptTouchEvent メソッドが実行されます)。もし 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) {
            // クリックタスクを 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 の dispatchTouchEvent ACTION_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 を空にします。この時、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 は空なので、mFirstTouchTarget は空になります
                            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 が空になるため、ParentView はイベントを分配しません。
結果:ParentView 自身がイベントを処理します。

//ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

        // 最初の ACTION_MOVE では mFirstTouchTarget が空になります
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

            ...
        } else {
            intercepted = true;
        }

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


        if (mFirstTouchTarget == null) {
            // ChildView は空です
            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;
    }
}
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。