博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
CoordinatorLayout三部曲学习之三:AppBarLayout联动源码学习
阅读量:6184 次
发布时间:2019-06-21

本文共 19631 字,大约阅读时间需要 65 分钟。

hot3.png

今天学习整理一下AppBarLayout与CoordinatorLayout以及Behavior交互逻辑的过程,首先使用一张图先概括一下各个类主要功能吧(本文章使用NestedScrollView充当滑动的内嵌子View)。

  • CoordinatorLayout实现NestedScrollingParent2接口,用于处理与滑动子View的联动交互(这里使用的是NestedScrollView),实际上交由Behavior进行处理,CoordinatorLayout为其代理类。
  • AppBarLayout中默认使用了AppBarLayout.Behavior,主要功能是接收CoordinatorLayout传输过来的滑动事件,并且相对应的进行处理,如NestedScrollView往上滑动到头时候,继续滑动则移动AppBarLayout到头。
  • NestedScrollView实现了NestedScrollingChild2接口,用于传输给CoordinatorLayout,并且消费CoordinatorLayout不消费的触摸事件,其中还是使用了AppBarLayout.ScrollingViewBehavior,功能是进行监听AppBarLayout的位移变化,从而进行相对应的变化,最明显的例子就是AppBarLayout上移过程中,NestedScrollView一起上移。

底下代码分析建立在下面例子之中:

....

那么现在我们直接看是看源码吧,这里主要弄明白两个逻辑:

  1. 当手指触摸AppBarLayout时候的滑动逻辑。
  2. 当手指触摸NestedScrollView时的滑动逻辑。

当手指触摸AppBarLayout时候的滑动逻辑

在弄清手指触摸AppBarLayout时候的滑动逻辑,需要了解一下AppBarLayout.Behavior这个类,ApprBarLayout的默认Behavior就是AppBarLayout.Behavior这个类,而AppBarLayout.Behavior继承自HeaderBehaviorHeaderBehavior又继承自ViewOffsetBehavior,这里先总结一下两个类的作用,需要详细的实现的请自行阅读源码吧:

  • ViewOffsetBehavior:该Behavior主要运用于View的移动,从名字就可以看出来,该类中提供了上下移动,左右移动的方法。
  • HeaderBehavior:该类主要用于View处理触摸事件以及触摸后的fling事件。

由于上面两个类功能的实现,使得AppBarLayout.Behavior具有了同时移动本身以及处理触摸事件的功能,在这篇文章又说明了CoordinateLayout的NestedScrollingParent2的实现全权委托给了Behavior类,所以AppBarLayout.Behavior就提供了ApprBarLayout对应的联动的方案。

那么我们直接从一开始入手,当我们手碰到AppBarLayout的时候,最终方法经由CoordinateLayout.OnInterceptEvent(...)调用了AppBarLayout.Behavior的对应方法中,上面说了HeaderBehavior处理了触摸事件,那么我们就看下对应的方法:

