美文网首页
Android嵌套滑动下篇

Android嵌套滑动下篇

作者: leilifengxingmw | 来源:发表于2020-12-20 11:32 被阅读0次
嵌套滑动版本1.gif

上篇文章 Android嵌套滑动上篇 中实现的滑动效果并不是很流畅,如上图所示。不流畅的原因是因为对于fling,NestedScrolling要么交给child处理,要么交给parent处理。

而通过NestedScrolling2可以实现fling类型的滚动先由外层控件处理一部分,剩余的再交给内层控件处理,这样使滑动效果比较流畅。实现的效果如下图所示:

嵌套滑动版本2.gif

GitHub源码

这里先说一下NestedScrolling2能让内层控件和外层控件在惯性滑动的时候更流畅的关键的逻辑。

  1. NestedScrolling2分发滚动事件的时候区分了滚动事件的类型:是正常的触摸滚动还是惯性滑动。

  2. 内层控件先调用dispatchNestedPreFling来处理惯性滑动。如果外层控件处理了惯性滑动,即外层控件的onNestedPreFling方法返回了true。那么NestedScrolling2和NestedScrolling的惯性滑动效果没有什么差异。

  3. 如果外层控件没有处理惯性滑动,也就是外层控件的onNestedPreFling方法返回了false。那么就会调用dispatchNestedFling方法并且内层控件自身开始惯性滑动mViewFlinger.fling(velocityX, velocityY),但是在惯性滑动的每一帧,通过Scroller计算出来的滚动距离通过dispatchNestedPreScroll先分发给外层控件。外层控件可以通过onNestedPreScroll先消耗部分滚动距离,然后内层控件再自身滚动。

流程图

NestedScrolling2流程.jpg

接下来先看一下NestedScrolling2相关的几个类。

NestedScrollingChild2继承了NestedScrollingChild接口并新增了几个方法。

public interface NestedScrollingChild2 extends NestedScrollingChild {

    boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

    void stopNestedScroll(@NestedScrollType int type);

    boolean hasNestedScrollingParent(@NestedScrollType int type);

    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type);

    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type);

}

新增的几个方法都有一个@NestedScrollType int type参数,是用来区分滚动类型的,有两个取值:

//触摸屏幕类型
public static final int TYPE_TOUCH = 0;

//惯性滑动类型   
public static final int TYPE_NON_TOUCH = 1;

对应的NestedScrollingParent2继承了NestedScrollingParent接口也新增了几个方法。

public interface NestedScrollingParent2 extends NestedScrollingParent {

    boolean onStartNestedScroll(View child, View target, @ScrollAxis int axes,
            @NestedScrollType int type);
    
    void onNestedScrollAccepted(View child, View target, @ScrollAxis int axes,
                @NestedScrollType int type);

    void onStopNestedScroll(View target, @NestedScrollType int type);

    void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
    
    void onNestedPreScroll(View target, int dx, int dy, @NonNull int[] consumed,
                @NestedScrollType int type);

}

最新的嵌套滑动已经都出NestedScrolling3了,这个更新也是日新月异啊,哈哈。

public interface NestedScrollingChild3 extends NestedScrollingChild2 {

    void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
            @Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type,
            @NonNull int[] consumed);
}
public interface NestedScrollingParent3 extends NestedScrollingParent2 {
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);
}

NestedScrolling3相对于NestedScrolling2的改变与本篇文章内容无关,我们暂时忽略。

下面我们以RecyclerView为例开始分析,androidx1.1.0的源码。

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
    //...
}

RecyclerView的onTouchEvent方法

@Override
public boolean onTouchEvent(MotionEvent e) {
    //...
    final MotionEvent vtev = MotionEvent.obtain(e);
    vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);

    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;
            }
            //注释1处
            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;

            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                //注释2处
                if (dispatchNestedPreScroll(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        mReusableIntPair, mScrollOffset, TYPE_TOUCH
                )) {
                    //注释3处
                    dx -= mReusableIntPair[0];
                    dy -= mReusableIntPair[1];
                    //...
                }

                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];
                //注释4处
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        } 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;
            //注释5处,调用fling方法
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
            //注释6处
            resetScroll();
        } break;
    }
    //...
    return true;
}

注释1处,调用startNestedScroll方法

@Override
public boolean startNestedScroll(int axes, int type) {
    //调用NestedScrollingChildHelper的startNestedScroll方法
    return getScrollingChildHelper().startNestedScroll(axes, type);
}

这里提一下,使用NestedScrollingChildHelper和NestedScrollingParentHelper是为了对Android 5.0 Lollipop (API 21)以前的版本做兼容。

