前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >针对 CoordinatorLayout 及 Behavior 的一次细节较真

针对 CoordinatorLayout 及 Behavior 的一次细节较真

作者头像
Frank909
发布2019-01-14 17:50:18
1.1K0
发布2019-01-14 17:50:18
举报
文章被收录于专栏:Frank909Frank909

我认真不是为了输赢,我就是认真。– 罗永浩

我一直对 Material Design 很感兴趣,每次在官网上阅读它的相关文档时,我总会有更进一步的体会。当然,Material Design 并不是仅仅针对 Android 而言的,它其实是一套普遍性的设计规范。而对于 Android 开发人员而言,我们涉及的往往是它的实现。也就是一个个个性鲜明的类。比如 RecyclerView 、CardView、Palette 等等。并且为了让开发者更轻松地开发出符合 Material Design 设计规范的界面,Google 开发人员直接提供了一个兼容包,它就是 Android Support Design Library。

引用这个包需要在 build.gradle 中添加依赖。

代码语言:javascript
复制
compile 'com.android.support:design:25.0.1'

在这个包中,最核心的一个类就是 CoordinatorLayout。因为其它的类都需要与它进行相关联才能互动。而今天的主题就是讨论这个类的一些细节。

这里写图片描述
这里写图片描述

上图中这种高大上的视觉和交互效果,第一次看的时候我心头就痒痒的,恨不得立马就去实现它。然后,就百度查找相关的博文,但是风格我都不是很喜欢。我不喜欢文章中放一个 xml 布局文件,然后配置一些属性,然后就没有了。

代码语言:javascript
复制
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.frank.supportdesigndemo.ScrollingActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:layout_marginTop="-28dp"
        android:fitsSystemWindows="true"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:src="@drawable/test"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.7"
                />
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay" />

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_scrolling" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|end"
        app:srcCompat="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>

我照着完成了,效果也达到了,但是感觉有些虚。我不得劲,或者说我内心纠结吧。内心千头万绪,上面的布局文件中,除了 Toolbar 我认识外,其它的控件,我一个都不熟悉。

我不喜欢这种感觉。因为我有许许多多的疑惑。

CoordinatorLayout 是什么? CoordinatorLayout 有什么作用? AppBarLayout 是什么? AppBarLayout 有什么作用? ……

接下来的文章篇幅会比较长,大家仔细阅读就好。如果时间不够,可以直接拖动到文章最后总结的那一节。不明白的地方再到文章中间部分阅读相关内容就可以了。但我希望读者还是顺序方式阅读,因为我相信如果你有许多疑惑,我的学习过程也许可以给你一些提示或者启迪。

更多的真相

在编程领域,学习一个陌生的事物,最好的途径可能就是阅读它的官方文档或者是源代码。带着心中的困惑,我前往 Android 官网,直接挑最显眼的 CoordinatorLayout 来进行研究。所以这篇文章我主讲 CoordinatorLayout。

这里写图片描述
这里写图片描述

官网解释 CoordinatorLayout 是一个超级 FrameLayout,然后可以作为一个容器指定与 child 的一些交互规则。

代码语言:javascript
复制
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {}

这是 CoordinatorLayout 的声明。它本质上就是一个 ViewGroup,注意的是它并没有继承自 FrameLayout,然后实现了 NestedScrollingParent 接口,我们先不管这个 NestedScrollingParent,NestedScrolliingParent 在文章后面适当的地方我会给出解释。

官网又说通过给CoordinaotrLayout 中的 child 指定 Behavior,就可以和 child 进行交互,或者是 child 之间互相进行相关的交互。并且自定义 View 时,可以通过 DefaultBehavior 这个注解来指定它关联的 Behavior。这里出现了新的名词:Behavior。于是,中断对 CoordinatorLayout 的跟踪,转到 Behavior 细节上来。

这里写图片描述
这里写图片描述

Behavior 其实是 CoordinatorLayout 中的一个静态内部类,并且是个泛型,接受任何 View 类型。官方文档真是惜字如金,更多的细节需要去阅读代码,也就是要靠猜测。这点很不爽的。好吧,官方文档说 Behavior 是针对 CoordinatorLayout 中 child 的交互插件。记住这个词:插件。插件也就代表如果一个 child 需要某种交互,它就需要加载对应的 Behavior,否则它就是不具备这种交互能力的。而 Behavior 本身是一个抽象类,它的实现类都是为了能够让用户作用在一个 View 上进行拖拽、滑动、快速滑动等手势。如果自己要定制某个交互动作,就需要自己实现一个 Behavior。

但是,对于我们而言,我们要实现一个 Behavior,我们用来干嘛呢?

是的,问问自己吧,我们如果自定义一个 Behavior,我们想干嘛?

前面内容有讲过,CoordinatorLayout 可以定义与它 child 的交互或者是某些 child 之间的交互。

我们先看看 Behavior 的代码细节,代码有精简。

代码语言:javascript
复制
public static abstract class Behavior<V extends View> {

    public Behavior() { }

    public Behavior(Context context, AttributeSet attrs) {}


    public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }

    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {  return false; }

    public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}


    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
            V child, View directTargetChild, View target, int nestedScrollAxes) {
        return false;
    }

    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
            View directTargetChild, View target, int nestedScrollAxes) {
        // Do nothing
    }

    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
        // Do nothing
    }

    public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
            int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        // Do nothing
    }

    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
            int dx, int dy, int[] consumed) {
        // Do nothing
    }

    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
            float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
            float velocityX, float velocityY) {
        return false;
    }
}

一般我们自定义一个 Behavior,目的有两个,一个是根据某些依赖的 View 的位置进行相应的操作。另外一个就是响应 CoordinatorLayout 中某些组件的滑动事件。 我们先看第一种情况。

两个 View 之间的依赖关系

如果一个 View 依赖于另外一个 View。那么它可能需要操作下面 3 个 API:

