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的子类实现,表明希望分发嵌套滚动操作给协作的父布局。有这些类满足:

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

    // 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<AppBarLayout> {

        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目的就一个,带着子视图一起响应滚动手势。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容