Androidイベント配信メカニズムとネストによるタッチイベントの競合解決策

17342 ワード

スライドを実現するための一般的な方法
  • は、scrollTo()scrollBy()によりスライド
  • を行う.
  • Scrollerを使用してスライド
  • を行う.
        @Override
        public boolean onTouchEvent(MotionEvent event) {
                case ACTION_UP:
                    scroller.startScroll(getScrollX(), 0, dx, 0);
                    invalidate();
                    break;
    
            }
            return super.onTouchEvent(event);
        }
    
        @Override
        public void computeScroll() {
            if (scroller.computeScrollOffset()) {
                scrollTo(scroller.getCurrX(), scroller.getCurrY());
                invalidate();
            }
        }
    

    上記のコードは基本的にScrollerの基本ソケットを用い,まずstartScroll(int startX, int startY, int dx, int dy)を呼び出し,次にcomputeScroll()メソッドを書き換えてScrollerが算出したX,Y座標を取得してscrollTo(int x, int y)を呼び出してスライドする.
  • プロパティアニメーションを使用してスライド
  • イベント配信ソースプロセス
    イベント配布のフローチャート:
    まず、いくつかの問題を持ってソースコードを見に行きます.そうしないと、ソースコードの多くの枝葉末節に干渉されます.
    1.Actionの出発点はどこですか.
    タッチパネルイベントが最初に渡されるのはActivityであるため,イベント配信の起点はActivityのdispatchTouchEventメソッドである.
        /**
         * Called to process touch screen events.  You can override this to
         * intercept all touch screen events before they are dispatched to the
         * window.  Be sure to call this implementation for touch screen events
         * that should be handled normally.
         *
         * @param ev The touch screen event.
         *
         * @return boolean Return true if this event was consumed.
         */
        public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            return onTouchEvent(ev);
        }
    

    このコードからActivityがgetWindow()を呼び出すsuperDispatchTouchEvent(MotionEvent ev)メソッドがわかります.ここでAndroidのWindowオブジェクトには実装クラスPhoneWindowが1つしかないことを知っていますので、PhonwViewのsuperDispatchTouchEvent(MotionEvent ev)メソッドを見てみましょう.
        // This is the top-level view of the window, containing the window decor.
        private DecorView mDecor;
        
        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
        }
    

    ここでは実際にDecorViewに渡されたsuperDispatchTouchEvent(MotionEvent ev)メソッドで処理されているのが見えますが、DecorViewをよく知っているのは、実際にパッケージされたFrameLayout、つまりTop-levelのView Groupであることがわかります.ここから私たちがよく知っているViewイベントの配布メカニズムの始まりです.
    2.View GroupはどのようにしてサブViewにイベントを配布しますか?
    まず、ViewGroupのブロックイベントの呼び出しを無視し、最も簡単な状況を考慮します.
                            for (int i = childrenCount - 1; i >= 0; i--) {
                                final int childIndex = getAndVerifyPreorderedIndex(
                                        childrenCount, i, customOrder);
                                final View child = getAndVerifyPreorderedView(
                                        preorderedList, children, childIndex);
                                ...
                                if (!canViewReceivePointerEvents(child)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
                                ...
                                resetCancelNextUpFlag(child);
                                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                    // Child wants to receive touch within its bounds.
                                    mLastTouchDownTime = ev.getDownTime();
                                    if (preorderedList != null) {
                                        // childIndex points into presorted list, find original index
                                        for (int j = 0; j < childrenCount; j++) {
                                            if (children[childIndex] == mChildren[j]) {
                                                mLastTouchDownIndex = j;
                                                break;
                                            }
                                        }
                                    } else {
                                        mLastTouchDownIndex = childIndex;
                                    }
                                    mLastTouchDownX = ev.getX();
                                    mLastTouchDownY = ev.getY();
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
                                ...
                            }
    

    コードから、イベントをサブViewに配布する方法は、dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)メソッドを1回実行することであり、このメソッドがtrueに戻った場合、すぐにループを停止し、タッチスクリーンイベントもこのサブViewによって消費されることがわかります.dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)が何をしたのか詳しく見ることができます.
        /**
         * Transforms a motion event into the coordinate space of a particular child view,
         * filters out irrelevant pointer ids, and overrides its action if necessary.
         * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
         */
        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
    
            ...
            
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            
            // Perform any necessary transformations and dispatch.
            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);
            }
    
            // Done.
            transformedEvent.recycle();
            return handled;
        }
    

    コードから、childがnullに等しくない場合、childのdispatchTouchEvent dispatchTouchEvent(MotionEvent event)メソッドが呼び出されることがわかる.childViewのイベント配信プロセスを続行します.
    ここでは、addTouchTarget(child, idBitsToAssign);という方法の具体的な役割に注目します.
        // First touch target in the linked list of touch targets.
        private TouchTarget mFirstTouchTarget;
        
        /**
         * Adds a touch target for specified child to the beginning of the list.
         * Assumes the target child is not already present.
         */
        private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
            mFirstTouchTarget = target;
            return target;
        }
    

    コードからこのもう一つのTouchTargetのチェーンテーブルが見えます.dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)でtrueを返した後、mFirstTouchTargetに値を割り当てます.このとき、mFirstTouchTargetはnullではありません.
    もちろん、dispatchTransformedTouchEventが実行される前に、タッチスクリーンイベントを受信することが不可能なサブViewがフィルタリングされる.
    if (!canViewReceivePointerEvents(child)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    

    コードから主に2つの点を判断していることがわかります
    1.クリックしたポイントがサブビューの領域内にあるか.
    2.サブViewがアニメーションを実行しているかどうか.
    上記のいずれかに該当する場合は直接continueし,ループの次の項目を行う.
    3.ビューグループのonInterceptTouchEvent(MotionEvent ev)メソッドがトリガーされるのはどのような場合ですか?
                // Check for interception.
                final boolean intercepted;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                    if (!disallowIntercept) {
                        intercepted = onInterceptTouchEvent(ev);
                        ev.setAction(action); // restore action in case it was changed
                    } else {
                        intercepted = false;
                    }
                } else {
                    // There are no touch targets and this action is not an initial down
                    // so this view group continues to intercept touches.
                    intercepted = true;
                }
    

    コードからonInterceptTouchEvent(ev)を実行する条件がいくつか見られ、私たちは一つずつ何を代表しているのかを見ることができます.
  • actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null
  • actionMasked == MotionEvent.ACTION_DOWNこれはよく理解できますタッチパネルイベントがACTIONであればDONWなら条件が成立します.
  • mFirstTouchTarget != null上記のソースコードの分析から分かるように、mFirstTouchTarget!=nullはタッチスクリーンイベントを表す前にすでにViewに消費されており、残りのイベントシーケンスはすべてそのViewに渡されて処理されます.

  • !disallowInterceptここではコード
  • を見ることができます
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    

    ここにはFLAG_DISALLOW_INTERCEPTというマークがありますが、このマークが0でなければdisallowInterceptはtrueであり、このとき!disallowInterceptは成立しません.では、この値をtrueにすれば?ここでは、requestDisallowInterceptTouchEvent(boolean disallowIntercept)という方法に関連しています.この方法により、親Viewが子Viewのイベントをブロックしないようにすることができます.そのため、この方法もイベントの衝突を解決する方法の一つです.
    4.View Groupのすべてのタッチスクリーンイベントをブロックしないようにしてもらえますか?
    この問題は実は否定的で、コードから私たちは見ることができます
                // Handle an initial down.
                if (actionMasked == MotionEvent.ACTION_DOWN) {
                    // Throw away all previous state when starting a new touch gesture.
                    // The framework may have dropped the up or cancel event for the previous gesture
                    // due to an app switch, ANR, or some other state change.
                    cancelAndClearTouchTargets(ev);
                    resetTouchState();
                }
                
        /**
         * Resets all touch state in preparation for a new cycle.
         */
        private void resetTouchState() {
            clearTouchTargets();
            resetCancelNextUpFlag(this);
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
            mNestedScrollAxes = SCROLL_AXIS_NONE;
        }
                
    

    コードからタッチパネルイベントがACTIONであることがわかります.DOWNではすべてのTouchTargetsとFLAG_がリセットされますDISALLOW_INTERCEPTなので、タッチパネルイベントはACTION_DOWNの場合、親View Groupは必ずonInterceptTouchEvent(ev)メソッドを実行します.
    5.setOnTouchListneer後にonTouchEvent(MotionEvent ev)メソッドがトリガーされますか?
    ViewのdispatchTouchEvent(MotionEvent event)法を解析することができます
                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;
                }
    

    このコードから、mOnTouchListener!=nullであればmOnTouchListener.onTouch(this, event)を実行し、trueを返すとresult =trueであり、onTouchEvent(event)はこれ以上実行されないため、onTouchEvent(event)がまだ実行されているかどうかはmOnTouchListener.onTouch(this, event)の戻り値を見る必要があることがわかります.
    6.あるViewがイベントの処理を開始すると、同じイベントシーケンスのイベントは他のView処理に渡されますか?
                // Check for interception.
                final boolean intercepted;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                    if (!disallowIntercept) {
                        intercepted = onInterceptTouchEvent(ev);
                        ev.setAction(action); // restore action in case it was changed
                    } else {
                        intercepted = false;
                    }
                } else {
                    // There are no touch targets and this action is not an initial down
                    // so this view group continues to intercept touches.
                    intercepted = true;
                }
                
                // Dispatch to touch targets.
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } else {
                    // Dispatch to touch targets, excluding the new touch target if we already
                    // dispatched to it.  Cancel touch targets if necessary.
                    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;
                    }
                }
    

    ここから、mFirstTouchTarget != nullであれば、タッチイベントViewGroupはブロックされず、mFirstTouchTargetを巡ってdispatchTransformedTouchEvent()が実行されることが分かる.ViewGrouponTouchEvent()がいつ実行されるかmFirstTouchTargetがnullの場合、ViewGroupのonTouchEvent()メソッドが呼び出されます.つまり2つの可能性がある.ACTION_DOWNの場合、ViewGroupのonInterceptTouchEvent()はtrueに戻ります.2.childを1回呼び出したdispatchTouchEvent()タッチイベントは消費されず、このときもViewGroupのonTouchEvent()が実行される
    ネストによるスライドイベントの競合の解決方法
  • 自分でイベントの配布を処理する過程は衝突を解決するこのような方式は自分でイベントの配布とブロックを処理する必要があり、比較的複雑である.同時に外部処理と内部処理の2種類に分けられる.開発者のイベント配信メカニズムの理解に対する要求は比較的高い.面接でよく聞かれる質問を簡単に挙げると、ScrollView ListViewはどのように処理すればよいのか、通常はスライドイベントがScrollViewにブロックされて消費されるが、一般的にはView GroupがACTION_DONEイベントをブロックしない場合はrequestDisallowInterceptTouchEvent(true);を呼び出してScrollViewがスライドイベントをブロックしないようにすると、スライドイベントがListViewに正常に伝達され、ListViewも正常にスライドできるようになる.
        @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = ev.getY();
                requestDisallowInterceptTouchEvent(true);
                break;
        return super.dispatchTouchEvent(ev);
    }
    
    しかし、この方法にも欠陥があり、ScrollViewにHeadViewが存在する場合、ListViewが一番上にスライドしたとき、インタラクティブなイベントをScrollViewに渡すとしたら?通常のイベント配布では、あるViewがイベントを消費すると、イベントシーケンスの次のイベントが消費されます.このとき、ScrollViewに手動でイベントを渡すことができますが、自分で書くと結合が非常に大きくなります.このように、
                case MotionEvent.ACTION_MOVE:
                boolean isMoveUp = mLastY - ev.getY() < 0;
                if (isMoveUp && firstVisiblePosition == 0) {
                    Log.d("tset", "needParent consume!");
                    requestDisallowInterceptTouchEvent(false);
                    ((ViewGroup)getParent()).onTouchEvent(ev);
                    return false;
                } else if (!isMoveUp && lastVisiblePosition == getAdapter().getCount() - 1) {
                    Log.d("tset", "needParent consume!");
                    requestDisallowInterceptTouchEvent(false);
                    ((ViewGroup)getParent()).onTouchEvent(ev);
                    return false;
                }
                break;
    
    でAndroidはNestedScrollingベースのソリューションを提供してくれました.
  • NestedScrollingの実装案に基づくNestedScrollingの主な考え方は、Childがイベントの受信者として働き、ChildがMoveイベントを受信すると、dispatchNestedPreScroll()を呼び出してイベントをParentに渡して処理し、ParentがコールバックonNestedPreScroll()を受信すると、Parentはイベントを消費する必要があるかどうか、距離のスライドを消費する必要があるかどうかを判断する.その後、Childは残りの消費されていない距離に従ってスライドを継続する.
  • CoordinatorLayout、Behaviorに基づく実装案(後にまとめを専門に書く)
  • 参考資料
    Android Scroller完全解析、Scrollerについてあなたが知っていることはすべて
    <>:任玉剛
    Androidネストスライド機構
    NestedScrollingは簡単なネストスライドを実現します
    Android NestedScrolingメカニズム完全解析ネストスライドを回す
    Awesome-Android-Interview