代码语言:javascript
复制
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }

public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {  return false; }

public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}

确定一个 View 对另外一个 View 是否依赖的时候,是通过 layoutDependsOn() 这个方法。注意参数,child 是要判断的主角,而 dependency 是宾角,如果 return true,表示依赖成立,反之不成立。当然,你可以复写这个方法对 dependency 进行类型判断否则是其它条件判断,然后再决定是否依赖。只有在 layoutDependsOn() 返回为 true 时,后面的 onDependentViewChanged() 和 onDependentViewRemoved() 才会被调用。

当依赖的那个 View 发生变化时,这个变化代码注释有解释,指的是 dependency 的尺寸和位置发生的变化,当有变化时 Behavior 的 onDependentViewChanged() 方法会被调用。如果复写这个方法时,改变了 child 的尺寸和位置参数,则需要返回 true,默认情况是返回 false。

onDependentView() 被调用时一般是指 dependency 被它的 parent 移除,或者是 child 设定了新的 anchor。

有了上面 3 个 API,我们就能应付在 CoordinatorLayout 中一个子 View 对别个一个子 View 的依赖情景了。

可能会有同学不明白,依赖是为何?或者说是何种依赖。为了避免概念过于空洞抽象。下面,我们用一个简单的例子来让大家感受一下,加深理解。

为了演示效果,我首先在屏幕上定义一个能够响应拖动的自定义 View,我叫它 DependencyView 好了。

这里写图片描述
这里写图片描述

它的代码很简单,主要是继承一个 TextView,然后在触摸事件中对自身位置进行位移。

代码语言:javascript
复制
public class DependencyView extends TextView {

    private final int mSlop;
    private float mLastX;
    private float mLastY;

    public DependencyView(Context context) {
        this(context,null);
    }

    public DependencyView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public DependencyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setClickable(true);

        mSlop = ViewConfiguration.getTouchSlop();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
       // return super.onTouchEvent(event);
        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                int deltax = (int) (event.getX() - mLastX);
                int deltay = (int) (event.getY() - mLastY);
                if (Math.abs(deltax) > mSlop || Math.abs(deltay) > mSlop) {
                    ViewCompat.offsetTopAndBottom(this,deltay);
                    ViewCompat.offsetLeftAndRight(this,deltax);
                    mLastX = event.getX();
                    mLastY = event.getY();
                }

                break;

            case MotionEvent.ACTION_UP:
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            default:
                break;

        }

        return true;
    }
}

前置条件已经确定好了,现在我们要向目标 Behavior 出发了。

做一个跟屁虫

实现一个 Behavior,让它支配一个 View 去紧紧跟随所依赖的 View。在这里,我们让依赖方始终显示在被依赖方的正下方,不论被依赖方位置怎么变换,依赖方始终紧紧相随。那么,代码怎么写。

代码语言:javascript
复制
public class MyBehavior extends CoordinatorLayout.Behavior <View>{

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof DependencyView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        int dependBottom = dependency.getBottom();

        child.setY(dependBottom + 50);
        child.setX(dependency.getLeft());

        return true;
    }

}

我们省略了其它代码,只保留了核心的两个方法,大家一看就懂。通过判断 dependency 是否为 DependencyView 类型来决定是否对其进行依赖。然后在 onDependentViewChanged() 方法中获取 dependency 的位置参数来设置 child 的位置参数,从而实现了预期效果。注意的是更改 child 的位置后,要 return true。

下面来验证。我们在布局文件中对一个 ImageView 设置 MyBehavior,然后观察它的现象。

代码语言:javascript
复制
<ImageView
    android:id="@+id/iv_test"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@mipmap/ic_launcher"
    app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>
这里写图片描述
这里写图片描述

当然这种依赖,并非是一对一的关系,可能是一对多。或者是多对多。

我们再修改一个代码,如果 child 是一个 TextView 就让它始终在 dependency 的上方显示,否则在它下方显示。

代码语言:javascript
复制
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

    float x = child.getX();
    float y = child.getY();

    int dependTop= dependency.getTop();
    int dependBottom = dependency.getBottom();

    x = dependency.getX();

    if ( child instanceof TextView ) {
        y = dependTop - child.getHeight() - 20;
    } else {
        y = dependBottom + 50;
    }


    child.setX(x);
    child.setY(y);

    return true;
}

上面代码清晰易懂,我们再在 xml 布局文件中添加一个 TextView。

代码语言:javascript
复制
<ImageView
        android:id="@+id/iv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="for test"
    app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>

效果如下:

这里写图片描述
这里写图片描述

到此,我们算是弄明白了在 Behavior 中针对被依赖的对象尺寸及位置变化时,依赖方应该如何处理的流程了。接着往下的内容就是处理滑动相关了。不过,在这之前先对一个地方进行说明,那就是如何对于一个 View 设置 Behavior。

Behavior 的设置方法

1. 在 xml 属性中进行设置

对应属性是 app:layout_behavior。要设置的是一条字符串,一般是 Behavior 的全限定类名如 com.frank.supportdesigndemo.MyBehavior,当然,在当前目录下你可以用 . 代替如 .MyBehavior

2. 在代码中设置

主要是设置对应 View 的 LayoutParam

代码语言:javascript
复制
CoordinatorLayout.LayoutParams layoutParams = 
    (CoordinatorLayout.LayoutParams) mIvTest.getLayoutParams();

layoutParams.setBehavior(new MyBehavior());

3. 通过注解

自定义 View 时,通过 CoordinatorLayout.DefaultBehavior 这个注解,就可以为该 View 默认绑定一个对应的 Behavior。Android 源码中有现成的例子。

代码语言:javascript
复制
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {}

可以看到 AppBarLayout 被注解绑定了 AppBarLayout.Behavior 这个 Behavior。所以,之后要研究 AppBarLayout 的话也需要研究它的 Behavior。不过,这是后话。

