AppBarLayout&CoordinatorLayout&Behavior

标签(空格分隔): android

AppBarLayout

  1. AppBarLayout继承自vertical的Linearlayout
  2. app:expanded="false"(setExpanded) 控制是折叠还是展开
  3. addOnOffsetChangedListener可以监控vertical偏移量
  4. 内部view可以app:layout_scrollFlags控制滚动

    scroll|exitUntilCollapsed 向上滑动以minHeight折叠在顶端,必须设置minHeight属性
    scroll|enterAlways|enterAlwaysCollapsed 快速返回,先以minHeight显示,滚动控件下滑到顶端时,则可继续滑动展开
    scroll|enterAlways|exitUntilCollapsed 快速返回,向上滑动以minHeight折叠在顶端,必须设置minHeight属性
    snap 要嘛全部显示要嘛全不显示

主要源码分析

//检测ScrollInterpolator子view,刷新子view折叠状态以minHeight
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {super.onLayout(changed, l, t, r, b);//无效化滚动范围invalidateScrollRanges();mHaveChildWithInterpolator = false;for (int i = 0, z = getChildCount(); i < z; i++) {final View child = getChildAt(i);final LayoutParams childLp = (LayoutParams) child.getLayoutParams();final Interpolator interpolator = childLp.getScrollInterpolator();if (interpolator != null) {mHaveChildWithInterpolator = true;break;}}updateCollapsible();
}private void updateCollapsible() {boolean haveCollapsibleChild = false;for (int i = 0, z = getChildCount(); i < z; i++) {if (((LayoutParams) getChildAt(i).getLayoutParams()).isCollapsible()) {haveCollapsibleChild = true;break;}}setCollapsibleState(haveCollapsibleChild);
}AppBarLayout中的LayoutParams主要封装了ScrollFlags(app:layout_scrollFlags)等相关//计算总的滚动范围(也是预向上的滚动范围getUpNestedPreScrollRange)
public final int getTotalScrollRange() {if (mTotalScrollRange != INVALID_SCROLL_RANGE) {return mTotalScrollRange;}//顺序遍历,如果第一个子view没有scroll,则直接退出了,后面的设置也将无效int range = 0;for (int i = 0, z = getChildCount(); i < z; i++) {final View child = getChildAt(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();final int childHeight = child.getMeasuredHeight();final int flags = lp.mScrollFlags;//从这里可以看出如果要实现滚动,则子控件必须包含scroll flag(app:layout_scrollFlags="scroll" or LayoutParams#setScrollFlags)if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {// We're set to scroll so add the child's heightrange += childHeight + lp.topMargin + lp.bottomMargin;//如果包含exitUntilCollapsed flag则减去该child view 的最小高度,所以设置了这个flag则滚动到到这个view为最小显示高度止if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {// For a collapsing scroll, we to take the collapsed height into account.// We also break straight away since later views can't scroll beneath// usrange -= ViewCompat.getMinimumHeight(child);break;}} else {// As soon as a view doesn't have the scroll flag, we end the range calculation.// This is because views below can not scroll under a fixed view.break;}}return mTotalScrollRange = Math.max(0, range - getTopInset());
}//预向下的滚动范围
int getDownNestedPreScrollRange() {if (mDownPreScrollRange != INVALID_SCROLL_RANGE) {// If we already have a valid value, return itreturn mDownPreScrollRange;}//倒序遍历,处理是否有快速返回模式。所以设置的效果只能体现在最后一个子view上//eg。第一个child view scroll|enterAlways,第二个child view scroll//向下滑动时快速返回的是第二个child viewint range = 0;for (int i = getChildCount() - 1; i >= 0; i--) {final View child = getChildAt(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();final int childHeight = child.getMeasuredHeight();final int flags = lp.mScrollFlags;//是否是快速返回模式 scroll|enterAlways//FLAG_QUICK_RETURN==SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYSif ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {// First take the margin into accountrange += lp.topMargin + lp.bottomMargin;// The view has the quick return flag combination...//如果还是enterAlwaysCollapsed,则滚动范围加上minHeight,即快速返回时不是全部高度是minHeight//scroll|enterAlways|enterAlwaysCollapsedif ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {// If they're set to enter collapsed, use the minimum heightrange += ViewCompat.getMinimumHeight(child);} else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {// Only enter by the amount of the collapsed height//如果是exitUntilCollapsed,则最小高度不能够被折叠//scroll|enterAlways|exitUntilCollapsedrange += childHeight - ViewCompat.getMinimumHeight(child);} else {// Else use the full heightrange += childHeight;}} else if (range > 0) {// If we've hit an non-quick return scrollable view, and we've already hit a// quick return view, return nowbreak;}}return mDownPreScrollRange = Math.max(0, range);}//向下滚动范围
int getDownNestedScrollRange() {if (mDownScrollRange != INVALID_SCROLL_RANGE) {// If we already have a valid value, return itreturn mDownScrollRange;}int range = 0;for (int i = 0, z = getChildCount(); i < z; i++) {final View child = getChildAt(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();int childHeight = child.getMeasuredHeight();childHeight += lp.topMargin + lp.bottomMargin;final int flags = lp.mScrollFlags;//检查是否有scroll标记if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {// We're set to scroll so add the child's heightrange += childHeight;//是否有exitUntilCollapsed标记,减去minHegihtif ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {// For a collapsing exit scroll, we to take the collapsed height into account.// We also break the range straight away since later views can't scroll// beneath usrange -= ViewCompat.getMinimumHeight(child) + getTopInset();break;}} else {// As soon as a view doesn't have the scroll flag, we end the range calculation.// This is because views below can not scroll under a fixed view.break;}}return mDownScrollRange = Math.max(0, range);
}

CoordinatorLayout&Behavior

  1. CoordinatorLayout继承自ViewGroup
  2. LayoutParams给child view提供了一系列属性,app:layout_anchor设置锚点,app:layout_anchorGravity设置处于锚点什么位置,app:layout_behavior="@string/appbar_scrolling_view_behavior"指定behavior

主要源码分析
首先实现Behavior必须保证存在Context,AttributeSet两个参数的构造方法,内部利用反射获取的Behavior。指定Behavior有2种方式
1. 利用xml中app:layout_behavior指定,取值为Behavior的全包名
2. 利用@CoordinatorLayout.DefaultBehavior注解指定

static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {Context.class,AttributeSet.class
};
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);//获取xml配置behavior
mBehaviorResolved = a.hasValue(R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {mBehavior = parseBehavior(context, attrs, a.getString(R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
//反射指定
LayoutParams getResolvedLayoutParams(View child) {final LayoutParams result = (LayoutParams) child.getLayoutParams();if (!result.mBehaviorResolved) {Class<?> childClass = child.getClass();DefaultBehavior defaultBehavior = null;while (childClass != null &&(defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {childClass = childClass.getSuperclass();}if (defaultBehavior != null) {try {result.setBehavior(defaultBehavior.value().newInstance());} catch (Exception e) {Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +" could not be instantiated. Did you forget a default constructor?", e);}}result.mBehaviorResolved = true;}return result;
}

绑定解绑Behavior到LayoutParams

//CoordinatorLayout.LayoutParams实例化完成后回调,setBehavior将引发回调
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
}//从CoordinatorLayout子view解绑时调用
public void onDetachedFromLayoutParams() {
}

CoordinatorLayouot初始化LayoutParams中回调绑定

if (mBehaviorResolved) {mBehavior = parseBehavior(context, attrs, a.getString(R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
a.recycle();if (mBehavior != null) {// If we have a Behavior, dispatch that it has been attachedmBehavior.onAttachedToLayoutParams(this);
}

指定新的Behavior,解绑之前的并绑定新的

public void setBehavior(@Nullable Behavior behavior) {if (mBehavior != behavior) {if (mBehavior != null) {// First detach any old behaviormBehavior.onDetachedFromLayoutParams();}mBehavior = behavior;mBehaviorTag = null;mBehaviorResolved = true;if (behavior != null) {// Now dispatch that the Behavior has been attachedbehavior.onAttachedToLayoutParams(this);}}
}

绑定后先确定依赖关系

//返回true确定依赖关系(可能调用多次) child 为behavior dependency为被依赖的view
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {return false;
}//依赖view发生变化时回调
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {return false;
}//依赖view移除时回调
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}

在onMeasu中执行了prepareChildren方法确定了依赖关系,内部执行了LayoutParams的dependsOn,内部引用了Behavior的layoutDependsOn

private void prepareChildren() {//保存排序后的子视图mDependencySortedChildren.clear();//有向无环图排序mChildDag.clear();for (int i = 0, count = getChildCount(); i < count; i++) {final View view = getChildAt(i);//寻找behavior并返回LayoutParamsfinal LayoutParams lp = getResolvedLayoutParams(view);//寻找锚点viewlp.findAnchorView(this, view);//保存child viewmChildDag.addNode(view);// Now iterate again over the other children, adding any dependencies to the graph//添加依赖关系图,方便随后布局viewfor (int j = 0; j < count; j++) {if (j == i) {continue;}final View other = getChildAt(j);final LayoutParams otherLp = getResolvedLayoutParams(other);//返回true,确定依赖关系if (otherLp.dependsOn(this, other, view)) {if (!mChildDag.contains(other)) {// Make sure that the other node is addedmChildDag.addNode(other);}// Now add the dependency to the graphmChildDag.addEdge(view, other);}}}// Finally add the sorted graph list to our listmDependencySortedChildren.addAll(mChildDag.getSortedList());// We also need to reverse the result since we want the start of the list to contain// Views which have no dependencies, then dependent views after thatCollections.reverse(mDependencySortedChildren);
}View findAnchorView(CoordinatorLayout parent, View forChild) {//没有指定直接返回if (mAnchorId == View.NO_ID) {mAnchorView = mAnchorDirectChild = null;return null;}//如果锚点view为空或者通过setAnchorId更改了锚点view则if (mAnchorView == null || !verifyAnchorView(forChild, parent)) {//赋值锚点viewresolveAnchorView(forChild, parent);}return mAnchorView;
}//检验锚点view是否还有效
private boolean verifyAnchorView(View forChild, CoordinatorLayout parent) {if (mAnchorView.getId() != mAnchorId) {return false;}View directChild = mAnchorView;for (ViewParent p = mAnchorView.getParent();p != parent;p = p.getParent()) {if (p == null || p == forChild) {mAnchorView = mAnchorDirectChild = null;return false;}if (p instanceof View) {directChild = (View) p;}}mAnchorDirectChild = directChild;return true;
}//设置锚点view和锚点view CoordinatorLayout的直接子view
private void resolveAnchorView(final View forChild, final CoordinatorLayout parent) {mAnchorView = parent.findViewById(mAnchorId);if (mAnchorView != null) {//锚点view不能为CoordinatorLayoutif (mAnchorView == parent) {if (parent.isInEditMode()) {mAnchorView = mAnchorDirectChild = null;return;}throw new IllegalStateException("View can not be anchored to the the parent CoordinatorLayout");}//如果锚点view是CoordinatorLayout的直接子view,直接结束。如果不是直接子view,则循环找到为止,同时从代码中可以看出锚点view不能是一个层次结构的内部view,如:<LinearLayoutandroid:id="@+id/ll"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_gravity="bottom"app:layout_anchor="@+id/bind_ck"android:orientation="horizontal"><Buttonandroid:id="@+id/bind_ck"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="绑定ck并解绑tv"/></LinearLayout>View directChild = mAnchorView;for (ViewParent p = mAnchorView.getParent();p != parent && p != null;p = p.getParent()) {if (p == forChild) {if (parent.isInEditMode()) {mAnchorView = mAnchorDirectChild = null;return;}throw new IllegalStateException("Anchor must not be a descendant of the anchored view");}if (p instanceof View) {directChild = (View) p;}}//锚点view CoordinatorLayout的直接子viewmAnchorDirectChild = directChild;} else {if (parent.isInEditMode()) {mAnchorView = mAnchorDirectChild = null;return;}throw new IllegalStateException("Could not find CoordinatorLayout descendant view"+ " with id " + parent.getResources().getResourceName(mAnchorId)+ " to anchor view " + forChild);}
}//回调Behavior的layoutDependsOn
boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {//dependency == mAnchorDirectChild永远为false?return dependency == mAnchorDirectChild|| shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))|| (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}

确定依赖关系后则是安排布局

//CoordinatorLayout onLayout中回调,自己控制度量,false使用默认的,true自定义
public boolean onMeasureChild(CoordinatorLayout parent, V child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed) {return false;
}
//child指Behavior控件,返回true则自定义Behavior,false使用默认的。CoordinatorLayout onLayout中回调,可以代理改变behavior,如修改文本
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {return false;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {final int layoutDirection = ViewCompat.getLayoutDirection(this);final int childCount = mDependencySortedChildren.size();for (int i = 0; i < childCount; i++) {final View child = mDependencySortedChildren.get(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();final Behavior behavior = lp.getBehavior();//回调Behavior的onLayoutChildif (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {onLayoutChild(child, layoutDirection);}}
}

联动事件处理。CoordinatorLayout实现了NestedScrollingParent,在对应的回调方法中在传递给Behavior的同名方法处理

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {boolean handled = false;final int childCount = getChildCount();for (int i = 0; i < childCount; i++) {final View view = getChildAt(i);final LayoutParams lp = (LayoutParams) view.getLayoutParams();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;
}

同时事件拦截和处理也会传递给Behavior处理,Behavior先于其它子view获取到事件

ViewOffsetBehavior主要是处理移动

//先使用默认方式布局完成,后面通过setTopAndBottomOffset,setLeftAndRightOffset就可以实现view移动
@Override
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {// First let lay the child outlayoutChild(parent, child, layoutDirection);if (mViewOffsetHelper == null) {mViewOffsetHelper = new ViewOffsetHelper(child);}mViewOffsetHelper.onViewLayout();if (mTempTopBottomOffset != 0) {mViewOffsetHelper.setTopAndBottomOffset(mTempTopBottomOffset);mTempTopBottomOffset = 0;}if (mTempLeftRightOffset != 0) {mViewOffsetHelper.setLeftAndRightOffset(mTempLeftRightOffset);mTempLeftRightOffset = 0;}return true;
}

HeaderBehavior继承自ViewOffsetBehavior,增加了对事件的处理。

@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {if (mTouchSlop < 0) {mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();}final int action = ev.getAction();// 如果已经开始了拖动则直接拦截if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {return true;}switch (MotionEventCompat.getActionMasked(ev)) {//down先不拦截事件case MotionEvent.ACTION_DOWN: {mIsBeingDragged = false;final int x = (int) ev.getX();final int y = (int) ev.getY();//是否能拖动并且处于view内if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {mLastMotionY = y;mActivePointerId = ev.getPointerId(0);ensureVelocityTracker();}break;}//move如果达到要求,拦截case MotionEvent.ACTION_MOVE: {//没有有效pointid直接结束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;
}

现在看具体的AppBarLayout#Behavior,CoordinatorLayout下的ns控件发起ns事件,CoordinatorLayout进行对应的ns事件回调。

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,View directTargetChild, View target, int nestedScrollAxes) {//如果是竖直方向的滚动,并且AppBarLayout内有可滚动控件(前面的滚动标记),并且滚动区域足够大,如果不够大,不协同处理。这就是为什么cl+apb,如果cl内没有ns控件,或者ns不能有效滚动时apb 不能折叠的原因final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0&& child.hasScrollableChildren()&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();if (started && mOffsetAnimator != null) {// Cancel any offset animationmOffsetAnimator.cancel();}// A new nested scroll has started so clear out the previous refmLastNestedScrollingChildRef = null;return started;
}

后面都是根据计算出的范围进行处理,eg

@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 downmin = -child.getTotalScrollRange();max = min + child.getDownNestedPreScrollRange();} else {// We're scrolling upmin = -child.getUpNestedPreScrollRange();max = 0;}consumed[1] = scroll(coordinatorLayout, child, dy, min, max);}
}

最后简单看下ScrollingViewBehavior,这就是为什么需要指定app:layout_behavior="@string/appbar_scrolling_view_behavior"的缘故。定位。

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

最新文章

  1. SpringBoot 缓存之 @Cacheable 详细介绍
  2. 关于集体生活和个人生活的思考
  3. spring-security问题记录
  4. hdu 4324 Triangle LOVE
  5. 12 [虚拟化] 进程抽象;fork,execve,exit
  6. github代码_GitHub代码空间如何提高生产力和降低障碍
  7. SQL 2000 中如何 纵表变横表
  8. 我的 WinClock 项目系列之一 (概述)
  9. android c vector用法,c – 使用std:vector作为低级缓冲区
  10. 2-set 1823: [JSOI2010]满汉全席
  11. 地图学属于计算机类吗,地图学
  12. 流量红利不再,餐企做私域流量要趁早
  13. SSL 3.0曝出Poodle漏洞的解决方案
  14. ClickHouse中文官方文档
  15. 苯四乙酸 cas1820793-31-4 齐岳中间体|单体材料
  16. java8的时期和时间
  17. c语言中将分钟的时间转换为小时和分钟并存的形式
  18. 语文文学常识。不转可惜!谁整理的?太佩服啦!
  19. pdf怎么拆分成一页一页的?办公常备工具说明
  20. 我的Java学习之路(第七天)------数组

热门文章

  1. 9.SpringCloud Gateway网关
  2. 应用特征值矩阵对角方法
  3. 奇异值分解推导详解以及几何意义
  4. 使用Matlab将多个图形Figure文件合并
  5. 大白话讲调度:非支配遗传算法与柔性作业车间调度
  6. 下列各命令中可以在计算机屏幕上,下列各命令中,可以在计算机屏幕上放映演示文稿的是()。...
  7. thinkpad选择启动项_联系ThinkPad笔记本怎么设置第一启动项
  8. 打开.sql文件的方法
  9. 手机网页端查看百度等搜索引擎网页快照的方法
  10. 基于MATLAB的指纹识别系统仿真设计,基于Matlab的指纹识别系统的研究与实现