欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

关于CoordinatorLayout AppBarLayout原理的一些分析

程序员文章站 2024-01-03 10:20:22
...

这几天学了一些CoordinatorLayout、AppBarLayout配合使用的一些方法,之前还写了一篇CoordinatorLayout Behavior一些笔记,通过这几天对源码的阅读,现在对CoordinatorLayout、AppBarLayout这部分的内容有了更深一层的理解,接下来我就把我所理解的源码简单的分析一下。

一、 NestedScrolling机制

CoordinatorLayout、AppBarLayout分别实现了NestedScrolling机制中需要的接口和接口中的一些方法,如果大家对NestedScrolling不是很了解,可以先去网上了解一下,这里我简单说明一下这个机制的原理:Nested这个单词的意思是“嵌套”,这个机制其实就是嵌套滑动的一种处理机制,它和之前只能单一View消耗滑动事件的处理机制不同,它会在子View处理滑动事件时,先将滑动事件传递到父View中,询问父View是否需要消耗滑动事件,如果父View需要消耗滑动事件,子View会将此次x,y滑动的距离先传递到父View中,父View会先消耗滑动事件,如果父View没消耗全部的滑动距离,子View会消耗剩余的滑动距离,如果剩余的滑动距离大于子View剩余需要的滑动距离(例如RecyclerView距离自身Content滑动到顶部的距离只有10,但是此次滑动距离dy有50,父View消耗了30,剩余20大于RecyclerView剩余需要滑动的距离),子View会把剩下的滑动距离再次传递给父View,由父View去消耗。
我推荐两篇我觉得还挺不错的文章可以帮助理解这个机制:Android NestedScrolling机制完全解析 带你玩转嵌套滑动android NestedScroll嵌套滑动机制完全解析-原来如此简单

二、可以实现的效果

说了这么多,这个机制到底可以实现什么样的效果呢,其实就是滑动起来非常的顺滑,例如,我在界面中放了一个RecyclerView,RecyclerView上面放了一个AppBarLayout包裹的ImageView,当我滑动这个界面时,不会像原来那种机制需要在RecyclerView滑动到顶部时,需要抬起手指进行下次滑动才能把RecyclerView上面的View滑出屏幕以外,效果图如下:
关于CoordinatorLayout AppBarLayout原理的一些分析

三、原理分析

下面开始进行我对源码阅读的分析理解,这里主要分成两个部分,主要是RecyclerView、CoordinatorLayout 、AppBarLayout如何实现了NestedScrolling机制。
先简要概括一下总体的中心思想,根据上文对NestedScrolling的介绍,这里的RecyclerView就是子View,CoordinatorLayout就是父View,AppBarLayout是父View在判断是否消耗事件,在判断方法中主要依据的View。主要的过程都是在RecyclerView的onTouchEvent中,分别在Down和Move事件中完成了整个机制的流程。这里说一句题外话,为什么ListView、GirdView不能实现这种效果?因为这两个View并没有实现NestedScrolling机制中相关的方法,可以看一下RecyclerView源码,我们会发现RecycerView定义如下:

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild

1.RecyclerView中MotionEvent.ACTION_DOWN做了哪些事儿?

这里我先来一张流程图:
关于CoordinatorLayout AppBarLayout原理的一些分析
这里所做的一件事儿,就是子View在滑动事件开始时,传递给父View,父View会去判断是否需要消耗此次事件,下面就是源码的分析

//RecyclerView
@Override
    public boolean onTouchEvent(MotionEvent e){
        //.................
        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;
                }
                //调用NestedScrollingChildHelper
                startNestedScroll(nestedScrollAxis);
            } break;
            //..........
        }

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

        return true;
    }

上面的startNestedScroll方法就会调用到NestedScrollingChildHelper中的startNestedScroll方法。Helper中该方法的实现如下:

    //NestedScrollingChildHelper
    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            //通过while循环,不断的去判断是否有View的ParentView需要消耗这次滑动事件
            while (p != null) {
                //判断parent是否需要消耗
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    //父View消耗滑动事件
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
       //循环结束,没有发现需要消耗的View
        return false;
    }

在这个方法中,所做的事儿只有一件,去循环遍历并且询问这个View的ParentView和ParentView的ParentView是否需要消耗这次事件,如果有消耗的返回true否则返回false,这里判断的方法使用了ViewParentCompat.onStartNestedScroll(p, child, mView, axes),这个方法实现很简单,里面仅仅是调用了我们传入的参数p的onStartNestedScroll方法,在我的事例中,p就是CoordinatorLayout,所以我们可以直接查看CoordinatorLayout中onStartNestedScroll方法的实现