Behavior 对滑动事件的响应。

其实对于这样的行为,我存在过困惑。官方文档的内容太少了,说的是滑动,但是我并不明白是什么滑动。是响应谁的滑动。

我们一般接触到的滑动控件是 ScrollView、ListView 和 RecyclerView。而 CoordinatorLayout 本身能够滑动吗?

滑动相关的代码如下:

代码语言:javascript
复制
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
            V child, View directTargetChild, View target, int nestedScrollAxes) {
        return false;
    }

public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
        View directTargetChild, View target, int nestedScrollAxes) {
    // Do nothing
}

public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
    // Do nothing
}

public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    // Do nothing
}

public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dx, int dy, int[] consumed) {
    // Do nothing
}

public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY, boolean consumed) {
    return false;
}

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY) {
    return false;
}

为了观察滑动这个行为,我在 MyBehavior 中进行编写了一些调试代码。

代码语言:javascript
复制
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,
                           int dxConsumed, int dyConsumed, int dxUnconsumed,
                           int dyUnconsumed) {
    Log.d(TAG,"onNestedScroll:"+dxConsumed+" dy:"+dyConsumed);
    super.onNestedScroll(coordinatorLayout, child, target, dxConsumed,
            dyConsumed, dxUnconsumed, dyUnconsumed);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                                   View directTargetChild, View target, int nestedScrollAxes) {
    Log.d(TAG,"onStartNestedScroll");
    return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
            nestedScrollAxes);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    Log.d(TAG,"onNestedPreScroll  dx:"+dx+" dy:"+dy);
}

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                             float velocityX, float velocityY, boolean consumed) {
    Log.d(TAG,"onNestedFling");
    return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}

然后我就在模拟器上用鼠标在 CoordinatorLayout 上拼命滑动,想制造一些滑动事件出来,看看 MyBehavior 相应的 API 能不能触发,然后观察 Log。

这里写图片描述
这里写图片描述

很遗憾,我无功而返。

认真查阅文档和源码。我将注意力放在 onStartNestedScroll() 方法上了。

代码语言:javascript
复制
/**
 * Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll.
 *
 * <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond
 * to this event and return true to indicate that the CoordinatorLayout should act as
 * a nested scrolling parent for this scroll. Only Behaviors that return true from
 * this method will receive subsequent nested scroll events.</p>
 *
 * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
 *                          associated with
 * @param child the child view of the CoordinatorLayout this Behavior is associated with
 * @param directTargetChild the child view of the CoordinatorLayout that either is or
 *                          contains the target of the nested scroll operation
 * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
 * @param nestedScrollAxes the axes that this nested scroll applies to. See
 *                         {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
 *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL}
 * @return true if the Behavior wishes to accept this nested scroll
 *
 * @see NestedScrollingParent#onStartNestedScroll(View, View, int)
 */
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
        V child, View directTargetChild, View target, int nestedScrollAxes) {
    return false;
}

注释说了,当一个 CoordinatorLayout 的后代企图触发一个 nested scroll 事件时,这个方法被调用。nested scroll 我不知道是什么,有些人称呼为嵌套滑动。那就用嵌套滑动来翻译吧。注释中说过,只有在 onStartNestedSroll() 方法返回 true 时,后续的嵌套滑动事件才会响应。

后续的响应函数应该就是指的是这几个方法

代码语言:javascript
复制
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
        View directTargetChild, View target, int nestedScrollAxes) {}

public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {}

public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {}

public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
        int dx, int dy, int[] consumed) {}

public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY, boolean consumed) {}

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY) {}

那么,我们从源头方法 onStartNestedScroll() 中开始分析。

借助于 AndroidStudio,我们很容易查找到 Behavior 中 onStartNestedScroll() 方法在哪里被调用。

这里写图片描述
这里写图片描述

原来,它是在 CoordinatorLayout 中被调用,我们跟进去看一看。

代码语言:javascript
复制
@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;
}

这是 CoordinatorLayout 中的一个方法,它获取子 View 的 Behavior,然后调用 Behavior 的 onStartNestedScroll() 方法。

再深入一点,谁调用了 CoordinatorLayout 的 onStartNestedScroll() 呢?

我们继续追踪。

这里写图片描述
这里写图片描述

发现有 3 个类可以调用它,一个是 View,另外一个是 ViewParentCompatLollipop 和 ViewParentCompatStubImpl。那么其实归根到底就是 View 和 ViewParentCompat。我们先从 View 开始分析好了。