NestedScrollingChildHelper的startNestedScroll方法

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    //根据嵌套滑动的类型来获取NestedScrollingParent
    if (hasNestedScrollingParent(type)) {
        // 嵌套滑动已经在处理过程中,直接返回true
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        //遍历父级控件
        while (p != null) {
            //注释1处
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                //注释2处
                setNestedScrollingParentForType(type, p);
                //注释3处
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

如果开启了嵌套滑动就遍历父控件,询问是否有父控件想要处理。

注释1处,ViewParentCompat的onStartNestedScroll方法

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
            int nestedScrollAxes, int type) {
    if (parent instanceof NestedScrollingParent2) {
        //注释1处,首先尝试调用NestedScrollingParent2的API
        return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                nestedScrollAxes, type);
    } else if (type == ViewCompat.TYPE_TOUCH) {//注释2处,NestedScrollingParent只处理正常的触摸类型

        if (Build.VERSION.SDK_INT >= 21) {//大于21版本直接调用ViewParent的方法即可。
            try {
                return parent.onStartNestedScroll(child, target, nestedScrollAxes);
            } catch (AbstractMethodError e) {
                Log.e(TAG, "ViewParent " + parent + " does not implement interface "
                        + "method onStartNestedScroll", e);
            }
        } else if (parent instanceof NestedScrollingParent) {
            //NestedScrollingParent处理
            return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                    nestedScrollAxes);
        }
    }
    return false;
}

注释1处,首先尝试调用NestedScrollingParent2的onStartNestedScroll(child, target,nestedScrollAxes, type)

注释2处,NestedScrollingParent只处理正常的触摸类型,丢弃掉type参数,onStartNestedScroll(child, target,nestedScrollAxes)。大于21版本直接调用ViewParent的方法即可。否则调用NestedScrollingParent处理。

后面的分析中,我们就只看NestedScrollingParent2相关的内容。

回到NestedScrollingChildHelper的startNestedScroll方法的注释2处。

private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
    switch (type) {
        case TYPE_TOUCH:
            //处理触摸类型滑动事件的外层控件
            mNestedScrollingParentTouch = p;
            break;
        case TYPE_NON_TOUCH:
            //处理惯性滑动类型事件的外层控件
            mNestedScrollingParentNonTouch = p;
            break;
    }
}

根据滑动的类型将ViewParent赋值给不同的变量保存。

注释3处

//注释3处
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);

内部会调用NestedScrollingParent2的onNestedScrollAccepted方法。

回到RecyclerView的onTouchEvent方法的注释2处

//注释2处
if (dispatchNestedPreScroll(
        canScrollHorizontally ? dx : 0,
        canScrollVertically ? dy : 0,
        mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
    //注释3处
    dx -= mReusableIntPair[0];
    dy -= mReusableIntPair[1];
    //...
}

注释2处,RecyclerView的dispatchNestedPreScroll方法

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
        int type) {
    return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
            type);
}

最终会调用NestedScrollingParent2的onNestedPreScroll方法。

注释3处,如果NestedScrollingParent2消耗了一些滑动距离,减去消耗的距离。

RecyclerView的onTouchEvent方法的注释4处,调用scrollByInternal方法

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0;
        int unconsumedY = 0;
        int consumedX = 0;
        int consumedY = 0;

        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            //内部自身滑动
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            //剩余的滑动距离
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        //将剩余的滑动距离再次分发给处理嵌套滑动的父View
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];
        boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

        //...       
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

方法内部首先调用scrollStep方法自身滑动,然后计算出剩余的滑动距离。然后将剩余的滑动距离再次分发给处理嵌套滑动的父View。

@Override
public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) {
    getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
            dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}

最终会调用NestedScrollingParent2的onNestedScroll方法。

到目前为止,NestedScrolling2和NestedScrolling并没有差别。

回到RecyclerView的onTouchEvent方法的注释5处,调用fling方法。从这里开始NestedScrolling2和NestedScrolling的处理逻辑产生了差异。

public boolean fling(int velocityX, int velocityY) {
    //...
    //注释1处
    if (!dispatchNestedPreFling(velocityX, velocityY)) {
        final boolean canScroll = canScrollHorizontal || canScrollVertical;
        //注释2处
        dispatchNestedFling(velocityX, velocityY, canScroll);

        //...

        if (canScroll) {
            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontal) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertical) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            //注释3处
            startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);

            velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
            velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
            //注释4处
            mViewFlinger.fling(velocityX, velocityY);
            return true;
        }
    }
    return false;
}

注释1处,先询问外层控件是否要处理惯性滑动,如果外层控件处理了,fling方法直接返回false,自身不滑动。

