标签(空格分隔): android
AppBarLayout
app:expanded="false"(setExpanded)
控制是折叠还是展开addOnOffsetChangedListener
可以监控vertical偏移量内部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 height
range += 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
// us
range -= 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 it
return mDownPreScrollRange;
}
//倒序遍历,处理是否有快速返回模式。所以设置的效果只能体现在最后一个子view上
//eg。第一个child view scroll|enterAlways,第二个child view scroll
//向下滑动时快速返回的是第二个child view
int 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_ALWAYS
if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
// First take the margin into account
range += lp.topMargin + lp.bottomMargin;
// The view has the quick return flag combination...
//如果还是enterAlwaysCollapsed,则滚动范围加上minHeight,即快速返回时不是全部高度是minHeight
//scroll|enterAlways|enterAlwaysCollapsed
if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
// If they're set to enter collapsed, use the minimum height
range += 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|exitUntilCollapsed
range += childHeight - ViewCompat.getMinimumHeight(child);
} else {
// Else use the full height
range += 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 now
break;
}
}
return mDownPreScrollRange = Math.max(0, range);
}
//向下滚动范围
int getDownNestedScrollRange() {
if (mDownScrollRange != INVALID_SCROLL_RANGE) {
// If we already have a valid value, return it
return 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 height
range += childHeight;
//是否有exitUntilCollapsed标记,减去minHegiht
if ((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 us
range -= 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
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 attached
mBehavior.onAttachedToLayoutParams(this);
}
指定新的Behavior,解绑之前的并绑定新的
public void setBehavior(@Nullable Behavior behavior) {
if (mBehavior != behavior) {
if (mBehavior != null) {
// First detach any old behavior
mBehavior.onDetachedFromLayoutParams();
}
mBehavior = behavior;
mBehaviorTag = null;
mBehaviorResolved = true;
if (behavior != null) {
// Now dispatch that the Behavior has been attached
behavior.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并返回LayoutParams
final LayoutParams lp = getResolvedLayoutParams(view);
//寻找锚点view
lp.findAnchorView(this, view);
//保存child view
mChildDag.addNode(view);
// Now iterate again over the other children, adding any dependencies to the graph
//添加依赖关系图,方便随后布局view
for (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 added
mChildDag.addNode(other);
}
// Now add the dependency to the graph
mChildDag.addEdge(view, other);
}
}
}
// Finally add the sorted graph list to our list
mDependencySortedChildren.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 that
Collections.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)) {
//赋值锚点view
resolveAnchorView(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不能为CoordinatorLayout
if (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,如:
<LinearLayout
android: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">
<Button
android: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的直接子view
mAnchorDirectChild = 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的onLayoutChild
if (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 out
layoutChild(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 animation
mOffsetAnimator.cancel();
}
// A new nested scroll has started so clear out the previous ref
mLastNestedScrollingChildRef = 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 down
min = -child.getTotalScrollRange();
max = min + child.getDownNestedPreScrollRange();
} else {
// We're scrolling up
min = -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 AppBarLayouts
return dependency instanceof AppBarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
View dependency) {
offsetChildAsNeeded(parent, child, dependency);
return false;
}