代码语言:javascript
复制
public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = getParent();
        View child = this;
        while (p != null) {
            try {
                if (p.onStartNestedScroll(child, this, axes)) {
                    mNestedScrollingParent = p;
                    p.onNestedScrollAccepted(child, this, axes);
                    return true;
                }
            } catch (AbstractMethodError e) {
                Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
                        "method onStartNestedScroll", e);
                // Allow the search upward to continue
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

逻辑已经很清晰了,当一个 View 的 startNestedScroll() 方法触发时,如果符合规则,则会遍历自己的 parent,调用 parent 的 onStartNestedScroll() 方法。因为 CoordinatorLayout 是一个 ViewGroup,所以它就是一个 ViewParent 对象。所以,如果一个 CoordinatorLayout 中的后代触发了 startNestedScroll() 方法,如果符合某种条件,那么它的 onStartNestedScroll() 方法就会调用,再进一步会调用相应 Behavior 的方法。

注意我在上文中的措辞,我说过符合规则或者说符合某种条件,那么条件的具体是什么呢?

代码语言:javascript
复制
/**
 * Returns true if nested scrolling is enabled for this view.
 *
 * <p>If nested scrolling is enabled and this View class implementation supports it,
 * this view will act as a nested scrolling child view when applicable, forwarding data
 * about the scroll operation in progress to a compatible and cooperating nested scrolling
 * parent.</p>
 *
 * @return true if nested scrolling is enabled
 *
 * @see #setNestedScrollingEnabled(boolean)
 */
public boolean isNestedScrollingEnabled() {
    return (mPrivateFlags3 & PFLAG3_NESTED_SCROLLING_ENABLED) ==
            PFLAG3_NESTED_SCROLLING_ENABLED;
}

当 isNestedScrollingEnabled() 返回 true 时,它的 ViewParent 的 onStartNestedScroll() 才能被触发。这个方法的逻辑就是判断一个 View 中 mPrivateFlags3 这个变量中的 PFLAG3_NESTED_SCROLLING_ENABLED 这一 bit 是否被置为 1 。

注释有提到,另外一个方法 setNestedScrollingEnabled() 来设置能不能拥有嵌套滑动的能力。

代码语言:javascript
复制
public void setNestedScrollingEnabled(boolean enabled) {
    if (enabled) {
        mPrivateFlags3 |= PFLAG3_NESTED_SCROLLING_ENABLED;
    } else {
        stopNestedScroll();
        mPrivateFlags3 &= ~PFLAG3_NESTED_SCROLLING_ENABLED;
    }
}

看到这里的时候,我有了一个大胆的想法。

大胆,用 Button 产生一个 nested scroll 事件

如果一个 View 符合嵌套滑动的条件。也就是通过调用 setNestedScrollingEnabled(true),然后调用它的 startNestedScroll() 方法,它理论上是应该可以产生嵌套滑动事件的。好吧,我们来试一下,我们在布局文件中添加一个普通的 Button,然后给它设置点击事件。代码如下:

代码语言:javascript
复制
mBtnTest = (Button) findViewById(R.id.btn_nested_scroll);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    mBtnTest.setNestedScrollingEnabled(true);
}

mBtnTest.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mBtnTest.startNestedScroll(View.SCROLL_AXIS_HORIZONTAL);
        }
    }
});

因为我之前在 MyBehavior 中对相关的方法有 log 代码,所以如果 CoordinatorLayout 中发生嵌套滑动事件,log 是有输出的。

这里写图片描述
这里写图片描述

如上图所示,结果符合预期。不过,我们看上面的代码,当一个 View 只有在版本在 Lollipop 及以上时,它才能调用嵌套滑动相关的 api。如果是 5.0 版本以下呢?其实系统做了兼容。

代码语言:javascript
复制
mBtnTest = (Button) findViewById(R.id.btn_nested_scroll);
ViewCompat.setNestedScrollingEnabled(mBtnTest,true);
mBtnTest.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            ViewCompat.startNestedScroll(mBtnTest,View.SCROLL_AXIS_HORIZONTAL);
        }
    }
});

我们可以通过 ViewCompat 这个类来完成对应的操作。不停歇,继续跟踪下去。

代码语言:javascript
复制
public static void setNestedScrollingEnabled(View view, boolean enabled) {
        IMPL.setNestedScrollingEnabled(view, enabled);
}

通过 IMPL 这个代理来完成。

代码语言:javascript
复制
static final ViewCompatImpl IMPL;
static {
    final int version = android.os.Build.VERSION.SDK_INT;
    if (BuildCompat.isAtLeastN()) {
        IMPL = new Api24ViewCompatImpl();
    } else if (version >= 23) {
        IMPL = new MarshmallowViewCompatImpl();
    } else if (version >= 21) {
        IMPL = new LollipopViewCompatImpl();
    } else if (version >= 19) {
        IMPL = new KitKatViewCompatImpl();
    } else if (version >= 18) {
        IMPL = new JbMr2ViewCompatImpl();
    } else if (version >= 17) {
        IMPL = new JbMr1ViewCompatImpl();
    } else if (version >= 16) {
        IMPL = new JBViewCompatImpl();
    } else if (version >= 15) {
        IMPL = new ICSMr1ViewCompatImpl();
    } else if (version >= 14) {
        IMPL = new ICSViewCompatImpl();
    } else if (version >= 11) {
        IMPL = new HCViewCompatImpl();
    } else {
        IMPL = new BaseViewCompatImpl();
    }
}

针对不同的系统版本,IMPL 有不同的实现,所以它才能够做到兼容。我们知道在 Lollipop 版本 View 已经自带了嵌套滑动和相关属性和方法,现在我们就关心最低的版本,它们是如何处理这种情况的。最低的兼容版本是 BaseViewCompatImpl。

代码语言:javascript
复制
static class BaseViewCompatImpl implements ViewCompatImpl {

    @Override
    public void setNestedScrollingEnabled(View view, boolean enabled) {
        if (view instanceof NestedScrollingChild) {
            ((NestedScrollingChild) view).setNestedScrollingEnabled(enabled);
        }
    }

    @Override
    public boolean isNestedScrollingEnabled(View view) {
        if (view instanceof NestedScrollingChild) {
            return ((NestedScrollingChild) view).isNestedScrollingEnabled();
        }
        return false;
    }



    @Override
    public boolean startNestedScroll(View view, int axes) {
        if (view instanceof NestedScrollingChild) {
            return ((NestedScrollingChild) view).startNestedScroll(axes);
        }
        return false;
    }


}

代码有删简,但足够水落石出了。如果在 5.0 的系统版本以下,如果一个 View 想发起嵌套滑动事件,你得保证这个 View 实现了 NestedScrollingChild 接口。

想触发嵌套滑动事件吗?你是 NestedScrollingChild 吗?

如果在 5.0 的系统版本以上,我们要 setNestedScrollingEnabled(true),如果在这个版本以下,得保证这个 View 本身是 NestedScrollingChild 的实现类才行。现在就需要把焦点放在 NestedScrollingChild 上了。

代码语言:javascript
复制
public interface NestedScrollingChild {

    public void setNestedScrollingEnabled(boolean enabled);


    public boolean isNestedScrollingEnabled();


