Android NestedScrollingスライド衝突問題解決(1)-関連インタフェース

55525 ワード

Android NestedScrollingスライド衝突問題の解決
親Viewおよび子Viewがスライド可能で、スライド方向が一致している場合(CoordinatorLayoutにRecyclerViewまたはWebviewが組み込まれている場合など)、スライド競合の解決はAndroidが提供するNestedScrollingインタフェースに依存する必要があります.
NestedScrollingインタフェースは、NestedScrollingParentNestedScrollingChildの2つの部分に分かれています.
説明の便宜上、以下、NestedScrollingParentと略すとNPであり、NestedScrollingChildNCである.
NestedScrollingChild
含まれるインタフェース:
public interface NestedScrollingChild {
    /**
     *     View    nested scroll  
     * @param enabled     
     */
    void setNestedScrollingEnabled(boolean enabled);

    /**
     *   View     nested scroll  
     * @return
     */
    boolean isNestedScrollingEnabled();

    /**
     *  axes    nested scroll    
     * @param axes     
     * @return    NestedScrollingParent        
     */
    boolean startNestedScroll(@ViewCompat.ScrollAxis int axes);

    /**
     *   nested scroll
     */
    void stopNestedScroll();

    /**
     *      NestedScrollingParent         
     * @return    
     */
    boolean hasNestedScrollingParent();

    /**
     * nested scroll        ,           ,       (     NestedScrollingParent          )
     * @param dx         x    
     * @param dy         y    
     * @param consumed            (      ,consumed[0]          x   ,consumed[1]          y   )
     * @param offsetInWindow     ,   null。     ,          view     (offsetInWindow[0] offsetInWindow[1]   x  y   )
     * @return                     
     */
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
                                    @Nullable int[] offsetInWindow);

    /**
     *    View                (           , NestedScrollingParent           )
     * @param dxConsumed       x     
     * @param dyConsumed       y     
     * @param dxUnconsumed     x     
     * @param dyUnconsumed     y     
     * @param offsetInWindow     ,   null。     ,          view     (offsetInWindow[0] offsetInWindow[1]   x  y   )
     * @return
     */
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                 int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);

    /**
     *    NestedScrollingChild  fling         (    NestedScrollingParent        fling)
     * @param velocityX x   
     * @param velocityY y   
     * @return             fling
     */
    boolean dispatchNestedPreFling(float velocityX, float velocityY);

    /**
     *   fling  
     * @param velocityX x     
     * @param velocityY y     
     * @param consumed   NestedScrollingChild       fling
     * @return NestedScrollingParent       fling
     */
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
}

NCNPの子孫(必ずしも直接子Viewとは限らない)であり、連合スライドのリクエスト側でもあり、スライドによって生成された一連のMotionEventはこのViewで追跡処理され、一般にこのViewはonTouchEventMotionEventの追跡分析に基づいてスライド要求を開始する.例えば、以下のRecyclerViewのうちonTouchEventの簡略化されたバージョン.
@Override
public boolean onTouchEvent(MotionEvent e) {
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    boolean eventAddedToVelocityTracker = false;

    final MotionEvent vtev = MotionEvent.obtain(e);
    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();

    if (action == MotionEvent.ACTION_DOWN) {
        mNestedOffsets[0] = mNestedOffsets[1] = 0;
    }
    vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            mScrollPointerId = e.getPointerId(0);
            mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontally) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertically) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            //       
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
        } break;

        case MotionEvent.ACTION_MOVE: {
            final int x = (int) (e.getX(index) + 0.5f);
            final int y = (int) (e.getY(index) + 0.5f);
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;

            //     NP             (      )
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
                // NP         
                dx -= mScrollConsumed[0]; // NP    X     
                dy -= mScrollConsumed[1]; // NP   Y     
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // Updated the nested offsets
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
            }

            //                       
            if (mScrollState != SCROLL_STATE_DRAGGING) {
                boolean startScroll = false;
                if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                    if (dx > 0) {
                        dx -= mTouchSlop;
                    } else {
                        dx += mTouchSlop;
                    }
                    startScroll = true;
                }
                if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                    if (dy > 0) {
                        dy -= mTouchSlop;
                    } else {
                        dy += mTouchSlop;
                    }
                    startScroll = true;
                }
                if (startScroll) {
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
            }

            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];

                //       
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                if (mGapWorker != null && (dx != 0 || dy != 0)) {
                    mGapWorker.postFromTraversal(this, dx, dy);
                }
            }
        } break;

        case MotionEvent.ACTION_POINTER_UP: {
            onPointerUp(e);
        } break;

        case MotionEvent.ACTION_UP: {
            mVelocityTracker.addMovement(vtev);
            eventAddedToVelocityTracker = true;
            mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
            final float xvel = canScrollHorizontally
                    ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
            final float yvel = canScrollVertically
                    ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
            //       fling  (          ,      ) 
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
            resetTouch();
        } break;

        case MotionEvent.ACTION_CANCEL: {
            cancelTouch();
        } break;
    }

    if (!eventAddedToVelocityTracker) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();

    return true;
}

