美文网首页
CoordinatorLayout解析

CoordinatorLayout解析

作者: Yasin27878 | 来源:发表于2018-12-14 14:50 被阅读21次

    CoordinatorLayout使用解析

    一. CoordinatorLayout介绍

    1. CoordinatorLayout是一个“加强版”FrameLayout,

    它主要有两个用途:

    1. 用作应用的顶层布局管理器,也就是作为用户界面中所有UI控件的容器
    2. 用作相互之间距有特定交互行为的UI控件的容器
      通过为CoordinatorLayout的子View指定Behavior,就可以实现它们之间的交互行为。
      Behavior可以用来实现一系列的交互行为和布局变化,比如说侧滑菜单、可滑动删除的UI元素,以及跟随着其他UI控件移动的按钮等。

    2.文字不够形象, 直接来欣赏一下facebook的效果

    image

    3. CoordinatorLayout的使用

    使用CoordinatorLayout需要在Gradle加入Support Design Library:

    compile 'com.android.support:appcompat-v7:26.1.0'
    

    二.AppBarLayout, CollapsingToolbarLayout的使用

    1.AppBarLayout,CollapsingToolbarLayout是为了配合CoordinatorLayout使用而简单实现相互关系的控件

    2.AppBarLayout 官方文档

    1). AppBarLayout 介绍:
    - 实现了material designs 滑动手势
    - AppBarLayout的子View应该通过{setScrollFlags(int)}或者相关的布局xml属性{app:layout_scrollFlags}提供他们想要的滚动行为。
    - 最好作为CoordinatorLayou的直接子View使用
    - 要拥有一个可滚动的兄弟View并且通过为可滚动的兄弟View设置ScrollingViewBehavior实例来实现绑定。
    
    2).AppBarLayout滑动区域 查看

    当设计滚动行为时,App bar包含构成滚动结构的四个主要区域(称为块):

    • Status bar
    • Tool bar
    • Tab bar/search bar
    • Flexible space: 用来容纳图像或者扩展app bar的期望宽高比
    3) 滑动类型

    AppBarLayout里面的View都是通过设置app:layout_scrollFlags属性控制滑动,Google提供了5种滑动表现类型,分别是

    - scroll:表示向下滚动的时候,设置了这个属性的View会被滚出屏幕范围,直到消失
    - enterAlways:表示向上滚动的时候,设置了这个属性的View会随着滚动手势逐渐出现,直到恢复原来设置的位置(需搭配scroll使用)
    
    - enterAlwaysCollapsed:是enterAlways的附加选项,一般跟enterAlways一起使用,它是指,View在往下“出现”的时候,首先是enterAlways效果,当View的高度达到最小高度时,View就暂时不去往下滚动,直到ScrollView滑动到顶部不再滑动时,View再继续往下滑动,直到滑到View的顶部结束。
    
    - exitUntilCollapsed:值设为exitUntilCollapsed的View,当这个View要往上逐渐“消逝”时,会一直往上滑动,直到剩下的的高度达到它的最小高度后,再响应ScrollView的内部滑动事件(需搭配scroll使用)
    - snap 表示在滑动过程中如果停止滑动,则头部会就近折叠(要么恢复原状,要么折叠成一个Toolbar或者不显示)(需搭配scroll使用)
    
    
    4)重要的监听方法

    addOnOffsetChangedListener()

    appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
        @Override
        public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
              // TODO
        }
    });
    
    

    3.CollapsingToolbarLayout 官方文档

    1.)CollapsingToolbarLayout 介绍:

    CollapsingToolbarLayout是实现一个可以折叠的Toolbar,作为AppbarLayout的直接子View使用,CollapsingToolbarLayout控件提供了一下功能:

        - 折叠标题:当标题栏较大的时候,在布局完全显示的情况下可以显示标题栏,但是标题栏折叠、变小或者布局滚动出屏幕的时候,可以通过setTitle(CharSequence)设置标题显示,通过设置collapsedTextAppearance和expandedTextAppearance属性可以调整标题栏的外观。
    
        - 内容遮罩:通过 setContentScrim(Drawable)更改当滚动到临界点的时候显示或者隐藏
    
        - 状态栏遮罩:可以通过setStatusBarScrim(Drawable)设置遮罩在滚动到临界点之后是显示还是隐藏,必须要在5.0以后设置了fitSystemWindows属性才可以使用
    
        - 视差滚动子视图:子视图可以在这个视差范围内滚动,See COLLAPSE_MODE_PARALLAX and setParallaxMultiplier(float).
    
        - 寄托子视图:子视图可以选择在全局范围内被固定,当实现一个折叠的时候,允许Toolbar被固定在合适的位置,详细见COLLAPSE_MODE_PIN
    
    2) 折叠塌陷的属性collapseMode

    app:layout_collapseMode="pin"属性:这个属性是设置折叠的模式,Android提供有两个值,分别是:

    - pin:设置这个值,当CollapsingToolbarLayout完全折叠之后,View还会显示在屏幕上
    
    - parallax:设置这个值,在内容滚动时,CollapsingToolbarLayout中的View(比如我们这里的ImageView)也会同时滚动,实现视差滚动效果,通常和layout_collapseParallaxMultiplier(设置视差因子)搭配使用。
    
    
    • app:layout_collapseParallaxMultiplier="0.7"属性:设置视差滚动因子,值得范围是0~1.

    • 视差滚动因子数值越大,视觉差越大

    - 如果这里的值为0,则在头部折叠的过程中,ImageView的顶部在慢慢隐藏,底部不动
    - 如果这里的值为1,ImageView的顶部不懂,底部慢慢隐藏,
    - 如果这里的取值为0~1之间,则在折叠的过程中,ImageView的顶部和底部都会隐藏,但是头部和底部隐藏的快慢是不一样的,具体速度和视觉乘数有关
    

    简单来说 0~1代表上下方向折叠的快慢 0上部折叠速度快 1下部折叠速度快

    三.Behavior

    1.Behavior介绍

    1.作用于CoordinatorLayout的子View的交互行为插件。

    一个Behavior 实现了用户的一个或者多个交互行为,它们可能包括拖拽、滑动、快滑或者其他一些手势。

    2. Behavior 是一个顶层抽象类,其他的一些具体行为的Behavior 都是继承自这个类。
    public static abstract class Behavior<V extends View> {
    
            public Behavior() {
            }
    
            public Behavior(Context context, AttributeSet attrs) {
            }
           //省略了若干方法
    }
    
    

    其中有一个泛型,它的作用是指定要使用这个Behavior的View的类型,可以是Button、TextView等等。

    3.自定义Behavior可以选择重写以下的几个重要方法:
      • layoutDependsOn():确定使用Behavior的View要依赖的View的类型
      • onDependentViewChanged():当被依赖的View状态改变时调用
      • onDependentViewRemoved():当被依赖的View移除时调用
      • onStartNestedScroll():嵌套滑动开始(ACTION_DOWN),确定Behavior是否要监听此次事件
      • onNestedScrollAccepted() : onStartNestedScroll返回true才会触发这个方法,接受滚动处理后回调,可以在这个方法里做一些准备工作,如一些状态的重置等。
      • onStopNestedScroll():嵌套滑动结束(ACTION_UP或ACTION_CANCEL)
      • onNestedPreScroll():嵌套滑动进行中,要监听的子 View将要滑动,滑动事件即将被消费(但最终被谁消费,可以通过代码控制)
      • onNestedScroll():嵌套滑动进行中,要监听的子 View的滑动事件已经被消费
      • onNestedFling():要监听的子 View在快速滑动中
      • onNestedPreFling():要监听的子View即将快速滑动
      • onLayoutChild 确定使用Behavior的View位置

    2.绑定Behavior的三种方式

    Behavior无法独立完成工作,必须与实际调用的CoordinatorLayout子视图相绑定。具体有三种方式:通过代码绑定、在XML中绑定或者通过注释实现自动绑定。

    1. 通过代码绑定Behavior

    如果将Behavior当作绑定到CoordinatorLayout中每个视图的附加数据,那么发现Behavior实际上是存储在各个视图的LayoutParams中也就不足为奇了(之前有关于布局的博文)。也是因此,Behavior需要绑定到CoordinatorLayout的直接子项中,因为只有那些子项会包含LayoutParams的特定Behavior子类

    TitleBehavior titleBehavior = new TitleBehavior();
    CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) yourView.getLayoutParams();
    params.setBehavior(titleBehavior);
    
    
    2. app:layout_behavior布局属性

    在布局中设置,值为自定义 Behavior类的名字字符串(包含路径)

    有两种写法,包含包名的全路径和以”.”开头的省略项目包名的路径:

    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    app:layout_behavior="com.yasin.coordinatorlayoutdemo.TitleBehavior"
    app:layout_behavior=".TitleBehavior"
    
    
    3. @CoordinatorLayout.DefaultBehavior类注解

    在需要使用 Behavior的控件源码定义中添加该注解,然后通过反射机制获取。系统的 AppBarLayout、 FloatingActionButton都采用了这种方式,所以无需在布局中重复设置。

    @CoordinatorLayout.DefaultBehavior(TitleBehavior.class)
    public class TitleLayout extends FrameLayout {}
    

    3.Behavior实现View间交互的原理

    behavior像是view的一个属性,其实它是view的LayoutParam的一个属性,就像宽高一样。当然不是任何一个view的LayoutParam都有这个属性的,只有LayoutParam为android.support.design.widget.CoordinatorLayout.LayoutParams才有这个属性.

    所以,就是只有CoordinatorLayout的子view的LayoutParam可以设置behavior。

    • 我们可以在CoordinatorLayout.LayoutParams中找到Behavior属性.
    
    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
            Behavior mBehavior;
            boolean mBehaviorResolved = false;
            ...
            final Rect mLastChildRect = new Rect();
            }
    
    1.Behavior初始化过程
    1. xml方式:app:layout_behavior=”com.yasin.coordinatorlayoutdemo.TitleBehavior”

    infate的时候,会根据xml去构造LayoutParams,所以我们可以在CoordinatorLayout.LayoutParams看到behavior的初始化过程

    LayoutParams(Context context, AttributeSet attrs) {
        super(context, attrs);
    
        final TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.CoordinatorLayout_LayoutParams);
        ...
        mBehaviorResolved = a.hasValue(
                R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
        if (mBehaviorResolved) {
            mBehavior = parseBehavior(context, attrs, a.getString(
                    R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
        }
    
        a.recycle();
    }
    

    2.注解方式:@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)

    在CoordinatorLayout的onMeasure的时候会调用prepareChildren,进而调用getResolvedLayoutParams,在getResolvedLayoutParams里会把注解里的默认Behavior赋值给mBehavior,主要代码如下:

    LayoutParams getResolvedLayoutParams(View child) {
            final LayoutParams result = (LayoutParams) child.getLayoutParams();
            //如果xml内写了behavior,此时result.mBehaviorResolved就为true,不会进去
            if (!result.mBehaviorResolved) {
                Class<?> childClass = child.getClass();
                DefaultBehavior defaultBehavior = null;
                while (childClass != null &&
                        (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
                    childClass = childClass.getSuperclass();
                }
                if (defaultBehavior != null) {
                    try {
                        result.setBehavior(defaultBehavior.value().newInstance());
                    } catch (Exception e) {
                        Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
                                " could not be instantiated. Did you forget a default constructor?", e);
                    }
                }
                result.mBehaviorResolved = true;
            }
            return result;
        }
    
    2.Behavior是如何发挥作用的

    measure和layout是Android绘制视图的关键组件,因此Behavior只有在onMeasureChild()和onLayoutChild()回调前拦截父视图的measure和layout,才能达到预计的效果。

    我们再来看看onMeasure的代码,和Behavior相关的主要看prepareChildren和ensurePreDrawListener两个方法

    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            prepareChildren();
            ensurePreDrawListener();
            。。。
    

    先看 preareChildren()

     private void prepareChildren() {
             //清空mDependencySortedChildren
            mDependencySortedChildren.clear();
            for (int i = 0, count = getChildCount(); i < count; i++) {
                final View child = getChildAt(i);
    
                final LayoutParams lp = getResolvedLayoutParams(child);
                lp.findAnchorView(this, child);
                //加入child
                mDependencySortedChildren.add(child);
            }
              // Finally add the sorted graph list to our list
            mDependencySortedChildren.addAll(mChildDag.getSortedList());
            // 我们还需要反转结果,因为我们希望列表的开头包含没有依赖项的视图,然后在此之后依赖视图。
            Collections.reverse(mDependencySortedChildren);
        }
    
    

    prepareChildren内主要是搞出来一个mDependencySortedChildren,根据依赖关系对child进行排序。先把mDependencySortedChildren clear,然后遍历子view,全部加入到mDependencySortedChildren内,最后对mDependencySortedChildren进行排序

    排序这块没看懂.看了翻译和别的文档大概意思是:被依赖的view放前面,依赖的view放后面. 比如我们fab依赖于snackbar,那么snackbar必然放在fab的前边。这么排序有什么用?其实是提高一点效率,后文会说的。

    次看ensurePreDrawListener()

    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) {
                    //加入PreDrawListener
                    addPreDrawListener();
                } else {
                    removePreDrawListener();
                }
            }
        }
    

    在prepareChildren确定mDependencySortedChildren之后,会执行ensurePreDrawListener,在这里写判断下CoordinatorLayout的子view是否存在依赖关系,如果存在的话就hasDependencies为true,后边会加入OnPreDrawListener,也就是监听依赖View的布局变化
    就是在重绘之前,会调用OnPreDrawListener的onPreDraw方法。再在onPreDraw里面调用了onChildViewsChanged。

    再看onChildViewsChanged()

    final void onChildViewsChanged(@DispatchChangeEvent final int type) {
                    ........
            final int childCount = mDependencySortedChildren.size();
            for (int i = 0; i < childCount; i++) {
            ....
                // 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 (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                            // If this is from a pre-draw and we have already been changed
                            // from a nested scroll, skip the dispatch and reset the flag
                            checkLp.resetChangedAfterNestedScroll();
                            continue;
                        }
    
                        final boolean handled;
                        switch (type) {
                            case EVENT_VIEW_REMOVED:
                                // EVENT_VIEW_REMOVED means that we need to dispatch
                                // onDependentViewRemoved() instead
                                b.onDependentViewRemoved(this, checkChild, child);
                                handled = true;
                                break;
                            default:
                                // Otherwise we dispatch onDependentViewChanged()
                                handled = b.onDependentViewChanged(this, checkChild, child);
                                break;
                        }
                }
        .......
    
          
        }
    

    显然从这里就开始触发View的交互了

    onDependentViewRemoved()和onDependentViewChanged()方法

    四.自定义Behavior

    通常自定义Behavior分为两种情况:

    • 1.通过监听一个View的状态,如位置、大小的变化,来改变其他View的行为,这种只需要重写2个方法就可以了,分别是layoutDependsOn 和onDependentViewChanged, layoutDependsOn方法判断是指定依赖的View时,返回true,然后在onDependentViewChanged 里,被依赖的View做需要的行为动作。

    • 2.是重写onStartNestedScroll、onNestedPreScroll、onNestedScroll等一系列方法.可以实现比较复杂的方法

    相关链接

    Android 详细分析AppBarLayout的五种ScrollFlags链接

    相关文章

      网友评论

          本文标题:CoordinatorLayout解析

          本文链接:https://www.haomeiwen.com/subject/sprghqtx.html