CoordinatorLayout 使用及源码解析

简介

CoordinatorLayout 实现了 NestedScrollingParent 接口,是一个容器。作为一个 “super-powered FrameLayout”,主要有以下两个作用:

  1. 作为顶层布局;
  2. 作为协调子 View 之间交互的容器。

使用 CoordinatorLayout 需要在 build.gradle 加入:

 implementation 'com.android.support:design:XXXXXX'

具体使用

1. CoordinatorLayout 与 FloatingActionButton

 首先看看FloatingActionButton单独使用时的情况,布局如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout     
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"     
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:onClick="onClick"
        android:layout_marginRight="10dp"
        android:layout_marginBottom="10dp"/>
</RelativeLayout>

点击这个FloatingActionButton后,将弹出一个 Snackbar:

public void onClick(View v) {
    switch (v.getId()) {
        case R.id.fab:
            Snackbar.make(findViewById(R.id.contentView), "Snackbar", Snackbar.LENGTH_SHORT).show();
            break;
        ...
    }
}

效果如下:

                                                              

此时FloatingActionButton会被 Snackbar 遮挡,此时就需要 CoordinatorLayout登场了。

调整后的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:id="@+id/anchorView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"/>
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_anchor="@id/anchorView"
        app:layout_anchorGravity="bottom|right"
        android:onClick="onClick"
        android:layout_marginRight="10dp"
        android:layout_marginBottom="10dp"/>
</android.support.design.widget.CoordinatorLayout>

CoordinatorLayout 提供了两个属性用来设置 FloatingActionButton 的位置:

  • layout_anchor:设置 FAB 的锚点,我们熟悉的 PopupWindow 也有类似概念。
  • layout_anchorGravity:设置相对锚点的位置,如bottom|right表示 FloatingActionButton 位于锚点的右下角。

再次运行程序,此时Snackbar 显示和隐藏的时候,CoordinatorLayout 会动态调整 FAB 的位置,效果图如下:

                                                       

2. CoordinatorLayout 与 AppBarLayout

AppBarLayout 是一个垂直布局的 LinearLayout,它主要是为了实现 “Material Design”风格的标题栏的特性,比如动态联动:

                                                       

不使用 CoordinatorLayout,实现这个效果的方案有两种:

  1. 自己处理触摸事件的分发,来改变标题栏的位置。
  2. 使用 support.v4 引入的 NestedScrolling 机制。

关于上面两种实现方式,可以参考我的另外一篇文章:NestedScrolling机制原理解析。而CoordinatorLayout 实现了 NestedScrollingParent 接口,所以我们配合一个实现了 NestedScrollingChild 接口的 View 就可以轻松的实现以上效果。

AppBarLayout必须作为CoordinatorLayout的直接子View,否则它的大部分功能将不会生效,如layout_scrollFlags等。

2.1 layout_scrollFlags属性

AppBarLayout必须作为CoordinatorLayout的直接子View,否则它的大部分功能将不会生效,包括layout_scrollFlags

简单起见,我们使用 AppBarLayout 包裹 TextView 来实现上面的效果:

<android.support.design.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"           
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="标题"
            android:textSize="20sp"
            android:gravity="center"
            android:paddingTop="10dp"
            android:paddingBottom="10dp"
            android:textColor="@android:color/white"
            android:background="@color/colorPrimary"
            app:layout_scrollFlags="scroll"/>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <TextView
            android:id="@+id/tv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="30sp"
            android:gravity="center_horizontal"/>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

 

注意layout_behavior这个属性:

app:layout_behavior="@string/appbar_scrolling_view_behavior"

这个值是系统提供的,表示使用android.support.design.widget.AppBarLayout.ScrollingViewBehavior来处理 NestedScrollView 与 AppBarLayout 的关系,关于Behavior的概念这里暂不介绍,后面会讲解。

再回过头来看例子中的 layout_scrollFlags属性:

scroll:当上划的时候,先整体向上滚动,直到 AppBarLayout 完全隐藏,再开始滚动 Scrolling View;当下拉的时候,直到 Scrolling View 顶部完全出现后,再开始滚动 AppBarLayout 到完全显示。

                                                 

enterAlways:需要与scroll一起使用(scroll|enterAlways),具体作用与 scroll 类似, 只不过下划的时候先显示 AppBarLayout 到完全,再滚动 Scrolling View。

                                                 