    public boolean startNestedScroll(int axes);


    public void stopNestedScroll();


    public boolean hasNestedScrollingParent();


    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);


    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);


    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);


    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

借助于 AndroidStudio,通过 CTRL + T 快捷键,我们可以得到目前 NestedScrollingChild 的实现者。

这里写图片描述
这里写图片描述

它有 4 个实现类:NavigationMenuView、NestedScrollView、RecyclerView、SwipleRefreshLayout。

好吧,这次追踪完结了。我们不需要将一个 View 设置能够滑动,然后再模拟滑动事件了。现在系统提供了 4 个来挑选。RecyclerView 和 SwipleRefreshLayout 我们自然是熟悉,NestedScrollView 看名字就可以联想到是能产生嵌套滑动的 ScrollView。

接下来的任务是什么?

别忘记了这一节的主题是自定义 Behavior。我们只在第一部分探索了 child 之间的依赖互动关系,还没有去讨论 Behavior 中如何响应嵌套滑动事件。之前的千回百转,我只是想找到能够挑起嵌套滑动事端的 View 而已。现在找到了之后,我们继续之前的话题。我们现在将一个 NestedScrollView 放进布局文件中,滑动它的内容,它将产生嵌套滑动事件。Behavior 需要针对自身业务逻辑进行相应的处理。

代码语言:javascript
复制
<android.support.v4.widget.NestedScrollView
        android:layout_marginTop="200dp"
        android:layout_width="300dp"
        android:layout_height="wrap_content">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin"
        android:text="@string/large_text" />
</android.support.v4.widget.NestedScrollView>

我们的目的是当 NestedScrollView 内容滑动时,MyBehavior 规定关联的 ImageView 对象进行相应的位移,这主要是在 Y 轴方向上。首先我们得实现这个方法。

代码语言:javascript
复制
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                                   View directTargetChild, View target, int nestedScrollAxes) {
    Log.d(TAG,"onStartNestedScroll");
    return child instanceof ImageView && nestedScrollAxes == View.SCROLL_AXIS_VERTICAL;
}

只有 child 是 ImageView 类型,并且滑动的方向是 Y 轴时才响应。然后,我们可以针对滑动事件产生的位移对 child 进行操作了。

代码语言:javascript
复制
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    Log.d(TAG,"onNestedPreScroll  dx:"+dx+" dy:"+dy);
    ViewCompat.offsetTopAndBottom(child,dy);
}

我们要复写 onNestedPreScroll() 方法,dx 和 dy 是滑动的位移。另外还有一个方法 onNestedScroll()。两个方法的不同在于顺序的先后,onNestedPreScroll() 在 滑动事件准备作用的时候先行调用,注意是准备作用,然后把已经消耗过的距离传递给 consumed 这个数组当中。而 onNestedScroll() 是滑动事件作用时调用的。它的参数包括位移信息,以及已经在 onNestedPreScroll() 消耗过的位移数值。我们一般实现 onNestedPreScroll() 方法就好了。

在上面代码中,我们通过读取 dy 的值,来让 child 进行 Y 轴方向上的移动。 当然,在这之前我们要将 MyBehavior 做一些处理。将它与 TestView 解除依赖。

代码语言:javascript
复制
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    //return dependency instanceof DependencyView;
    return false;
}

然后,我们回顾下 xml 布局文件。

代码语言:javascript
复制
<ImageView
        android:id="@+id/iv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="for test"
    app:layout_behavior="com.frank.supportdesigndemo.MyBehavior"/>

<android.support.v4.widget.NestedScrollView
    android:layout_marginTop="200dp"
    android:layout_width="300dp"
    android:layout_height="wrap_content">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin"
        android:text="@string/large_text" />
</android.support.v4.widget.NestedScrollView>

布局中 ImageView 和 TextView 同时被设置了 MyBehavior 属性,但是根据代码逻辑,最终应该只有 ImageView 能够进行 Y 轴方向的移动。那么事实如何?

这里写图片描述
这里写图片描述

效果达到了预期。

可能有细心的同学还会发现,Byhavior 中有两个与 Fling 相关的API。

代码语言:javascript
复制
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY, boolean consumed) {
    return false;
}

public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
        float velocityX, float velocityY) {
    return false;
}

顾名思义,这就和 fling 操作有关。快速滑动 NestedScrollView 或者 RecyclerView,手指停下来的时候,滑动并没有马上停止,这就是 fling 操作。 与前面的 NestedScroll 相似,我们可以在 fling 动作即将发生时,通过 onNestedPreFling 获知,如果在这个方法返回值为 true 的话会怎么样?它将会拦截这次 fling 动作,表明响应中的 child 自己处理了这次 fling 意图,那么 NestedScrollView 反而操作不了这个动作,因为系统会当作 child 消耗过这次事件。大家可以自行去尝试一下。我们把注意点放在一个有趣的实验上。

这个实验的目的是当 MyBehavior 响应 fling 动作时,如果滑动方向向下,ImageView 就放大。反之缩小到原先的大小。

代码语言:javascript
复制
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
    Log.d(TAG,"onNestedPreFling velocityY:"+velocityY);
    if ( velocityY > 0 ) {
        child.animate().scaleX(2.0f).scaleY(2.0f).setDuration(2000).start();
    } else {
        child.animate().scaleX(1.0f).scaleY(1.0f).setDuration(2000).start();
    }

    return false;
//        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}

在上面的代码中,我运用了一个 ViewPropertyAnimator 动画来对 scaleX 和 scaleY 进行处理。我们来看看效果吧。

这里写图片描述
这里写图片描述

挺有意思的是吗?

自定义 Behavior 的总结

  1. 确定 CoordinatorLayout 中 View 与 View 之间的依赖关系,通过 layoutDependsOn() 方法,返回值为 true 则依赖,否则不依赖。
  2. 当一个被依赖项 dependency 尺寸或者位置发生变化时,依赖方会通过 Byhavior 获取到,然后在 onDependentViewChanged 中处理。如果在这个方法中 child 尺寸或者位置发生了变化,则需要 return true。
  3. 当 Behavior 中的 View 准备响应嵌套滑动时,它不需要通过 layoutDependsOn() 来进行依赖绑定。只需要在 onStartNestedScroll() 方法中通过返回值告知 ViewParent,它是否对嵌套滑动感兴趣。返回值为 true 时,后续的滑动事件才能被响应。
  4. 嵌套滑动包括滑动(scroll) 和 快速滑动(fling) 两种情况。开发者根据实际情况运用就好了。
  5. Behavior 通过 3 种方式绑定:1. xml 布局文件。2. 代码设置 layoutparam。3. 自定义 View 的注解。

弄清楚上面的规则后,恭喜你,你已经掌握了自定义 Behavior 的基础技能。

再多一点细节?

不知道大家还记得不,文章前面部分我有试图去找出谁能产生 Nested Scroll 事件,结果发现它需要是 NestedScrollChild 对象。但是,我们忽略了一个细节,这个细节就是一个 NestedScrollChild 调用 startNestedScroll() 方法时,其实它需要借助它的祖先的力量。只有某个祖先的 onStartNestedScroll() 返回为真的时候,它持续事件才能延续下去。

view.java

代码语言:javascript
复制
public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = getParent();
        View child = this;
        while (p != null) {
            try {
                if (p.onStartNestedScroll(child, this, axes)) {
                    mNestedScrollingParent = p;
                    p.onNestedScrollAccepted(child, this, axes);
                    return true;
                }
            } catch (AbstractMethodError e) {
                Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
                        "method onStartNestedScroll", e);
                // Allow the search upward to continue
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

毫无疑问,ViewParent 充当了重要的角色。

代码语言:javascript
复制
public interface ViewParent {

    public void requestLayout();


    public void invalidateChild(View child, Rect r);


    public ViewParent getParent();


    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);


    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);


    public void onStopNestedScroll(View target);


    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);


    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);


    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);


    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

}

ViewParent 只是一个接口,常见的实现类是 ViewGroup,我曾经一度把 ViewParent 等同于 ViewGroup,结果在分析 View 绘制流程时吃尽苦头,因为 ViewParent 还有其它的实现类,比如 ViewRoot。不过这是题外话,我们接着聊 ViewParent。ViewParent 提供了 nested scroll 相关的 API,但是 5.0 版本才加进去的,如果要兼容的话,我们需要分析 ViewParentCompat 这个类。

代码语言:javascript
复制
public final class ViewParentCompat {

    interface ViewParentCompatImpl {
        public boolean requestSendAccessibilityEvent(
                ViewParent parent, View child, AccessibilityEvent event);
        boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes);
        void onNestedScrollAccepted(ViewParent parent, View child, View target,
                int nestedScrollAxes);
        void onStopNestedScroll(ViewParent parent, View target);
        void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed);
        void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed);
        boolean onNestedFling(ViewParent parent, View target, float velocityX, float velocityY,
                boolean consumed);
        boolean onNestedPreFling(ViewParent parent, View target, float velocityX, float velocityY);
        void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child,
                View source, int changeType);
    }

    static class ViewParentCompatStubImpl implements ViewParentCompatImpl {


        @Override
        public boolean onStartNestedScroll(ViewParent parent, View child, View target,
                int nestedScrollAxes) {
            if (parent instanceof NestedScrollingParent) {
                return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                        nestedScrollAxes);
            }
            return false;
        }
    }
}

这次,又挖掘出了一些真相。在 5.0 版本以下,如果一个 ViewParent 要响应嵌套滑动事件,就得保证它自己是一个 NestedScrollingParent 对象。

代码语言:javascript
复制
public interface NestedScrollingParent {


public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);


public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);


public void onStopNestedScroll(View target);


public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
        int dxUnconsumed, int dyUnconsumed);


public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);


public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);


public boolean onNestedPreFling(View target, float velocityX, float velocityY);


public int getNestedScrollAxes();
}

还是借助于 AndroidStudio 和快捷键 CTRL+T。我们可以得到它的实现类。

这里写图片描述
这里写图片描述

我们可以发现目前系统已经实现的 NestedScrollingParent 有 4 个:ActionBarOverlayLayout、CoordinatorLayout、NestedScrollView 和 SwipleRefreshLayout。

本文所讨论的 CoordinatorLayout 之所以能够处理嵌套滑动事件,这是因为它本身是一个 NestedScrollingParent。另外一个有意思的地方就是,NestedScrollView 它有两种身份,它同时实现了 NestedScrollingChild 和 NestedScrollParent 接口,也就说明它具备了两种能力,这为它本身的扩展提供了许多可能性。

Nested scroll 的流程

到这里的时候,一个嵌套滑动的事件的起始我们才彻底明白。它是由一个 NestedScrollingChild(5.0 版本 setNestedScrollEnable(true) 就好了) 发起,通过向上遍历 parent,借助于 parent 对象的相关方法来完成交互。值得注意的是 5.0 版本以下,parent 要保证是一个 NestedScrollingParent 对象。

我们今天的文章是分析 CoordinatorLayout 及它的 Behavior,所以用一张图来概念更清晰明了。

这里写图片描述
这里写图片描述

文章到此,又可以完结了。对于自定义一个 Behavior 而言,我们已经明白了它的功能及如何实现自己特定的功能。但是对于 CoordinatorLayout 本身而言,它还有许多细节需要说明。但是这些细节跟通用的自定义 ViewGroup 并无多大差别。唯一不同的地方的因为 Behavior 的存在。

CoordinatorLayout 的其它细节

