CoordinatorLayout源码解析
来源:互联网 发布:巫师3优化好 编辑:程序博客网 时间:2024/05/22 06:49
Google
推出Support Design Library
已经两年了,没错,两年了!虽然推出了这么久,也只是使用,并没有深入研究过,所以想要深入了解一下,于是有了此文。
备注:本文源码基于25.3.0
监听View的变化
在onAttachedToWindow()
方法中,使用ViewTreeObserver
注册一个回调监听View变化。
@Overridepublic void onAttachedToWindow() { ... if (mNeedsPreDrawListener) { if (mOnPreDrawListener == null) { mOnPreDrawListener = new OnPreDrawListener(); } final ViewTreeObserver vto = getViewTreeObserver(); vto.addOnPreDrawListener(mOnPreDrawListener); //视图将要绘制时,会回调此接口 } ...}
查看下OnPreDrawListener
的具体实现,发现其调用了onChildViewsChanged()
方法,并传递了一个type
为EVENT_PRE_DRAW
的参数,来进行标记。让我们来看看onChidViewsChanged()
方法具体做了些什么?
子View能够相互依赖工作的根源——onChildViewsChanged()
// type:根据type来判断是什么时期调用的此方法,具体有3个type// EVENT_PRE_DRAW 将要绘制时, EVENT_NESTED_SCROLL 滚动, EVENT_VIEW_REMOVED 移除final void onChildViewsChanged(@DispatchChangeEvent final int type) { final int layoutDirection = ViewCompat.getLayoutDirection(this); final int childCount = mDependencySortedChildren.size(); final Rect inset = acquireTempRect(); final Rect drawRect = acquireTempRect(); final Rect lastDrawRect = acquireTempRect(); for (int i = 0; i < childCount; i++) { final View child = mDependencySortedChildren.get(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) { // Do not try to update GONE child views in pre draw updates. continue; } // Check child views before for anchor for (int j = 0; j < i; j++) { final View checkChild = mDependencySortedChildren.get(j); if (lp.mAnchorDirectChild == checkChild) { //调整child的位置到其所依赖的View(layout_anchor所设置的)的相关位置 offsetChildToAnchor(child, layoutDirection); } } // Get the current draw rect of the view getChildRect(child, true, drawRect); // Accumulate inset sizes // 根据不同的Gravity,记录view进入CoordinatorLayout的尺寸 // lp.insetEdge保存的是子View以什么方式进入CoordinatorLayout if (lp.insetEdge != Gravity.NO_GRAVITY && !drawRect.isEmpty()) { final int absInsetEdge = GravityCompat.getAbsoluteGravity( lp.insetEdge, layoutDirection); switch (absInsetEdge & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.TOP: inset.top = Math.max(inset.top, drawRect.bottom); break; case Gravity.BOTTOM: inset.bottom = Math.max(inset.bottom, getHeight() - drawRect.top); break; } switch (absInsetEdge & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: inset.left = Math.max(inset.left, drawRect.right); break; case Gravity.RIGHT: inset.right = Math.max(inset.right, getWidth() - drawRect.left); break; } } // lp.dodgeInsetEdges 保存的是子View(A)需要避免的‘位置’ // 其他子View(B)以相同的方式进入,会影响子View(A)的显示,子View(A)需要改变自身,避免被覆盖 // Dodge inset edges if necessary if (lp.dodgeInsetEdges != Gravity.NO_GRAVITY && child.getVisibility() == View.VISIBLE) { offsetChildByInset(child, inset, layoutDirection); //子View(A)改变自身位置 } if (type != EVENT_VIEW_REMOVED) { // Did it change? if not continue getLastChildRect(child, lastDrawRect); if (lastDrawRect.equals(drawRect)) { continue; } recordLastChildRect(child, drawRect); } // Update any behavior-dependent views for the change for (int j = i + 1; j < childCount; j++) { final View checkChild = mDependencySortedChildren.get(j); final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams(); final Behavior b = checkLp.getBehavior(); //layoutDependsOn方法用于确定两个View是否有依赖关系 if (b != null && b.layoutDependsOn(this, checkChild, child)) { if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) { // If this is from a pre-draw and we have already been changed // from a nested scroll, skip the dispatch and reset the flag checkLp.resetChangedAfterNestedScroll(); continue; } final boolean handled; switch (type) { case EVENT_VIEW_REMOVED: // EVENT_VIEW_REMOVED means that we need to dispatch // onDependentViewRemoved() instead b.onDependentViewRemoved(this, checkChild, child); handled = true; break; default: // 回调Behavior的b.onDependentViewChanged,处理是否跟随依赖View,而改变自身状态(具体的改变状态的方式,在此方法中处理) handled = b.onDependentViewChanged(this, checkChild, child); break; } if (type == EVENT_NESTED_SCROLL) { // If this is from a nested scroll, set the flag so that we may skip // any resulting onPreDraw dispatch (if needed) checkLp.setChangedAfterNestedScroll(handled); } } } } releaseTempRect(inset); releaseTempRect(drawRect); releaseTempRect(lastDrawRect);}
通过对onChildViewsChanged()
方法的解读,发现其使用了三种方式来处理View之间的关联。
- 通过判断
layout_ahchor
所设置的锚视图,使用offsetChildToAnchor()
方法来改变View的位置。 - 通过
LayoutParams
中存储的insetEdge
和dodgeInsetEdges
来进行判断(详细信息请查看注释),最后调用offsetChildByInset()
方法来改变View的位置。 - 通过
Behavior
的layoutDependsOn()
方法,如果Behavior
重写了layoutDependsOn()
方法,在其中做了View的依赖判断,最终会回调Behavior
的onDependentViewChanged()
方法,具体要怎么处理,就是onDependentViewChanged()
实现的。
注意:mDependencySortedChildren
是根据View的依赖关系排序存储的,具体排序涉及到的方法有prepareChildren()
和CoordinatorLayout.LayoutParams.dependsOn()
,在这里就不做阐述了,有兴趣的可以自行研读。
我们再来看看offsetChidToAnchor()
和offsetChildByInset()
方法。
设置layout_anchor
锚视图后,View的位置是如何改变的——offsetChildToAnchor
void offsetChildToAnchor(View child, int layoutDirection) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.mAnchorView != null) { final Rect anchorRect = acquireTempRect(); final Rect childRect = acquireTempRect(); final Rect desiredChildRect = acquireTempRect(); // 获取Anchor View的Rect getDescendantRect(lp.mAnchorView, anchorRect); // 获取child View的Rect getChildRect(child, false, childRect); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); // 获取到想要的Rect,保存在desiredChildRect中 getDesiredAnchoredChildRectWithoutConstraints(child, layoutDirection, anchorRect, desiredChildRect, lp, childWidth, childHeight); // 通过对desiredChildRect和childRect比对,看是否位置发生了变化 boolean changed = desiredChildRect.left != childRect.left || desiredChildRect.top != childRect.top; // 加入margin和padding,计算最终的Rect,保存在desiredChildRect中 constrainChildRect(lp, desiredChildRect, childWidth, childHeight); final int dx = desiredChildRect.left - childRect.left; final int dy = desiredChildRect.top - childRect.top; //改变View的位置 if (dx != 0) { ViewCompat.offsetLeftAndRight(child, dx); } if (dy != 0) { ViewCompat.offsetTopAndBottom(child, dy); } // 如果位置有变化,且View有Behavior,则通知其Behavior的onDependentViewChanged方法 if (changed) { // If we have needed to move, make sure to notify the child's Behavior final Behavior b = lp.getBehavior(); if (b != null) { b.onDependentViewChanged(this, child, lp.mAnchorView); } } releaseTempRect(anchorRect); releaseTempRect(childRect); releaseTempRect(desiredChildRect); }}
分析完offsetChildToAnchor()
,我们知道了,当设置了layout_anchor
,如果View的位置改变,也会回调Behavior
的onDependentViewChanged()
方法。
那offsetChildByInset()
又做了些什么呢?
子View之间是如何避免被“遮挡”——offsetChidByInset
private void offsetChildByInset(final View child, final Rect inset, final int layoutDirection) { ... final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Behavior behavior = lp.getBehavior(); final Rect dodgeRect = acquireTempRect(); final Rect bounds = acquireTempRect(); bounds.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); // getInsetDodgeRect方法默认返回false,需要自己实现最终的Rect并设置给dodgeRect,并返回true if (behavior != null && behavior.getInsetDodgeRect(this, child, dodgeRect)) { // Make sure that the rect is within the view's bounds if (!bounds.contains(dodgeRect)) { throw new IllegalArgumentException("Rect should be within the child's bounds." + " Rect:" + dodgeRect.toShortString() + " | Bounds:" + bounds.toShortString()); } } else { dodgeRect.set(bounds); } // We can release the bounds rect now releaseTempRect(bounds); if (dodgeRect.isEmpty()) { // Rect is empty so there is nothing to dodge against, skip... releaseTempRect(dodgeRect); return; } // 获取需要躲避的方向 final int absDodgeInsetEdges = GravityCompat.getAbsoluteGravity(lp.dodgeInsetEdges, layoutDirection); // 根据absDodgeInsetEdges,改变View相应的位置 boolean offsetY = false; if ((absDodgeInsetEdges & Gravity.TOP) == Gravity.TOP) { int distance = dodgeRect.top - lp.topMargin - lp.mInsetOffsetY; if (distance < inset.top) { setInsetOffsetY(child, inset.top - distance); offsetY = true; } } ...// 省略Gravity.BOTTOM判断,和Gravity.TOP类似 if (!offsetY) { setInsetOffsetY(child, 0); } boolean offsetX = false; if ((absDodgeInsetEdges & Gravity.LEFT) == Gravity.LEFT) { int distance = dodgeRect.left - lp.leftMargin - lp.mInsetOffsetX; if (distance < inset.left) { setInsetOffsetX(child, inset.left - distance); offsetX = true; } } ...// 省略Gravity.RIGHT判断,和Gravity.LEFT类似 if (!offsetX) { setInsetOffsetX(child, 0); } releaseTempRect(dodgeRect);}
阅读完offsetChildByInset()
方法,发现如果我们的View需要避免某个方向的其他View进入,我们需要实现Behavior
的getInsetDodgeRect()
方法,还要设置LayoutParams.dodgeInsetEdges
,dodgeInsetEdges
的设置可以重写Behavior
的onAttachedToLayoutParams(CoordinatorLayout.LayoutParams params)
方法。
CoodinatorLayout中的滚动机制——NestedScrolling
NestedScrolling
机制,是从Android 5.0
开始引入,提供了一套父View和子View滑动交互的机制。包含两个接口和两个帮助类:
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper
父View必须实现NestedScrollingParent
接口,而其必须要有一个子View实现NestedScrollingChild
接口,只有这样才能达到预想的滑动交互效果。实现NestedScrollingChild
接口很简单,只需要在其实现的方法中调用NestedScrollingChildHelper
中对应的方法即可。并在相应的Touch事件中调用startNestedScroll()
方法以及stopNestedScroll()
方法,剩下的通知父View等事情,NestedScrollingChildHelper
都帮我们处理好了。
对与NestedScrolling
机制,这里只做简要的说明,具体的分析留待以后的文章。如果有想了解的,可先自行搜索相关文章。
回到正题CoordinatorLayout
,没错,CoordinatorLayout
充当的就是父View的角色,其实现了NestedScrollingParent
接口,具体的实现如下所示:
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { boolean handled = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { ...// 省略获取view及其LayoutParams final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target, nestedScrollAxes); handled |= accepted; //存储是否要处理滚动 lp.acceptNestedScroll(accepted); } else { lp.acceptNestedScroll(false); } } return handled;}@Overridepublic void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); ... final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { ...// 省略获取view及其LayoutParams final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes); } }}@Overridepublic void onStopNestedScroll(View target) { mNestedScrollingParentHelper.onStopNestedScroll(target); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { ...// 省略获取view及其LayoutParams if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { viewBehavior.onStopNestedScroll(this, view, target); } lp.resetNestedScroll(); lp.resetChangedAfterNestedScroll(); } ...}@Overridepublic 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++) { ...// 省略获取view及其LayoutParams 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); }}@Overridepublic 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++) { ...// 省略获取view及其LayoutParams if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { mTempIntPair[0] = mTempIntPair[1] = 0; 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代表自身去执行相应方向的距离滑动 consumed[0] = xConsumed; consumed[1] = yConsumed; if (accepted) {//注意调用了onChildViewsChanged onChildViewsChanged(EVENT_NESTED_SCROLL); }}@Overridepublic boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { boolean handled = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { ...// 省略获取view及其LayoutParams if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY, consumed); } } if (handled) {// 注意调用了onChildViewsChanged onChildViewsChanged(EVENT_NESTED_SCROLL); } return handled;}@Overridepublic boolean onNestedPreFling(View target, float velocityX, float velocityY) { boolean handled = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { ...// 省略获取view及其LayoutParams if (!lp.isNestedScrollAccepted()) { continue; } final Behavior viewBehavior = lp.getBehavior(); if (viewBehavior != null) { handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY); } } return handled;}@Overridepublic int getNestedScrollAxes() { return mNestedScrollingParentHelper.getNestedScrollAxes();}
代码虽然有点长,但是没有什么难点,相应的方法都被委派给了Behavior
的对应方法处理。如果我们的子View想要响应滚动效果,只需要重写Behavior
的相关方法。
开发最关心的——Behavior
通过上面的分析,大家应该都发现了,几乎所有的东西都和Behavior
有关。我们想要自己的控件在CoordinatorLayout
中有炫酷的效果,那么我们只需要自定义自己的Behavior
,实现相关方法即可。
如何给View设置Behavior
- 在xml中通过
layout_behavior
绑定,例如app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior"
- 在自定义的控件中,使用
DefaultBehavior
注解绑定,例如AppBarLayout
中的@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
- 在代码中通过
LayoutParams
设置
需要注意的:
Behavior
需要设置给CoordinatorLayout
的直接子View,因为Behavior
的解析是在CoordinatorLayout.LayoutParams
的构造方法中进行的,只有直接子View才具有CoordinatorLayout.LayoutParams
。- 自定义
Behavior
必须具有Behavior(Context context, AttributeSet attrs)
构造方法,因为Behavior
的实例化是靠类的反射完成的,具体可看如下源码:
//指定Behavior的参数类型static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] { Context.class, AttributeSet.class};static Behavior parseBehavior(Context context, AttributeSet attrs, String name) { if (TextUtils.isEmpty(name)) { return null; } // 获取完整包名 final String fullName; if (name.startsWith(".")) { // Relative to the app package. Prepend the app package name. fullName = context.getPackageName() + name; } else if (name.indexOf('.') >= 0) { // Fully qualified package name. fullName = name; } else { // Assume stock behavior in this package (if we have one) fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME) ? (WIDGET_PACKAGE_NAME + '.' + name) : name; } try { Map<String, Constructor<Behavior>> constructors = sConstructors.get(); if (constructors == null) { constructors = new HashMap<>(); sConstructors.set(constructors); } Constructor<Behavior> c = constructors.get(fullName); if (c == null) { // 利用反射获取Behavior final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true, context.getClassLoader()); // 指定了具体的构造参数类型 c = clazz.getConstructor(CONSTRUCTOR_PARAMS); c.setAccessible(true); constructors.put(fullName, c); } return c.newInstance(context, attrs); } catch (Exception e) { throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e); }}
Behavior中常用的方法
layoutDependsOn
通过之前的分析,layoutDependsOn
是用来确定依赖关系的,如果想要控件依赖某个控件,重写这个方法是必须的。例如给RecycleView
设置的ScrollingViewBehavior
,关联AppBarLayout
,代码如下:
@Overridepublic boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { // We depend on any AppBarLayouts return dependency instanceof AppBarLayout;}
onDependentViewChanged
根据之前的分析,此方法会在依赖View发生改变的时候回调,我们可以在此方法中做相应的处理,达到想要的效果。
/* <p>If the Behavior changes the child view's size or position, it should return true. * The default implementation returns false.</p> * * @param parent the parent view of the given child * @param child the child view to manipulate * @param dependency the dependent view that changed * @return true if the Behavior changed the child view's size or position, false otherwise */public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) { return false;}
注意官方的注释,如果改变了child的位置、大小,需要返回true
。
onStartNestedScroll
如果想要实现滚动效果,在你想要滚动的条件下,此方法需要返回true
。其返回值会通过lp.acceptNestedScroll(accepted)
存储在LayoutParams
中。其他滚动相关回调都会基于此返回值,true
的时候才会被回调。可以查看前面的CoordinatorLayout
滚动机制部分,都用用下面的代码进行判断:
if (!lp.isNestedScrollAccepted()) { continue; }
onNestedPreScroll
Child滑动前,都会通知Parent,Parent会回调此方法,可以在此方法中做滑动拦截。该方法的会传入内部View移动的dx,dy,如果你需要消耗一定的dx,dy,就通过最后一个参数consumed进行指定,例如我要消耗一半的dy,就可以写consumed[1]=dy/2。
onNestedScroll
Child滑动以后,会通知Parent,回调onNestedScroll()。可以在此方法中做进一步的滚动处理,因为其可以获取到被消耗的和未被消耗的滚动距离。
结语
到这里CoordinatorLayout
的源码解析也算是完结了,虽然只是分析了部分源码,但是也大致清楚了其工作原理。如果有分析得不对的地方还望指正。最后推荐几篇不错的文章:
- 源码看CoordinatorLayout.Behavior原理——亓斌
- 一步一步深入理解CoordinatorLayout——程序亦非猿
- Android NestedScrolling 实战
- Android NestedScrolling机制完全解析 带你玩转嵌套滑动——张鸿洋
- CoordinatorLayout源码解析
- CoordinatorLayout调用原理源码解析
- CoordinatorLayout源码解析之从NestedScrolling说起
- CoordinatorLayout源码解析之初识Behavior
- CoordinatorLayout解析
- CoordinatorLayout源码解析,探索Behavior机制的奥秘
- Android CoordinatorLayout解析
- CoordinatorLayout使用全解析
- coordinatorlayout和behavior解析
- 源码看CoordinatorLayout.Behavior原理
- CoordinatorLayout自定义Behavior&源码分析
- 源码看CoordinatorLayout.Behavior原理
- CoordinatorLayout与Behavior源码分析
- CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout全解析
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- CoordinatorLayout
- 1、HTTP监控工具httpry
- 使用 Visual Studio 进行调试
- VIM学习手记1-从命令模式直接进入单词替换修改
- 【C】获取一个数二进制序列中所有的偶数位和奇数位,分别输出二进制序列
- 表格排序——javascript
- CoordinatorLayout源码解析
- python定位
- 文章标题
- pixhawk新增编译选项、板级配置的方法
- 远程连接Hostinger MySQL数据库 setup remote MySQL Workbench connection to Hostinger MySQL database
- Behavior实现UC浏览器首页动画效果
- 【C】将三个数按从大到小输出
- java中读取中文字符和非中文字符
- pg学习_基本表定义_数据类型