enterAlwaysCollapsed:需要和scroll 和 enterAlways 一起使用(scroll|enterAlways|enterAlwaysCollapsed),和 enterAlways 不一样的是,向下滚动时不会显示 AppBarLayout 到完全再滚动 Scrolling View,而是先滚动 AppBarLayout 到最小高度,再滚动 Scrolling View,最后再滚动 AppBarLayout 到完全显示。

注意:需要定义 View 的最小高度(minHeight)才有效果:

android:minHeight="10dp"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"

                                               

exitUntilCollapsed:需要和scroll 一起使用,与enterAlwaysCollapsed不同的是,exitUntilCollapsed定义了 AppBarLayout 消失的规则。当上划时,AppBarLayout 向上滚动退出直至最小高度(minHeight),然后 Scrolling View 开始滚动,也就是说AppBarLayout 不会完全退出屏幕。当向下滚动的时候,直到 Scrolling View 顶部完全出现后,才会开始滚动 AppBarLayout 到完全显示。

                                               

2.2 CollapsingToolbarLayout

CollapsingToolbarLayout继承自FrameLayout, 被设计作为AppBarLayout的子View,并作为Toolbar的包装器。主要实现以下功能 
- Collapsing title(可以折叠 的 标题 ) 
- Content scrim(内容装饰),当我们滑动的位置 到达一定阀值的时候,内容 装饰将会被显示或者隐藏 
- Status bar scrim(状态栏布) 
- Parallax scrolling children,滑动的时候孩子呈现视觉特差效果 
- Pinned position children,固定位置的 孩子,它是用来实现 Toolbar 的折叠效果,一般它的直接子 View 是 Toolbar,当然也可以是其它类型的 View。

下面我们一起来看一下几个常量

常量解释说明
int COLLAPSE_MODE_OFFThe view will act as normal with no collapsing behavior.(这个 View将会 呈现正常的结果,不会表现出折叠效果)
int COLLAPSE_MODE_PARALLAXThe view will scroll in a parallax fashion. See setParallaxMultiplier(float) to change the multiplier used.(在滑动的时候这个View 会呈现 出 视觉特差效果 )
int COLLAPSE_MODE_PINThe view will pin in place until it reaches the bottom of the CollapsingToolbarLayout.(当这个View到达 CollapsingToolbarLayout的底部的时候,这个View 将会被放置,即代替整个CollapsingToolbarLayout)

我们有两种方法可以设置这个常量,

方法一:在代码中直接设置

setCollapseMode(int collapseMode)

方法 二:在布局文件中使用自定义属性

app:layout_collapseMode="pin"

如果你不使用 Toolbar,有些效果没法直接实现,比如下图的“My files”文字在折叠和展开的时候,有一个过渡效果:

                                                 

也就意味着 CollapsingToolbarLayout 设置 title 的相关方法无效,比如:setTitle、setCollapsedTitleTextColor、setExpandedTitleGravity等,更多方法可以自行查阅 API 。

另外,layout_scrollFlags 中的 exitUntilCollapsed 属性也会失效,即使你设置了 minHeight,所以官方也说明了CollapsingToolbarLayout 是为了配合 Toolbar 而设计:

2.2.1 CollapsingToolbarLayout的几种效果:

  • CollapsingToolbarLayout & enterAlwaysCollapsed
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout     
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="150dp">
        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="@color/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed">
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                app:layout_collapseMode="pin"
                android:minHeight="10dp"
                android:background="@color/colorPrimary"/>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <TextView
            android:id="@+id/tv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="30sp"
            android:gravity="center_horizontal"/>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

                                             

  • CollapsingToolbarLayout & exitUntilCollapsed

修改下 CollapsingToolbarLayout 的 layout_scrollFlags:

app:layout_scrollFlags="scroll|exitUntilCollapsed"

                                              

2.2.2 layout_collapseMode属性

上述例子中设置了属性app:layout_collapseMode="pin" 确保 CollapsingToolbarLayout 折叠完成之前,Toolbar 一直固定在顶部不动。除了pin 之外还可以使用 parallax,视差的意思就是:移动过程中两个 View 的位置产生了一定的视觉差异。

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="150dp">
        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="@color/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:src="@drawable/bg"
                android:scaleType="centerCrop"
                app:layout_collapseParallaxMultiplier="0.9"
                app:layout_collapseMode="parallax"/>
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                app:layout_collapseMode="pin"/>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <TextView
            android:id="@+id/tv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="30sp"
            android:gravity="center_horizontal"/>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

                                               

注意这个属性:

app:layout_collapseParallaxMultiplier="0.9"

使用的视差因子值为0.9,那么试着设置视察因子为0.1:

app:layout_collapseParallaxMultiplier="0.9"

效果图:

                                              

可以看到0.9和0.1背景图片移动的距离是不一样的。

2.3 CoordinatorLayout中的Behavior

在 NestedScrolling机制原理解析 一文中已经介绍过:Scrolling child是使用 NestedScrollingChildHelper  作为代理来完成和scrolling parent的交互的,具体原理本文不再一一阐述。

我们已知的NestedScrollingParent接口包含以下方法:

  • onStartNestedScroll
  • onNestedScrollAccepted
  • onNestedPreScroll
  • onNestedScroll
  • onNestedPreFling
  • onNestedFling
  • onStopNestedScroll

在 Behavior 接口中同样里面也包含这些方法,与 NestedScrollingParent 方法几乎一一对应。在 CoordinatorLayout 里面。NestedScrollingParent 接口的方法的具体实现逻辑都会交给 Behavior 对应的方法去处理,我们可以从CoordinatorLayout源码中找到答案:

  • onStartNestedScroll 方法

遍历所有的孩子 ,如果可见性是 GONE,跳过。如果可见性不是 GONE,通过 layoutParams 拿到 Behavior,判断 behavior 是否为空,不为空,调用 behavior 的对应方法 onStartNestedScroll 和 acceptNestedScroll 方法。

    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;
    }
  • onNestedScrollAccepted 方法

首先调用 mNestedScrollingParentHelper 的相关方法,而后遍历孩子,通过 layoutParams 判断是否要处理滑动事件,处理的 话,回调 Behavior 的相关方法,不处理的话,跳过当前 View。

    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        mNestedScrollingDirectChild = child;
        mNestedScrollingTarget = target;
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes);
            }
        }
    }
  • onNestedPreScroll 方法

我们知道 onNestedPreScroll 是在 Scrolling child 滑动之前回调的,提供机会给 Scrooling Parent 先于 child 进行滑动的。

在 CoordinatorLayout 里面,它的处理流程是这样的。 遍历所有的孩子,判断可见性是否为 GONE,如果是 ,跳过当前 子 View,通过 LayoutParams 判断是否处理滑动事件,不处理滑动 事件,跳过,拿到 Behavior,判断 Behavior 是否为空,不过空,回调 Behavior 的 onNestedPreScroll 方法。

    public 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++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            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[0] = xConsumed;
        consumed[1] = yConsumed;
        if (accepted) {
            dispatchOnDependentViewChanged(true);
        }
    }
  • onNestedScroll 方法

在 Scrolling Child 滑动之后,提供机会给 Scrolling Parent 滑动,事件的处理逻辑参见上文。

@Override
public 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++) {
        final View view = getChildAt(i);
        if (view.getVisibility() == GONE) {
            // If the child is GONE, skip...
            continue;
        }

        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
          // 如果之前没有处理滑动事件,直接返回,不调用 onStopNestedScroll 方法
        if (!lp.isNestedScrollAccepted()) {
            continue;
        }

        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
             // 调用 behavior 的相应方法
            viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                    dxUnconsumed, dyUnconsumed);
            accepted = true;
        }
    }

    if (accepted) {
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }
}
  • onNestedPreFling 方法
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        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();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
            }
        }
        return handled;
    }
  • onNestedFling 方法
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        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();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,
                        consumed);
            }
        }
        if (handled) {
            dispatchOnDependentViewChanged(true);
        }
        return handled;
    }
  • onStopNestedScroll 方法
    public void onStopNestedScroll(View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onStopNestedScroll(this, view, target);
            }
            lp.resetNestedScroll();
            lp.resetChangedAfterNestedScroll();
        }
        mNestedScrollingDirectChild = null;
        mNestedScrollingTarget = null;
    }

Behavior 相比 NestedScrollingParent 独有的方法

  • layoutDependsOn方法

默认返回false, 如果返回 true,则当 dependency 改变的 时候,将会回调 onDependentViewChanged 方法。比如,当使用依赖于 AppBarLayout 的 ScrollingViewBehavior 时,它会重写方法:

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

默认返回false, 与 layoutDependsOn 息息相关,当 layoutDependsOn 返回true的时候,才会回调这个方法。

那么 onDependentViewChanged 是如何监听得到 View 变化和移除的?其实是在 CoordinatorLayout 的 onAttachedToWindow 方法里面,他会为 ViewTreeObserver 视图树添加 OnPreDrawListener 监听。

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        resetTouchBehaviors();
        if (mNeedsPreDrawListener) {
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
        mIsAttachedToWindow = true;
    }
    class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            dispatchOnDependentViewChanged(false);
            return true;
        }
    }

在 OnPreDrawListener 监听里面会调用 dispatchOnDependentViewChanged 方法,在该方法里面会根据 View的状态回调 onDependentViewChanged 方法:

void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
        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();
            // Check child views before for anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }
            // Did it change? if not continue
            final Rect oldRect = mTempRect1;
            final Rect newRect = mTempRect2;
            getLastChildRect(child, oldRect);
            getChildRect(child, true, newRect);
            if (oldRect.equals(newRect)) {
                continue;
            }
            recordLastChildRect(child, newRect);
            // 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();
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
                        // If this is not from a nested scroll and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }
                    final boolean handled = b.onDependentViewChanged(this, checkChild, child);
                    if (fromNestedScroll) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
    }

我们知道当 View 被销毁的时候,会回调 onDetachedFromWindow 这个方法,因此相对在这个方法里面移除 View 视图树的 PreDrawListener 监听:

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        resetTouchBehaviors();
        if (mNeedsPreDrawListener && mOnPreDrawListener != null) {
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.removeOnPreDrawListener(mOnPreDrawListener);
        }
        if (mNestedScrollingTarget != null) {
            onStopNestedScroll(mNestedScrollingTarget);
        }
        mIsAttachedToWindow = false;
    }

回过头来看看 CoordinatorLayout 的 onMeasure 和 onLayout 过程

  • onMeasure方法

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //处理 child 的一些 相关属性 ,比如 Behavior等
        prepareChildren();
        //如果有依赖的话,添加 OnPreDrawListener 监听,没有的话,移除 OnPreDrawListener 监听
        ensurePreDrawListener();
        //省略了处理padding值的部分
        ...

        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            int keylineWidthUsed = 0;
            if (lp.keyline >= 0 && widthMode != MeasureSpec.UNSPECIFIED) {
                final int keylinePos = getKeyline(lp.keyline);
                final int keylineGravity = GravityCompat.getAbsoluteGravity(
                        resolveKeylineGravity(lp.gravity), layoutDirection)
                        & Gravity.HORIZONTAL_GRAVITY_MASK;
                if ((keylineGravity == Gravity.LEFT && !isRtl)
                        || (keylineGravity == Gravity.RIGHT && isRtl)) {
                    keylineWidthUsed = Math.max(0, widthSize - paddingRight - keylinePos);
                } else if ((keylineGravity == Gravity.RIGHT && !isRtl)
                        || (keylineGravity == Gravity.LEFT && isRtl)) {
                    keylineWidthUsed = Math.max(0, keylinePos - paddingLeft);
                }
            }
            final Behavior b = lp.getBehavior();
            // 回调 Behavior 的 onMeasureChild 方法
            if (b == null || !b.onMeasureChild(this, child, widthMeasureSpec, keylineWidthUsed,
                    heightMeasureSpec, 0)) {
                onMeasureChild(child, widthMeasureSpec, keylineWidthUsed,
                        heightMeasureSpec, 0);
            }
            widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                    lp.leftMargin + lp.rightMargin);
            heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                    lp.topMargin + lp.bottomMargin);
            childState = ViewCompat.combineMeasuredStates(childState,
                    ViewCompat.getMeasuredState(child));
        }
        final int width = ViewCompat.resolveSizeAndState(widthUsed, widthMeasureSpec,
                childState & ViewCompat.MEASURED_STATE_MASK);
        final int height = ViewCompat.resolveSizeAndState(heightUsed, heightMeasureSpec,
                childState << ViewCompat.MEASURED_HEIGHT_STATE_SHIFT);
        setMeasuredDimension(width, height);
    }

进入 prepareChildren 方法里面,可以发现它对 CoordinatorLayout 里面的子 View 进行了排序,排序的结果是 最后被依赖的 View 会排在最前面。举个例子 A 依赖于 B,那么 B会排在前面,A 会排在 B 的 后面。这样的排序结果是合理的,因为 A 既然依赖于 B,那么 B 肯定要有限 measure。


    private void prepareChildren() {
        final int childCount = getChildCount();
        boolean resortRequired = mDependencySortedChildren.size() != childCount;
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = getResolvedLayoutParams(child);
            if (!resortRequired && lp.isDirty(this, child)) {
                resortRequired = true;
            }
            lp.findAnchorView(this, child);
        }
        if (resortRequired) {
            mDependencySortedChildren.clear();
            for (int i = 0; i < childCount; i++) {
                mDependencySortedChildren.add(getChildAt(i));
            }
            Collections.sort(mDependencySortedChildren, mLayoutDependencyComparator);
        }
    }

