AppBarLayout&CoordinatorLayout&Behavior

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 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

  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 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值