Behavior 在之前也说过,它是一种插件。正因为这种机制,它将干涉 CoordinatorLayout 与 childView 之间的关系,Behavior 通过拦截 CoordinatorLayout 发给子 View 的信号,根据自身的规则进而来达到控制 childView 的目的。如果没有这些 Behavior 存在的话,CoordinatorLayout 跟普通的 ViewGroup 无疑。

那么,Behavior 干涉了 CoordinatorLayout 与它的 childView 之间的什么?可以说是方方面面。从测量、布局到触摸事件等等。

这里写图片描述
这里写图片描述

CoordinatorLayout 测量

CoordiantorLayout 是一个超级 FrameLayout,但是它却并不是 FrameLayout 的直接子类,只是一个普通的 ViewGroup 子类而已。FrameLayout 有什么物质?它就是一层一层的,按照位置信息进行布局,并没有像 LinearLayout 与 RelativeLayout 那么多约束。

所以,在查看相关代码之前,我们可以猜测的是,CoordinatorLayout 在 wrap_content 这种情况下,宽高的尺寸信息主要是要找出它子 View 的最大宽度或者最大高度,当然,还得参考 CoordianatorLayout 本身 parent 给它的建议尺寸。那么,实际情况是不是这样子呢?

代码语言:javascript
复制
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();


    final int paddingLeft = getPaddingLeft();
    final int paddingTop = getPaddingTop();
    final int paddingRight = getPaddingRight();
    final int paddingBottom = getPaddingBottom();
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final boolean isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL;
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    final int widthPadding = paddingLeft + paddingRight;
    final int heightPadding = paddingTop + paddingBottom;

    int widthUsed = getSuggestedMinimumWidth();
    int heightUsed = getSuggestedMinimumHeight();
    int childState = 0;

    final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(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();

        int childWidthMeasureSpec = widthMeasureSpec;
        int childHeightMeasureSpec = heightMeasureSpec;


        final Behavior b = lp.getBehavior();
        if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 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);
}

上面的代码中,我精简了一些线索无关的代码。我们重点要关注 widthUsed 和 heightUsed 两个变量,它们的作用就是为了保存 CoordinatorLayout 中最大尺寸的子 View 的尺寸。并且,在对子 View 进行遍历的时候,CoordinatorLayout 有主动向子 View 的 Behavior 传递测量的要求,如果 Behavior 自主测量了 child,则以它的结果为准,否则将调用 measureChild() 方法亲自测量。

CoordinatorLayout 布局

在 FrameLayout 中布局默认从左上角开始,但是可以通过 layoutparam 中的 Gravity 进行布局对齐。那么,CoordinatorLayout 布局时会如何表现?

代码语言:javascript
复制
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);
        }
    }
}

可以看到,CoordinatorLayout 将布局交给了子 View 的 Behavior,让它自行处理,如果 Behavior 没有处理关联的 View 布局的话,CoordinatorLayout 就会调用 onLayoutChild() 方法布局。我们再跟进去。

代码语言:javascript
复制
public void onLayoutChild(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (lp.checkAnchorChanged()) {
        throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
                + " measurement begins before layout is complete.");
    }
    if (lp.mAnchorView != null) {
        layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
    } else if (lp.keyline >= 0) {
        layoutChildWithKeyline(child, lp.keyline, layoutDirection);
    } else {
        layoutChild(child, layoutDirection);
    }
}

这一下引出了三个方法,并且有优先级的。layoutChildWithAnchor 优先级最高,然后是 layoutChildWithKeyline,最后才是普通的 layoutChild。

CoordinatorLayout 的锚定

LayoutParam 中有个 mAnchorView,Anchor 是锚点的意思,比如 View A 锚定了 View B,那么 View A 的 mAnchorView 就是 View B,布局的时候 View A 将参考 View B 的坐标。并且 layoutDirection 是参考的方向。它们都可以通过 xml 配置。

代码语言:javascript
复制
app:layout_anchor="@id/btn_coord"
app:layout_anchorGravity="bottom"

需要注意的是,当 View A 锚定 View B 时,就说明 View A 依赖于视图 View B。这个并不需要在 Behavior 中的 layoutDependsOn 返回 true。具体细节是因为下面的代码:

代码语言:javascript
复制
boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency == mAnchorDirectChild
            || shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
            || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}

上面这个方法是 LayoutPara 中的方法,判断 child 是不是依赖于 dependency,先判断的是 dependency 是否被 child 锚定,如果是的话就无需调用 Behavior 的 layoutDependsOn。

CoordinatorLayout 的参考线

除了锚定这个概念外,出现了 keyline 这个概念。keyline 应该是参考线的意思。指名这个 childView 布局时根据 keyline 的偏移量,再结合相应的 Gravity 进行布局。篇幅有限,感兴趣的同学自行去实践一下相应场景。

最后,我们聚集普通的 layoutChild() 方法。

代码语言:javascript
复制
private void layoutChild(View child, int layoutDirection) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect parent = mTempRect1;
    parent.set(getPaddingLeft() + lp.leftMargin,
            getPaddingTop() + lp.topMargin,
            getWidth() - getPaddingRight() - lp.rightMargin,
            getHeight() - getPaddingBottom() - lp.bottomMargin);

    if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this)
            && !ViewCompat.getFitsSystemWindows(child)) {
        // If we're set to handle insets but this child isn't, then it has been measured as
        // if there are no insets. We need to lay it out to match.
        parent.left += mLastInsets.getSystemWindowInsetLeft();
        parent.top += mLastInsets.getSystemWindowInsetTop();
        parent.right -= mLastInsets.getSystemWindowInsetRight();
        parent.bottom -= mLastInsets.getSystemWindowInsetBottom();
    }

    final Rect out = mTempRect2;
    GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
            child.getMeasuredHeight(), parent, out, layoutDirection);
    child.layout(out.left, out.top, out.right, out.bottom);
}