@Override    public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {      ....        if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {            return true;        }        switch (ev.getActionMasked()) {            case MotionEvent.ACTION_DOWN: {                mIsBeingDragged = false;                final int x = (int) ev.getX();                final int y = (int) ev.getY();                if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {                    mLastMotionY = y;                    mActivePointerId = ev.getPointerId(0);                    ensureVelocityTracker();                }                break;            }            case MotionEvent.ACTION_MOVE: {                final int activePointerId = mActivePointerId;                if (activePointerId == INVALID_POINTER) {                    // If we don't have a valid id, the touch down wasn't on content.                    break;                }                final int pointerIndex = ev.findPointerIndex(activePointerId);                if (pointerIndex == -1) {                    break;                }                final int y = (int) ev.getY(pointerIndex);                final int yDiff = Math.abs(y - mLastMotionY);                if (yDiff > mTouchSlop) {                    mIsBeingDragged = true;                    mLastMotionY = y;                }                break;            }            case MotionEvent.ACTION_CANCEL:            case MotionEvent.ACTION_UP: {                mIsBeingDragged = false;                mActivePointerId = INVALID_POINTER;                if (mVelocityTracker != null) {                    mVelocityTracker.recycle();                    mVelocityTracker = null;                }                break;            }        }        if (mVelocityTracker != null) {            mVelocityTracker.addMovement(ev);        }        return mIsBeingDragged;    }

上述代码非常简单,就是返回mIsBeingDragged,当移动过程中大于TouchSlop的时候,拦截时间,进而交给onTouchEvent(...)做处理:

public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {       ...        switch (ev.getActionMasked()) {            ...            case MotionEvent.ACTION_MOVE: {                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);                if (activePointerIndex == -1) {                    return false;                }                final int y = (int) ev.getY(activePointerIndex);                int dy = mLastMotionY - y;                if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {                    mIsBeingDragged = true;                    if (dy > 0) {                        dy -= mTouchSlop;                    } else {                        dy += mTouchSlop;                    }                }                if (mIsBeingDragged) {                    mLastMotionY = y;                    // We're being dragged so scroll the ABL                    scroll(parent, child, dy, getMaxDragOffset(child), 0);                }                break;            }            case MotionEvent.ACTION_UP:                if (mVelocityTracker != null) {                    mVelocityTracker.addMovement(ev);                    mVelocityTracker.computeCurrentVelocity(1000);                    float yvel = mVelocityTracker.getYVelocity(mActivePointerId);                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);                }         ...        return true;    }

主要逻辑还是在ACTION_MOVE中,可以看到在滑动过程中调用了scroll(...)方法,scroll(...)方法在HeaderBehavior中进行实现,最终调用到了额setHeaderTopBottomOffset(...)方法,该方法在AppBarLayout.Behavior中进行了重写,所以,我们直接看AppBarLayout.Behavior中的源码即可:

@Override   //newOffeset传入了dy,也就是我们手指移动距离上一次移动的距离,   //minOffset等于AppBarLayout的负的height,maxOffset等于0。        int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,                AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {            final int curOffset = getTopBottomOffsetForScrollingSibling();//获取当前的滑动Offset            int consumed = 0;			//AppBarLayout滑动的距离如果超出了minOffset或者maxOffset,则直接返回0            if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {               //矫正newOffset,使其minOffset<=newOffset<=maxOffset                newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);				//由于默认没设置Interpolator,所以interpolatedOffset=newOffset;                if (curOffset != newOffset) {                    final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()                            ? interpolateOffset(appBarLayout, newOffset)                            : newOffset;					//调用ViewOffsetBehvaior的方法setTopAndBottomOffset(...),最终通过					//ViewCompat.offsetTopAndBottom()移动AppBarLayout                    final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);                    //记录下消费了多少的dy。                    consumed = curOffset - newOffset;                   //没设置Interpolator的情况, mOffsetDelta永远=0                    mOffsetDelta = newOffset - interpolatedOffset;					....                     //分发回调OnOffsetChangedListener.onOffsetChanged(...)                    appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());                                      updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,                            newOffset < curOffset ? -1 : 1, false);                }           ...            return consumed;        }

上面注释也解释的比较清楚了,通过setTopAndBottomOffset()来达到了移动我们的AppBarLayout,那么这里AppBarLayout就可以跟着手上下移动了,但是,NestedScrollView还没跟着移动呢,如果按照上面的分析来看。上面的总结可以得知,NestedScrollView也实现了一个ScrollingViewBehaviorScrollingViewBehavior也继承自ViewOffsetBehavior,说明当前的NestedScrollView也具备上下移动的功能,在阅读ScrollingViewBehavior源码中发现其实现了如下方法:

@Override        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {            // We depend on any AppBarLayouts            return dependency instanceof AppBarLayout;        }        @Override        public boolean onDependentViewChanged(CoordinatorLayout parent, View child,                View dependency) {            offsetChildAsNeeded(parent, child, dependency);            return false;        }

通过上面方法,并且结合该文章的分析可以知道,NestedScrollView依赖于AppBarLayout,在AppBarLayout移动的过程中,NestedScrollView会随着AppBarLayout的移动回调onDependentViewChanged(...)方法,进而调用offsetChildAsNeeded(parent, child, dependency)

private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {            final CoordinatorLayout.Behavior behavior =                    ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();            if (behavior instanceof Behavior) {                final Behavior ablBehavior = (Behavior) behavior;//获取AppBarLayout的behavior				//移动对应的距离                ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())                        + ablBehavior.mOffsetDelta                        + getVerticalLayoutGap()                        - getOverlapPixelsForOffset(dependency));            }        }

这样我们就知道了当手指移动AppBarLayout时候的过程,下面整理一下:

首先通过Behavior.onTouchEvent(...)收到滑动距离,进而通知AppBarLayout.Behavior调用ViewCompat.offsetTopAndBottom()进行滑动;在AppBarLayout滑动的过程中,由于NestedScrollView中的ScrollingViewBehavior会依赖于AppBarLayout,所以在AppBarLayout滑动时候,NestedScrollView也会随着滑动,调用的方法也是ViewCompat.offsetTopAndBottom()

接下来再看下fling过程,fling过程在手指离开时候会判断调用,即从ACTION_UP开始:

case MotionEvent.ACTION_UP:                if (mVelocityTracker != null) {                    mVelocityTracker.addMovement(ev);                    mVelocityTracker.computeCurrentVelocity(1000);                    float yvel = mVelocityTracker.getYVelocity(mActivePointerId);                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);                }

可以看到直接调用了fling(...)中:

final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,            int maxOffset, float velocityY) {			//重置FlingRunnable        if (mFlingRunnable != null) {            layout.removeCallbacks(mFlingRunnable);            mFlingRunnable = null;        }        if (mScroller == null) {            mScroller = new OverScroller(layout.getContext());        }        mScroller.fling(                0, getTopAndBottomOffset(), // curr                0, Math.round(velocityY), // velocity.                0, 0, // x                minOffset, maxOffset); // 最大距离不超过AppbarLayout的高度        if (mScroller.computeScrollOffset()) {            mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);            ViewCompat.postOnAnimation(layout, mFlingRunnable);            return true;        } else {            onFlingFinished(coordinatorLayout, layout);            return false;        }    }

代码也比较简单,主要通过FlingRunnable循环调用setHeaderTopBottomOffset()方法就把AppBarLayout进行了View的移动:

private class FlingRunnable implements Runnable {        private final CoordinatorLayout mParent;        private final V mLayout;        FlingRunnable(CoordinatorLayout parent, V layout) {            mParent = parent;            mLayout = layout;        }        @Override        public void run() {            if (mLayout != null && mScroller != null) {                if (mScroller.computeScrollOffset()) {                    setHeaderTopBottomOffset(mParent, mLayout, mScroller.getCurrY());                    ViewCompat.postOnAnimation(mLayout, this);                } else {                    onFlingFinished(mParent, mLayout);                }            }        }    }

再看下fling完后做了什么,这里从上述代码可以看到调用了onFlingFinished(mParent, mLayout)AppBarLayout.Behavior中实现了当前方法:

@Override        void onFlingFinished(CoordinatorLayout parent, AppBarLayout layout) {            // At the end of a manual fling, check to see if we need to snap to the edge-child            snapToChildIfNeeded(parent, layout);        }

snapToChildIfNeeded(...)方法会根据scrollFlags来进行处理,由于在上面xml中使用的是layout_scrollFlags=scroll,所以在 当前方法中并不会进行对应的逻辑处理,那么fling操作到此也完成了,这里看到fling()操作只建立在AppBarLayout上,也就是说无论我们多快速滑动,始终在AppBarLayout到达最大滑动距离,也就是AppBarLayout高度时候滑动就会停止,不会去联动NestedScrollView。

当手指触摸NestedScrollView时的滑动逻辑。

接下来来看当手指触摸NestedScrollView时的滑动逻辑,在文章中分析过,NestedScrollView作为子View滑动时候会首先调用startNestedScroll(...)方法来询问父View即CoordinatorLayout是否需要消费事件,CoordinatorLayout作为代理做发给对应Behavior,这里就分发给了AppBarLayout.Behavior的回调onStartNestedScroll(...),方法如下:

@Override  	//directTargetChild=target=NestedScrollView        public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,                View directTargetChild, View target, int nestedScrollAxes, int type) {			//如果滑动方向为VERTICAL且AppBarLayout的高度不等于0且NestedScrollView可以滑动,started=true;            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;        }

上述Demo满足started=true,所以说明CoordinatorLayout需要进行消费事件的处理,然后回调AppBarLayout.Behavior.onNestedPreScroll():

@Override        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,                View target, int dx, int dy, int[] consumed, int type) {            if (dy != 0) {                int min, max;                if (dy < 0) {                    //手指向下滑动                    min = -child.getTotalScrollRange();//getTotalScrollRange返回child的高度                    max = min + child.getDownNestedPreScrollRange();//getDownNestedPreScrollRange()返回0                } else {                    // 手指向上滑动                    min = -child.getUpNestedPreScrollRange();//同getTotalScrollRange                    max = 0;                }                if (min != max) {				//计算消费的距离                    consumed[1] = scroll(coordinatorLayout, child, dy, min, max);                }            }        }

上面代码中出现了许多get....Range()方法主要是为了在我们使用对应LayoutParam.scrollflage=COLLAPSED相关标志的时候会使用到,由于我们分析代码不涉及到,所以都是返回的AppBarLayout的滑动高度或者0,上面代码已经注释了。接下来计算comsumed[1]:

final int scroll(CoordinatorLayout coordinatorLayout, V header,            int dy, int minOffset, int maxOffset) {        return setHeaderTopBottomOffset(coordinatorLayout, header,                getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);    }

这个方法上面已经分析过了,重新贴下:

@Override   //newOffeset传入了dy,也就是我们手指移动距离上一次移动的距离,   //minOffset等于AppBarLayout的负的height,maxOffset等于0。        int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,                AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {            final int curOffset = getTopBottomOffsetForScrollingSibling();//获取当前的滑动Offset            int consumed = 0;			//AppBarLayout滑动的距离如果超出了minOffset或者maxOffset,则直接返回0            if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {               //矫正newOffset,使其minOffset<=newOffset<=maxOffset                newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);				//由于默认没设置Interpolator,所以interpolatedOffset=newOffset;                if (curOffset != newOffset) {                    final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()                            ? interpolateOffset(appBarLayout, newOffset)                            : newOffset;					//调用ViewOffsetBehvaior的方法setTopAndBottomOffset(...),最终通过					//ViewCompat.offsetTopAndBottom()移动AppBarLayout                    final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);                    //记录下消费了多少的dy。                    consumed = curOffset - newOffset;                   //没设置Interpolator的情况, mOffsetDelta永远=0                    mOffsetDelta = newOffset - interpolatedOffset;					....                     //分发回调OnOffsetChangedListener.onOffsetChanged(...)                    appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());                                      updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,                            newOffset < curOffset ? -1 : 1, false);                }           ...            return consumed;        }

consumed的值有两种情况:

  • 当滑动的距离在minOffset和maxOffset区间之内,则consume!=0,也就说明需要AppBarLayout进行消费,这里对应着AppBarLayout还没移出我们的视线时候的消费情况。
  • 当滑动的距离超出了minOffset或者maxOffset后,则consume==0,也就说明需要AppBarLayout不进行消费了,这里对应着AppBarLayout移出我们的视线时候的消费情况。

回到AppBarLayout.Behavior中继续看相关方法:

@Override        public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,                View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,                int type) {				//这个方法是做个兼容,在Demo中倒是没试出来调用时机,选择性忽略            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);            }        }        @Override        public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl,                View target, int type) {            if (type == ViewCompat.TYPE_TOUCH) {                // If we haven't been flung then let's see if the current view has been set to snap                snapToChildIfNeeded(coordinatorLayout, abl);//这个方法内部逻辑不会走,原因是scroll_flag=scroll            }            // Keep a reference to the previous nested scrolling child            mLastNestedScrollingChildRef = new WeakReference<>(target);        }

上面的方法比较简单,就不介绍了,接下来看下手指离开时候的处理,这时候应该回调对应Behavior的fling()方法,但是AppBarLayout在ACTION_UP这里并没有做多余的处理,甚至连fling相关回调都没调用,那只能从NestedScrollView的computeScroll()方法研究了:

public void computeScroll() {        if (mScroller.computeScrollOffset()) {            final int x = mScroller.getCurrX();            final int y = mScroller.getCurrY();            int dy = y - mLastScrollerY;            // Dispatch up to parent            if (dispatchNestedPreScroll(0, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) {                dy -= mScrollConsumed[1];            }            if (dy != 0) {                final int range = getScrollRange();                final int oldScrollY = getScrollY();                overScrollByCompat(0, dy, getScrollX(), oldScrollY, 0, range, 0, 0, false);                final int scrolledDeltaY = getScrollY() - oldScrollY;                final int unconsumedY = dy - scrolledDeltaY;                if (!dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, null,                        ViewCompat.TYPE_NON_TOUCH)) {                    final int mode = getOverScrollMode();                    final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS                            || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);                    if (canOverscroll) {                        ensureGlows();                        if (y <= 0 && oldScrollY > 0) {                            mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());                        } else if (y >= range && oldScrollY < range) {                            mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());                        }                    }                }            }            // Finally update the scroll positions and post an invalidation            mLastScrollerY = y;            ViewCompat.postInvalidateOnAnimation(this);        } else {            // We can't scroll any more, so stop any indirect scrolling            if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {                stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);            }            // and reset the scroller y            mLastScrollerY = 0;        }    }

一看到这我们就明白了,其实fling也就是对应的由手机来模拟我们触摸的过程,所以回调调用dispatchNestedPreScroll()dispatchNestedScroll()来进行通知AppBarLayout进行滑动,滑动的过程还是上面那一套,手指向下滑动时,当NestedScrollView滑动到顶的时候,就交付消费dy给AppBarLayout处理,而手指向上滑动时候则相反。

转载于:https://my.oschina.net/u/3863980/blog/1922943

你可能感兴趣的文章
2919 选择题
查看>>
1042. 字符统计(20)
查看>>
xpath详细讲解
查看>>
ECMAScript 5 —— Array 类型 (二)
查看>>
mysql数据库数据(字段数过大)太多导入不了的解决方法
查看>>
字符串逆序输出
查看>>
【原】Java学习笔记011 - 数组
查看>>
将数组A中的内容和数组B中的内容进行交换。(数组一样大)
查看>>
oracle物化视图
查看>>
浅谈JavaScript中的定时器
查看>>
SpringMVC修改功能
查看>>
Unity 编辑器 Inspector
查看>>
ArcGIS 客户端API加载大量数据的几种解决方法(转载)
查看>>
性能测试初学_loadrunner脚本增强
查看>>
通过队列解决Lucene文件并发创建索引
查看>>
Sharepoint ECMAScript
查看>>
jQuery获取自动截取过长的文本内容,显示成省略号
查看>>
nginx 代理http配置实例
查看>>
python: 不同级别的日志输出到不同文件的日志类
查看>>
一般处理程序HttpHandler的应用
查看>>