//CoordinatorLayout
@Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        boolean handled = false;

        final int childCount = getChildCount();
        //仍然是遍历子View,判断是否有View需要消耗
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == View.GONE) {
                // If it's GONE, don't dispatch
                continue;
            }
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            //判断behavior是否为空
            if (viewBehavior != null) {
                //获取View是否消耗滑动事件
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                        nestedScrollAxes);
                handled |= accepted;

                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }

这里我们可以看到,当父View也就是CoordinatorLayout判断是否消耗滑动事件的方式也很简单,就是遍历自己的子View,如果子View有消耗就返回true,这里使用的是 “|=” 只要有子View需要接收便是true,接着在当前例子中,ImageView包裹在AppBarLayout,那么在这个函数遍历中,就会获取到AppBarLayout的Behavior,并且调用AppBarLayout的中Behavior的onStartNestedScroll方法,就是上面时序图的最后一个LifeLine,AppBarLayout的中Behavior的onStartNestedScroll实现如下:

 //AppBarLayout$Behavior
 @Override
        public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                View directTargetChild, View target, int nestedScrollAxes) {
            // Return true if we're nested scrolling vertically, and we have scrollable children
            // and the scrolling view is big enough to scroll
            final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
                    && child.hasScrollableChildren()
                    && parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();

            if (started && mOffsetAnimator != null) {
                // Cancel any offset animation
                mOffsetAnimator.cancel();
            }

            // A new nested scroll has started so clear out the previous ref
            mLastNestedScrollingChildRef = null;

            return started;
        }

这里可以看到,在AppBarLayoutBehavioronStartNestedScrollnestedScrollAxesCoordinatorLayoutAppBarLayoutAppBarLayoutBehavior,我在CoordinatorLayout代码中并没有找到,后来忽然发现,原来是用了注解的形式在AppBarLayout开头声明了

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout

这样到此为止,我们第一个阶段的分析就完成了,当RecyclerView发生了MotionEvent.ACTION_DOWN事件时,经历了NestedScrollingChildHelper->ViewParentCompat->CoordinatorLayout->AppBarLayout来完成NestedScrolling机制中的第一步,子View在滑动事件发生时,告知父View是否需要消耗事件

2.RecyclerView中MotionEvent.ACTION_MOVE做了哪些事儿?

同样这里也首先来一张流程图:

关于CoordinatorLayout AppBarLayout原理的一些分析
这里做的事儿,就是在父View需要处理滑动事件时,先将滑动事件传递到父View,然后拿到剩下未消耗的距离自己消耗,如果在自己消耗后还有剩余,那么在传递给父View,下面开始一步一步的分析源码

  //RecyclerView
 @Override
    public boolean onTouchEvent(MotionEvent e) {
         //...............
        switch (action) {
            //..................
            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id " +
                            mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                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;
                //调用dispatchNestedPreScroll方法并且mScrollConsumed数组记录消耗
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }
                //......................
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
                    //scrollByInternal传递剩余消耗
                    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;
            //.........................
        }

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

        return true;
    }

这里首先是在子View消耗事件之前,通过调用dispatchNestedPreScroll方法,如果父View消耗事件,则子View的dx,dy会减去已经消耗掉的,dispatchNestedPreScroll主要调用了NestedScrollingChildHelper的dispatchNestedPreScroll方法,我们看一下实现

//NestedScrollingChildHelper
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    if (mTempNestedScrollConsumed == null) {
                        mTempNestedScrollConsumed = new int[2];
                    }
                    consumed = mTempNestedScrollConsumed;
                }
                consumed[0] = 0;
                consumed[1] = 0;
                //这里调用了onNestedPreScroll方法询问父View是否消耗
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

这里依然使用了ViewParentCompat的方法,ViewParentCompat.onNestedPreScroll方法依然是调用我们传入的父View的onNestedPreScroll方法,这里我的父View依然还是CoordinatorLayout,这里没用循环遍历,是因为之前我们已经在ViewParentCompat.startNestedScroll遍历中保存了mNestedScrollingParent为CoordinatorLayout,所以我们下一步可以直接查看CoordinatorLayout的onNestedPreScroll

//CoordinatorLayout
 @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                mTempIntPair[0] = mTempIntPair[1] = 0;
                //传递给子View进行消耗
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);

                xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                        : Math.min(xConsumed, mTempIntPair[0]);
                yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                        : Math.min(yConsumed, mTempIntPair[1]);

                accepted = true;
            }
        }

        consumed[0] = xConsumed;
        consumed[1] = yConsumed;

        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

可以看到,CoordinatorLayout对于是否消耗事件,依然是传递给子View去消耗,在我们例子中的这个布局下能够消耗掉这个事件的View就是AppBarLayout,这样事件就又传递给了CoordinatorLayout的子View去消耗,消耗完了以后,可以看到下面还调用了onChildViewsChanged这方法,这个方法的作用是做一些和Behavior相关的操作,有关这部分内容可以看我的上篇文章CoordinatorLayout Behavior一些笔记,这样对于viewBehavior.onNestedPreScroll这里,我们需要查看AppBarLayout$Behavior中的实现:

//AppBarLayout$Behavior
@Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dx, int dy, int[] consumed) {
            if (dy != 0 && !mSkipNestedPreScroll) {
                int min, max;
                if (dy < 0) {
                    // We're scrolling down
                    min = -child.getTotalScrollRange();
                    max = min + child.getDownNestedPreScrollRange();
                } else {
                    // We're scrolling up
                    min = -child.getUpNestedPreScrollRange();
                    max = 0;
                }
                //AppbarLayout只消耗dy的事件,将消耗的事件赋值给consumed[1]并且scroll自身内容
                consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
            }
        }

到这里,如果AppBarLayout需要消耗滑动事件的话,就会消耗并且滚动自己的内容。大家有没有好奇一点,整个过程并没有返回值,那么RecyclerView是如何通过一大堆调用拿到AppBarLayout的消耗呢?其实很简单,就是Java中传递数组时,和C++中按值传递不一样,Java中非基本类型的传递类似于C++中按引用传递,还记得我们RecyclerView中调用dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)这个方法么?这里mScrollConsumed数组就是传递到onNestedPreScroll中的 int[] consumed,所以只要给 int[] consumed赋值,就可以在RecyclerView拿到消耗的dx,dy,分别对应mScrollConsumed[0]和mScrollConsumed[1],接着在AppBarLayout处理完以后,我们还是看上面的时序图,会发现,NestedScrollingChildHelper中是需要有返回值的,需要RecyclerView判断父View是否消耗了滑动事件,我们可以看上面NestedScrollingChildHelper的dispatchNestedPreScroll方法中在子View处理消耗事件后,会return consumed[0] != 0 || consumed[1] != 0;

// NestedScrollingChildHelper
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
                //........................
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

这样这个父View的消耗就在子View滑动之前完成了,接着就是子View的滑动,并且如果还有没消耗完的滑动距离会传递给父View让父View去处理,这里的过程主要就是在RecyclerView的scrollByInternal方法中了:

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

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            eatRequestLayout();
            onEnterLayoutOrScroll();
            TraceCompat.beginSection(TRACE_SCROLL_TAG);
            if (x != 0) {
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }
            TraceCompat.endSection();
            repositionShadowingViews();
            onExitLayoutOrScroll();
            resumeRequestLayout(false);
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

         //dispatchNestedScroll方法传递consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset给父View
        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
            // 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];
        } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
            if (ev != null) {
                pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
            }
            considerReleasingGlowsOnScroll(x, y);
        }
        if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
        if (!awakenScrollBars()) {
            invalidate();
        }
        return consumedX != 0 || consumedY != 0;
    }

scrollByInternal在onTouchEvent中会被调用,scrollByInternal通过调用dispatchNestedScroll把事件传递给父View,其实仍然是调用了NestedScrollingChildHelper的dispatchNestedScroll,该方法的实现:

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

ViewParentCompat.onNestedScroll方法依然是调用了CoordinatorLayout的onNestedScroll方法,实现如下:

//CoordinatorLayout
@Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed) {
        final int childCount = getChildCount();
        boolean accepted = false;

        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
                accepted = true;
            }
        }

        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

这也依然一样,CoordinatorLayout会循环遍历,交给子View去处理,这里仍然还是AppBarLayout$Behavior的onNestedScroll:

//AppBarLayout$Behavior
 @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed) {
            if (dyUnconsumed < 0) {
                // If the scrolling view is scrolling down but not consuming, it's probably be at
                // the top of it's content
                scroll(coordinatorLayout, child, dyUnconsumed,
                        -child.getDownNestedScrollRange(), 0);
                // Set the expanding flag so that onNestedPreScroll doesn't handle any events
                mSkipNestedPreScroll = true;
            } else {
                // As we're no longer handling nested scrolls, reset the skip flag
                mSkipNestedPreScroll = false;
            }
        }

到这未消耗的事件就又传递到AppBarLayout了,这里的注释很清晰:If the scrolling view is scrolling down but not consuming, it’s probably be at the top of it’s content,翻译一下,就是如果scrolling view 向下滚动,但是没有消耗滚动事件,可能是已经滑倒了顶部,例如RecyclerView已经滑倒了第一个Item,然后AppBarLayout就会消耗剩余的事件在scroll方法中
到此,整个流程就已经清晰明了了,整个Scrolling机制在CoordinatorLayout AppBarLayout中就是这么实现的,整体的流程的概览就可以参照上面的两个时序图,对整个源码的分析和理解后,以后在使用起来我们就会更加的得心应手。

相关标签: 源码分析

上一篇:

下一篇: