CoordinatorLayout相关知识点详解

1.前言


提起CoordinatorLayout,大家立马能想到绚丽的首页滚动交互。可是真当去实践时,官方Demo提供的那一堆控件及其属性组合,令人头昏眼花、理不清思路。这次,我们从外(父)到内(子)来分析各控件组合及作用。

2.CoordinatorLayout


这是一个布局,继承自ViewGroup,从安置子视图的角度来说,相当于是强大的FrameLayout。对它的使用要明确两点:作为顶层布局使用;给内部的一个或多个子视图提供指定交互。先说说两个高级属性(已被封装好的交互):

  • anchor及anchorGravity。这可以将悬浮视图与内容视图关联,使之跟随内容移动。anchor可以指定为CoordinatorLayout内的任意视图Id(除了使用这个属性的视图及其子视图),而anchorGravity负责在内容视图内放置悬浮视图。
  • insetEdge和dodgeInsetEdges。这是一对同时使用的属性,目的是不让视图被遮挡。当两个视图有遮挡时,其中一个insetEdge设置为bottom,另一个dodgeInsetEdges也设置为bottom,则第二个向top方向避让。若两属性设置方向不一致时,无效;也可以设置多个方向或者给多个视图设置(部分情况会出问题)。详细参考jscoolstar的文章。

由于这两对交互比较常用且逻辑清晰,所以抽取出来。那么内部是由什么实现的呢?自己如何定义交互呢?这时得使用Behavior类,具体逻辑是这样:

  • CoordinatorLayout作为容器可以监听子视图状态上的变化,但是滚动视图只是内容变化,状态并未变化,监听不到。所以系统给监听者和被监听者分别提供两个接口NestedScrollingParent和NestedScrollingChild。第一个接口需要被ViewGroup的子类实现,表明希望协助完成嵌套子视图的滚动操作;第二个接口需要被View的子类实现,表明希望分发嵌套滚动操作给协作的父布局。有这些类满足:

    CoordinatorLayout相关知识点详解_第1张图片
    NestedScrolling.png

  • CoordinatorLayout则将状态变化和滚动变化分发给直接子视图,由它们的Behavior对象接受判断是否符合条件,进行什么操作,有点类似广播机制(其实是遍历子视图)。注意,Behavior在布局文件中由app:layout_behavior声明或者在从动视图类上加 @DefaultBehavior() 声明,初始化是在CoordinatorLayout的LayoutParams中通过反射完成的。详细信息可以参考源码分析。

3.Behavior


在自定义Behavior类之前,先声明两个概念,即主动与从动。由于是视图的交互,必然是其中一个发生变化引起另一个的变化,那么前者就是主动,后者便是从动。Behavior可以接收许多事件,我们主要重写依赖(状态变化)和滚动。

import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;

// 1.为了演示,声明Behavior的从动对象是TextView
// 使用时,被设置的对象类型是它或子类,否则报错
public class MyBehavior extends CoordinatorLayout.Behavior {

    // 2.重写构造函数,若在XML布局中使用,得有AttributeSet
    public MyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 3.自己判断是否需要执行依赖交互,返回true执行,false不执行
    // child从动对象,dependency主动对象
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, TextView child, View dependency) {
        return super.layoutDependsOn(parent, child, dependency);
    }

    // 4.自己实现交互操作,返回true表示需改变child大小和位置,false则不用
    // child从动对象,dependency主动对象
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, TextView child, View dependency) {
        return super.onDependentViewChanged(parent, child, dependency);
    }

    // 5.自己判断是否需要执行滚动交互,返回true执行,false不执行
    // child从动对象,target主动对象,directTarget为CoordinatorLayout子视图,是或包含主动对象,nestedScrollAxes水平还是竖直滑动
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, TextView child, View directTargetChild, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    // 6.自己实现交互操作
    // child从动对象,target主动对象,dxConsumed/dyConsumed为主动对象在水平和竖直上已滚动距离,另外两个是未滚动距离
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, TextView child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    // 7.这是快速滑动操作
    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, TextView child, View target, float velocityX, float velocityY, boolean consumed) {
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }
}

Behavior能在从动视图前收到CoordinatorLayout的所有触摸事件,并做出相应处理,与View的事件分发一致。详细参考Jude95的文章。

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, TextView child, MotionEvent ev) {
        return super.onInterceptTouchEvent(parent, child, ev);
    }
    @Override
    public boolean onTouchEvent(CoordinatorLayout parent, TextView child, MotionEvent ev) {
        return super.onTouchEvent(parent, child, ev);
    }

4.AppBarLayout


AppBarLayout继承自LinearLayout,最大的特色就是实现了滚动手势,并通过给子视图设置scrollFlags来操作它们的滚动行为。若要滚动,scroll必须第一个设置。但是此功能仅当它作为CoordinatorLayout的直接子视图时有效,这不是和Behavior的用法一样吗?看看源码。

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {

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

    public AppBarLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior {

        public ScrollingViewBehavior() {}

        public ScrollingViewBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @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;
        }
}
public static class Behavior extends HeaderBehavior {

        public Behavior() {}

        public Behavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                View directTargetChild, View target, int nestedScrollAxes) {
            // Return true if we're nested scrolling vertically, and we have scrollable children
            // and the scrolling view is big enough to scroll
            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;
        }
}

果然是通过Behavior实现AppBarLayout与滚动手势交互的。按照前面学的知识可以知道,onStartNestedScroll() 方法中的条件就是实现滚动的条件。那官方的Demo中为什么要给NestedScrollView设置AppBarLayout.ScrollingViewBehavior?通过ScrollingViewBehavior的 layoutDependsOn() 方法可知,是设置的视图依赖AppBarLayout,看来是为了根据AppBarLayout的移动来调整自己在界面中的位置,onDependentViewChanged() 方法证明了这点。

5.总结


知识点太杂了,我来理一理思路。CoordinatorLayout作为容器,接收子视图的变化,再分发对应事件,起到解耦的作用,使事件的交互对象只有它。Behavior则是从视图的事件处理,只在CoordinatorLayout的直接子视图中起作用,当满足条件就可以操作视图。AppBarLayout目的就一个,带着子视图一起响应滚动手势。

你可能感兴趣的:(CoordinatorLayout相关知识点详解)