およびscrollByInternal簡略化版:
boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0, unconsumedY = 0;
    int consumedX = 0, consumedY = 0;

    if (mAdapter != null) {
        if (x != 0) {
            //        X     
            consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
            //     X     
            unconsumedX = x - consumedX;
        }
        if (y != 0) {
            //        Y     
            consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
            //     Y     
            unconsumedY = y - consumedY;
        }
    }

    //    NP            
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
            TYPE_TOUCH)) {
        // Update the last touch co-ords, taking any scroll offset into account
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        if (ev != null) {
            ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        }
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
    } 
    
    //             
    return consumedX != 0 || consumedY != 0;
}


したがって、1回のスライド操作に対して、NCのインタフェース呼び出し順序は、startNestedScroll -> dispatchNestedPreScroll -> dispatchNestedScroll -> stopNestedScroll
一般的な処理ロジックは以下の疑似コードでまとめることができる.
    private int mLastX;
    private int mLastY;
    private int[] mConsumed = new int[2];
    private int[] mOffsetInWindow = new int[2];
    @Override
    void onTouchEvent(MotionEvent event) {
        int eventX = (int) event.getRawX();
        int eventY = (int) event.getRawY();
        int action = event.getAction();
        int deltaX = eventX - mLastX;
        int deltaY = eventY - mLastY;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally()) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically()) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis);
                break;
            case MotionEvent.ACTION_MOVE:
                if (dispatchNestedPreScroll(deltaX, deltaY, mConsumed, mOffsetInWindow)) {
                    deltaX -= mConsumed[0];
                    deltaY -= mConsumed[1];
                }
                int internalScrolledX = internalScrollByX(deltaX);
                int internalScrolledY = internalScrollByY(deltaY);
                deltaX -= internalScrolledX;
                deltaY -= internalScrolledY;
                if (deltaX != 0 || deltaY != 0) {
                    dispatchNestedScroll(mConsumed[0] + internalScrolledX, mConsumed[1] + internalScrolledY, deltaX, deltaY, mOffsetInWindow);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                stopNestedScroll();
                break;
        }
        mLastX = eventX;
        mLastY = eventY;
    }

    /**
     * X     
     * @param deltaX     
     * @return        
     */
    abstract int internalScrollByX(int deltaX);

    /**
     * Y     
     * @param deltaY     
     * @return        
     */
    abstract int internalScrollByY(int deltaY);

    /**
     *         
     * @return     
     */
    abstract boolean canScrollHorizontally();

    /**
     *         
     * @return     
     */
    abstract boolean canScrollVertically();

NestedScrollingParent
インタフェースを含める:

public interface NestedScrollingParent {

    /**
     *  NP           (NestedScrollingChild.startNestedScroll)
     * @param child        NP  view    view
     * @param target      NP  view
     * @param axes     
     * @return          
     */
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ViewCompat.ScrollAxis int axes);

    /**
     *           (onStartNestedScroll  true         ), NestedScrollingParent            
     * @param child        NP  view    view
     * @param target      NP  view
     * @param axes     
     */
    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ViewCompat.ScrollAxis int axes);

    /**
     *   nested scroll   (NestedScrollingChild  stopNestedScroll)
     * @param target      NP  view
     */
    void onStopNestedScroll(@NonNull View target);

    /**
     *  NestedScrollingChild      ,      
     * @param target      NP  view
     * @param dx x     
     * @param dy y     
     * @param consumed     ,               
     */
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);

    /**
     *   NestedScrollingChild         
     * @param target      NP  view
     * @param dxConsumed     x     
     * @param dyConsumed     y     
     * @param dxUnconsumed     x     
     * @param dyUnconsumed     y     
     */
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
                                 int dxUnconsumed, int dyUnconsumed);

    /**
     *  NestedScrollingChild     fling  
     * @param target      NP  view
     * @param velocityX x fling  
     * @param velocityY y fling  
     * @return      fling
     */
    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);

    /**
     *   fling  
     * @param target      NP  view
     * @param velocityX x fling  
     * @param velocityY y fling  
     * @param consumed NestedScrollingChild      fling
     * @return      fling
     */
    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);


    /**
     *       
     * @return     
     */
    @ViewCompat.ScrollAxis
    int getNestedScrollAxes();
}


インタフェース呼び出し順序
  • NCは、MotionEventの処理時に、スライド要求を開始することを決定し、startNestedScroll
  • を呼び出す.
  • startNestedScrollを呼び出すと、親viewを上へ階層的に遍歴し、onStartNestedScrollインタフェースを呼び出す.trueを返すと、このviewは今回のnested scrollに連動したNPであり、遍歴を中断する.falseを返すと、ルートviewまで上層に遍歴し続けます.ルートビューを巡回しても連動NPが見つからない場合は、後続のスライドは連動できません.見つかったら、手順3に進みます.
  • NCはNPのonNestedScrollAcceptedインタフェースを呼び出す.
  • NPのonNestedScrollAcceptedインタフェースが呼び出され、スライドの初期動作が行われます.
  • NCは、ユーザインタラクションがスライド距離を生成することを検出し、NPのonNestedPreScrollインターフェースを呼び出す.
  • NPのonNestedPreScrollインタフェースが呼び出され、今回のスライドを前処理し、部分的なスライド距離を消費する(または消費しない).
  • NCは残りのスライド距離を処理する.
  • NCが残りのスライド距離を処理しない場合、dispatchNestedScrollが呼び出される.
  • NPのonNestedScrollが呼び出され、残りのスライド距離の処理を継続するか否かが自己決定される.
  • インタラクションのスライドが終了し、NCはstopNestedScrollを呼び出す.
  • NPのonStopNestedScrollが呼び出されます.