果真如我所推测的一样,它只是通过 GravityCompat.apply() 方法,通过 Gravity 确定 childView 在 parent 中的显示位置。这个效果就等同于了 FrameLayout。

通过测量、布局之后,CoordinatorLayout 就可以正常绘制了。但是如果要进行一些触摸输入间的交互就还要分析一个内容。这就是它的 touch 相关的事件。

代码语言:javascript
复制
@Override
public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;

    final int action = MotionEventCompat.getActionMasked(ev);

    if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
        // Safe since performIntercept guarantees that
        // mBehaviorTouchView != null if it returns true
        final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
        }
    }

    // Keep the super implementation correct
    if (mBehaviorTouchView == null) {
        handled |= super.onTouchEvent(ev);
    } else if (cancelSuper) {
        if (cancelEvent == null) {
            final long now = SystemClock.uptimeMillis();
            cancelEvent = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
        }
        super.onTouchEvent(cancelEvent);
    }

    if (!handled && action == MotionEvent.ACTION_DOWN) {

    }

    if (cancelEvent != null) {
        cancelEvent.recycle();
    }

    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }

    return handled;
}

private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);

        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);
                    }
                    switch (type) {
                        case TYPE_ON_INTERCEPT:
                            b.onInterceptTouchEvent(this, child, cancelEvent);
                            break;
                        case TYPE_ON_TOUCH:
                            b.onTouchEvent(this, child, cancelEvent);
                            break;
                    }
                }
                continue;
            }

            if (!intercepted && b != null) {
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    case TYPE_ON_TOUCH:
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                if (intercepted) {
                    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;
    }

可以看到的是,如果 CoordinatorLayout 内部如果没有被拦截,那么它会传递触摸信息给 Behavior,如果有 Behavior 需要拦截这样的动作,那么就交给 Behavior,如果没有的话,它就会走传统的 ViewGroup 处理触摸事件的流程。

自此,CoordinatorLayout 绝大多数细节已经讨论完成。

总结

如果你是从头看到这里,我不知道你有没有这种感觉,像探索一样,经历了很长一段时间,顺着一条条线索,焦急、纠结,最终走出了一条道路。回首溯望,也许会有种风轻云淡的感觉。

这篇文章洋洋洒洒已经有千字以上了,因为篇幅过长,为了防止遗忘。现在可以将文章细节总结如下:

  1. CoordinatorLayout 是一个普通的 ViewGroup,它的布局特性类似于 FrameLayout。
  2. CoordinatorLayout 是超级 FrameLayout,它比 FrameLayout 更强悍的原因是它能与 Behavior 交互。
  3. CoordinatorLayout 与 Behavior 相辅相成,它们一起构建了一个美妙的交互系统。
  4. 自定义 Behavior 主要有 2 个目的:1 确定一个 View 依赖另外一个 View 的依赖关系。2 指定一个 View 响应嵌套滑动事件。
  5. 确定两个 View 的依赖关系,有两种途径。一个是在 Behavior 中的 layoutDepentOn() 返回 true。另外一种就是直接通过 xml 锚定一个 View。当被依赖方尺寸和位置变化时,Behavior 中的 onDependentViewChanged 方法会被调用。如果在这个方法中改变了主动依赖的那个 view 的尺寸或者位置信息,应该在方法最后 return true。
  6. 嵌套滑动分为 nested scroll 和 fling 两种。Behavior 中相应的 View 是否接受响应由 onStartNestedScroll() 返回值决定。一般在 onNestedPreScroll() 处理相应的 nested scroll 响应,在 onPreFling 处理 fling 事件。但是这个不绝对,根据实际情况决定。
  7. NestedScrollView 能够产生嵌套滑动事件是因为它本质上是一个 NestedScrollingChild 对象,而 CoordinatorLayout 能够响应是因为它本质上是一个 NestedScrollingParent 对象。
  8. Behavior 是一种插件机制,如果没有 Behavior 的存在,CoordinatorLayout 和普通的 FrameLayout 无异。Behavior 的存在,可以决定 CoordinatorLayout 中对应的 childview 的测量尺寸、布局位置、触摸响应。

最后,回到文章最开始的地方。我们已经熟悉了 CoordinatorLayout,NestedScrollView 也了解一点点。而对于 AppbarLayout 我们还不了解。但是之前的恐慌却不存在了,因为了解到 Behavior 的机制,我们可以知道 CoordinatorLayout 并不是一定要和 AppBarLayout 或者 FloatButton 一起配合使用,它是独立的,抛开它们,我们通过自定义 Behavior 也可以实现非常炫丽的交互效果。

而系统自定义的 Behavior 可以给开发者提供了许多场景的便利与降低开发难度。

不要重复造轮子。但不代表我们不需要去了解轮子。

接下来,我将会一一学习 Android Support Design 这个库中其它有意思的类,如 AppBarLayout、CollapsingToolbarLayout、FloatingActionButton 等等,当然,必不可少的是与它们配合使用的各类 Behavior,如非常牛逼的 AppBarLayout.ScrollingViewBehavior 和 BottomSheetBehavior 等等。

这个精彩世界值得我们探索。

源码github地址

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017年06月12日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 更多的真相
    • 两个 View 之间的依赖关系
      • 做一个跟屁虫
    • Behavior 的设置方法
      • 1. 在 xml 属性中进行设置
      • 2. 在代码中设置
      • 3. 通过注解
    • Behavior 对滑动事件的响应。
      • 大胆,用 Button 产生一个 nested scroll 事件
      • 想触发嵌套滑动事件吗?你是 NestedScrollingChild 吗?
  • 自定义 Behavior 的总结
    • 再多一点细节?
    • Nested scroll 的流程
    • CoordinatorLayout 的其它细节
      • CoordinatorLayout 测量
        • CoordinatorLayout 布局
          • CoordinatorLayout 的锚定
          • CoordinatorLayout 的参考线
      • 总结
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档