接下来 我们进入 ensurePreDrawListener 方法里面,看看里面到底做了什么

    /**
     * Add or remove the pre-draw listener as necessary.
     */
    void ensurePreDrawListener() {
        boolean hasDependencies = false;
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (hasDependencies(child)) {
                hasDependencies = true;
                break;
            }
        }
        if (hasDependencies != mNeedsPreDrawListener) {
            if (hasDependencies) {
                addPreDrawListener();
            } else {
                removePreDrawListener();
            }
        }
    }

其实就是遍历所有子view,判断是否有存在依赖的一个,如果有,添加onPreDrawListener,如果没有,移除onPreDrawListener。

  • onLayout 方法
    @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();
            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }
        }
    }

layout 过程相对比较简单,遍历所有孩子,如果可见性为 GONE ,跳过该孩子的 Layout。接着通过 LayoutParams 拿到 Behavior,如果 Behavior 为空或者 Behavior 没有处理自己的 layout 过程,调用 onLayoutChild 方法 去处理子View的位置摆放;如果 Behavior 有处理自己的 layout 过程,交给 Behavior 去处理 。

再看看CoordinatorLayout触摸事件传递,CoordinatorLayout 并不会直接处理事件,而是会尽可能地交给子 View 的Behavior 进行处理。onInterceptTouchEvent 和 onToucheEvent 这两个方法都会调用 performIntercept 来处理事件。


    /**
     * Populate a list with the current child views, sorted such that the topmost views
     * in z-order are at the front of the list. Useful for hit testing and event dispatch.
     */
    private void getTopSortedChildren(List<View> out) {
        out.clear();
        final boolean useCustomOrder = isChildrenDrawingOrderEnabled();
        final int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final int childIndex = useCustomOrder ? getChildDrawingOrder(childCount, i) : i;
            final View child = getChildAt(childIndex);
            out.add(child);
        }
        if (TOP_SORTED_CHILDREN_COMPARATOR != null) {
            Collections.sort(out, TOP_SORTED_CHILDREN_COMPARATOR);
        }
    }
    private boolean performIntercept(MotionEvent ev) {
        boolean intercepted = false;
        boolean newBlock = false;
        MotionEvent cancelEvent = null;
        final int action = ev.getActionMasked();
        final List<View> topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);
        // Let topmost child views inspect first
        final int childCount = topmostChildList.size();
        for (int i = 0; i < childCount; i++) {
            final View child = topmostChildList.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
                // Cancel all behaviors beneath the one that intercepted.
                // If the event is "down" then we don't have anything to cancel yet.
                if (b != null) {
                    if (cancelEvent != null) {
                        final long now = SystemClock.uptimeMillis();
                        cancelEvent = MotionEvent.obtain(now, now,
                                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                    }
                    b.onInterceptTouchEvent(this, child, cancelEvent);
                }
                continue;
            }
            if (!intercepted && b != null
                    && (intercepted = b.onInterceptTouchEvent(this, child, ev))) {
                mBehaviorTouchView = child;
            }
            // Don't keep going if we're not allowing interaction below this.
            // Setting newBlock will make sure we cancel the rest of the behaviors.
            final boolean wasBlocking = lp.didBlockInteraction();
            final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
            newBlock = isBlocking && !wasBlocking;
            if (isBlocking && !newBlock) {
                // Stop here since we don't have anything more to cancel - we already did
                // when the behavior first started blocking things below this point.
                break;
            }
        }
        topmostChildList.clear();
        return intercepted;
    }

可以看到首先通过 getTopSortedChildren 方法将child view按照 Z轴上往下排序(在5.0以上,按照z属性来排序,以下,则是按照添加顺序或者自定义的绘制顺序来排列);

遍历排序好的所有 Child,如果之前有Child 的 Behavior 对事件进行了拦截消费,就通过 onInterceptTouchEvent 发送Cancel事件给后续的所有Behavior; 如果之前没有 Child 消费过且当前 Child进行了消费,则记录下该 child。

转载于:https://my.oschina.net/u/3389024/blog/2396236

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值