@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}

方法内部会调用外层控件的onNestedPreFling方法。

如果外层控件处理了惯性滑动,即外层控件的onNestedPreFling方法返回了true,内层控件自身不滑动。

如果!dispatchNestedPreFling(velocityX, velocityY)为true,说明外层控件没有处理惯性滑动。注释2处:

@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}

方法内部会调用外层控件的onNestedFling方法。

注释3处,这里和NestedScrolling有差异。

@Override
public boolean startNestedScroll(int axes, int type) {
    return getScrollingChildHelper().startNestedScroll(axes, type);
}

注释3处,外层控件没有处理惯性滑动,那么内层控件就要开始自身的惯性滑动,在开始惯性滑动之前,会调用startNestedScroll方法,通知外层控件内层控件,这时候的滑动类型是TYPE_NON_TOUCH

NestedScrollingChildHelper的startNestedScroll方法

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    //根据嵌套滑动的类型来获取NestedScrollingParent
    if (hasNestedScrollingParent(type)) {
        // 嵌套滑动已经在处理过程中,直接返回true
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        //遍历父级控件
        while (p != null) {
            //注释1处
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                //注释2处
                setNestedScrollingParentForType(type, p);
                //注释3处
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

注释1处,如果外层控件要处理TYPE_NON_TOUCH类型的滚动,外层控件的onStartNestedScroll返回true。

注释3处,外层控件调用onNestedScrollAccepted方法。

我们回到fling方法的注释4处

//注释4处
mViewFlinger.fling(velocityX, velocityY);

ViewFlinger的fling方法。

public void fling(int velocityX, int velocityY) {
    setScrollState(SCROLL_STATE_SETTLING);
    mLastFlingX = mLastFlingY = 0;
    //fling
    mOverScroller.fling(0, 0, velocityX, velocityY,
            Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
    //请求重新绘制
    postOnAnimation();
}

Scroller怎么实现滚动的,这里就不展开了。

ViewFlinger实现了Runnable接口。

@Override
public void run() {
    //...           
    final OverScroller scroller = mOverScroller;
    //如果滚动还没结束
    if (scroller.computeScrollOffset()) {
        final int x = scroller.getCurrX();
        final int y = scroller.getCurrY();
        int unconsumedX = x - mLastFlingX;
        int unconsumedY = y - mLastFlingY;
        mLastFlingX = x;
        mLastFlingY = y;
        int consumedX = 0;
        int consumedY = 0;

        // Nested Pre Scroll
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        //注释1处,每一帧的计算出来的滚动距离先分发到外层控件
        if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
                TYPE_NON_TOUCH)) {
            //注释2处,减去外层控件消耗的距离
            unconsumedX -= mReusableIntPair[0];
            unconsumedY -= mReusableIntPair[1];
        }

        // Local Scroll
        if (mAdapter != null) {
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            //注释3处,自身滚动
            scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            //减去自身滚动的距离
            unconsumedX -= consumedX;
            unconsumedY -= consumedY;
        }

        // Nested Post Scroll
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        //注释4处,将剩余的滑动距离分发给外层控件
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
                TYPE_NON_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];

        //...
        //继续请求重绘
        postOnAnimation();
    }
}

注释1处,每一帧的计算出来的滚动距离先分发到外层控件,外层控件调用onNestedPreScroll先滚动。

注释2处,减去外层控件消耗的距离。

注释3处,自身滚动。

注释4处,将剩余的滑动距离分发给外层控件。外层控件调用onNestedScroll来决定是否要进行处理。

这里再总结一下

  1. NestedScrolling2分发滚动事件的时候区分了滚动事件的类型:是正常的触摸滚动还是惯性滑动。

  2. 内层控件先调用dispatchNestedPreFling来处理惯性滑动。如果外层控件处理了惯性滑动,即外层控件的onNestedPreFling方法返回了true。那么NestedScrolling2和NestedScrolling的惯性滑动效果没有什么差异。

  3. 如果外层控件没有处理惯性滑动,也就是外层控件的onNestedPreFling方法返回了false。那么就会调用dispatchNestedFling方法并且内层控件自身开始惯性滑动mViewFlinger.fling(velocityX, velocityY),但是在惯性滑动的每一帧,通过Scroller计算出来的滚动距离通过dispatchNestedPreScroll先分发给外层控件。外层控件可以通过onNestedPreScroll先消耗部分滚动距离,然后内层控件再自身滚动。

相关文章

网友评论

      本文标题:Android嵌套滑动下篇

      本文链接:https://www.haomeiwen.com/subject/prkvwktx.html