文章大纲

  • 引言
  • 一、CoordinatorLayout概述
  • 二、CoordinatorLayout.LayoutParams概述
  • 三、CoordinatorLayout.Behavior
    • 1、Behavior概述
    • 2、CoordinatorLayout.Behavior核心方法
      • 2.1、layoutDependsOn方法
      • 2.2、onDependentViewChanged方法
      • 2.3、onDependentViewRemoved方法
      • 2.4、onInterceptTouchEvent方法设置是否拦截触摸事件
      • 2.5、onTouchEvent方法处理触摸事件
      • 2.6、onMeasureChild方法测量使用Behavior的View尺寸
      • 2.7、onLayoutChild方法重新布局使用Behavior的View
  • 三、CoordinatorLayout的简单使用
    • 1、引入CoordinatorLayout 依赖
    • 2、继承CoordinatorLayout.Behavior实现自己的Behavior
    • 3、使用CoordinatorLayout作为顶层布局,同时给对应的View配置Behavior使之变成主题View
    • 4、在MainActivity中触发dependency View状态改变进行测试
  • 四、CoordinatorLayout的核心流程解析

引言

前面系列文章总结了Material Design 兼容库提供大部分新控件的使用,Android L之前,如果希望一个ViewGroup里的独立的控件互相关联和交互,比如说侧滑菜单、可滑动删除的UI元素等效果,需要自己去实现交互逻辑,而在引入Material Design 兼容库之后就十分简单了,CoodinatorLayout就提供交互逻辑,系列文章链接:

  • Android进阶——Material Design新控件之初识TabLayout(一)
  • Android进阶——Material Design新控件之TabLayout制作可滚动的Tabs页面(二)
  • Android进阶——Material Design新控件之Snackbar(三)
  • Android进阶——Material Design新控件之TextInputLayout(四)
  • Android进阶——Material Design新控件之FloatingActionButton(五)
  • Android进阶——Material Design新控件之NavigationView(六)
  • Android进阶——Material Design新控件之利用CoordinatorLayout协同多控件交互(七)

一、CoordinatorLayout概述

CoordinatorLayout继承自ViewGroup,可以看成是一个加强版的“FrameLayout”,在Android开发中提供的核心功能主要有:

  • 布局文件当中作为最顶层的布局容器控件
  • CoordinatorLayout中的直接子控件提供特定的交互功能,相当于是给CoordinatorLayout里的直接子控件建立依赖关系,使得原本相对独立的控件,产生“依赖”联系,可以实现一个目标控件改变时,另一个也随着改变。

简而言之,CoordinatorLayout可以使得其直接子控件之间产生“依赖”联系,具体是通过变形的“观察者模式”实现的

  • 主题(被观察者角色)——配置了Behavior的子View(在布局文件中使用Behavior的全类名字符串(也可以@string/R.string.xxx形式引入)来配置View的app:layout_behavior属性
  • 观察者角色—— 在绑定的Behavior的layoutDependsOn方法返回true时的dependency view

观察者View(位置、大小)改变时,就会触发Behavior的onDependentViewChanged方法(只有Dependency View 改变时才会触发)。

无论是主题还是观察者View,都必须是CoordinatorLayout内的直接子View

二、CoordinatorLayout.LayoutParams概述

CoordinatorLayout.LayoutParams是CoordinatorLayout的内部类,和其他ViewGroup功能类似,在CoordinatorLayout的generateLayoutParams方法中直接调用构造方法进行初始化且在CoordinatorLayout.LayoutParams构造方法内部调用CoordinatorLayout的parseBehavior根据配置的Behavior的类名反射创建Behavior并赋值到mBehavior字段,然后再通过Behavior的onAttachedToLayoutParams方法Called when the Behavior has been attached to a LayoutParams instance.,所以除了保存CoordinatorLayout内的子控件的布局信息之外,还保存着对应的Behavior对象引用 mBehavior

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {   ...   public static class LayoutParams extends MarginLayoutParams {   ...        Behavior mBehavior;        LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {    super(context, attrs);    final TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CoordinatorLayout_Layout);    this.gravity = a.getInteger(R.styleable.CoordinatorLayout_Layout_android_layout_gravity,Gravity.NO_GRAVITY);    mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,View.NO_ID);    this.anchorGravity = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,Gravity.NO_GRAVITY);    this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,-1);    insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);    dodgeInsetEdges = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);    mBehaviorResolved = a.hasValue(R.styleable.CoordinatorLayout_Layout_layout_behavior);    if (mBehaviorResolved) {        mBehavior = parseBehavior(context, attrs, a.getString(R.styleable.CoordinatorLayout_Layout_layout_behavior));    }    a.recycle();    if (mBehavior != null) {        // If we have a Behavior, dispatch that it has been attached        mBehavior.onAttachedToLayoutParams(this);    }}}...}

三、CoordinatorLayout.Behavior

1、Behavior概述

CoordinatorLayout.Behavior是CoordinatorLayout的抽象泛型内部类,Behvaior 本身并不具备具体的业务功能,本质上就只是为了进行解耦的而封装的一个交互接口集合类,而CoordinatorLayout可以借助Behavior使得独立的子View可以产生交互,是因为CoordinatorLayout内部把事件分发至Behavior,让Behavior具有可以控制其他子View的效果了,也是CoordinatorLayout中核心的设计,也正是因为这个CoordinatorLayout.Behavior使得CoordinatorLayout中的直接子控件间可以产生联系,CoordinatorLayout.Behavior可以理解为事件分发的传送渠道(并不负责具体的任务),只是负责调用对应子View的相关方法parseBehavior方法根据配置的Behavior的类名反射创建Behavior并赋值到mBehavior字段,这是继承Behavior时必须要重写两个参数的构造方法的原因。通俗来说,Behavior 设置在谁身上,就可以通过Behavior来改变它对应的状态,观察者改变时,主题也跟着改变

2、CoordinatorLayout.Behavior核心方法

CoordinatorLayout.Behavior中最核心的方法只有三个:layoutDependsOn方法、onDependentViewChanged方法和onDependentViewRemoved方法,通过这三个方法就可以实现直接子View之间的交互,至于其他方法是处理到其他业务情况的时候,比如说嵌套滑动、重新布局等等。

2.1、layoutDependsOn方法

当进行Layout请求的时候就会触发执行,给CoordinatorLayout中的直接子控件设置了对应的Behavior之后,绘制时至少会执行一次,表示是否给配置了Behavior 的CoordinatorLayout直接子View 指定一个作为观察者角色的子View,返回true则表示主题角色child view的观察者是dependency view, 当观察者角色View状态(大小、位置)发生变化时,不管被观察View 的顺序怎样,被观察的View也可监听到并回调对应的方法;反之则两者之间没有建立联系。简而言之,这个方法的作用是配置了Behavior的主题子控件被符合哪些条件逻辑的子控件观察的(即作为主题的观察者之一)(Determine whether the supplied child view has another specific sibling view as a layout dependency)。

/** * 用于给配置了Behavior的View(主题) 指定一个观察者角色的View,返回true则dependency 为主题的观察者 * @param parent child和dependency view的外层父布局 * @param child 绑定behavior 的View   (观察者) * @param dependency   被观察者的view (主题) * @return 如果child 是观察者观察的View 返回true,否则返回false */@Overridepublic boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {    Log.e("------>","layoutDependsOn:child= "+child.getClass().getName()            +"  dependency= "+dependency.getClass().getName());    //依次判断dependency 是否符合条件,    if(dependency instanceof Button){        return true;    }    return super.layoutDependsOn(parent, child, dependency);}

比如在CoordinatorLayout里的某一个子View配置了Behavior之后,CoordinatorLayout在布局时会循环去查找其所有的直接子View,逐一去判断这个子View是否可以作为主题的观察者,所以当CoordinatorLayout里有2个直接子控件时layoutDependsOn方法会触发3次(触发时机是处理layout request时,包含第一次绘制时和View改变时触发),如果打印dependency view的名称,你会看到CoordinatorLayout里所有的直接子View都会打印一遍(除了主题View)。

Android 进阶——Material Design新控件之利用CoordinatorLayout协同多控件交互(七)_第1张图片

2.2、onDependentViewChanged方法

当且仅当Dependency View 状态(位置、大小等)改变时就会触发,返回true则表示Behavior改变了主题的状态,可能会执行多次,当然第一次绘制到布局上也算是状态改变时,所以自然也会触发,至于当监听到改变之后,如何去实现什么样的效果则由我们自己去开发实现。

/** * 当被观察者的View 状态(如:位置、大小)发生变化时就会触发执行 * @return true if the Behavior changed the child view's size or position, false otherwise */@Overridepublic boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {    Log.e("------>","onDependentViewChanged:child= "+child.getClass().getName()            +"  dependency= "+dependency.getClass().getName());    return super.onDependentViewChanged(parent, child, dependency);}

可以在View改变时及时得知,以下为部分运行日志:

Android 进阶——Material Design新控件之利用CoordinatorLayout协同多控件交互(七)_第2张图片

2.3、onDependentViewRemoved方法

当依赖的Dependency View被移除时触发回调(Respond to a child’s dependent view being removed.)

/** * Respond to a child's dependent view being removed. * @param parent the parent view of the given child * @param child the child view to manipulate * @param dependency the dependent view that has been removed */public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child,        @NonNull View dependency) {}

2.4、onInterceptTouchEvent方法设置是否拦截触摸事件

设置是否拦截触摸事件,返回true则表示当前Behavior会拦截触摸事件,不会分发到CoordinatorLayout内的子View下了。(Respond to CoordinatorLayout touch events before they are dispatched to child views.)

public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,       @NonNull MotionEvent ev) {   return false;}

2.5、onTouchEvent方法处理触摸事件

public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,       @NonNull MotionEvent ev) {   return false;}

2.6、onMeasureChild方法测量使用Behavior的View尺寸

/** * Called when the parent CoordinatorLayout is about to measure the given child view. * @param child the child to measure * @return true if the Behavior measured the child view, false if the CoordinatorLayout *         should perform its default measurement */public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child,        int parentWidthMeasureSpec, int widthUsed,        int parentHeightMeasureSpec, int heightUsed) {    return false;}

2.7、onLayoutChild方法重新布局使用Behavior的View

/** * Called when the parent CoordinatorLayout is about the lay out the given child view. * @return true if the Behavior performed layout of the child view, false to request default layout behavior */public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child,        int layoutDirection) {    return false;}

三、CoordinatorLayout的简单使用

1、引入CoordinatorLayout 依赖

dependencies {    def coordinatorlayout_version = "1.0.0"    implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version"}

2、继承CoordinatorLayout.Behavior实现自己的Behavior

  • 必须重写形参列表为(Context context, AttributeSet attrs)的构造方法
  • 重写layoutDependsOn方法
  • 重写onDependentViewChanged方法
/** * 此Behavior 将要被绑定到TextView上,用于改变TextView的UI效果 * @author : Crazy.Mo */public class TextColorBehavior extends CoordinatorLayout.Behavior<TextView> {//避免因为CoordinatorLayout的onLayout执行时,一开始就调用了Behavior的onLayout造成,还未开始交互就一直执行了onDependentViewChanged    private boolean isFirst=true;    /**     * 必须重写,否则肯定报错     * @param context     * @param attrs     */    public TextColorBehavior(Context context, AttributeSet attrs) {        super(context, attrs);    }    /**     * 这个方法是响应layout请求的,将至少调用一次,返回true时,child和dependency建立了依赖联系     * 即child是依赖dependency的     * @param parent     * @param child     * @param dependency     * @return     */    @Override    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {        //确定什么样的dependency 才有资格做child的观察者,逻辑由你自己决定        if(dependency instanceof Button){            return true;        }        return false;    }    /**     * 当dependency 的状态改变时就会触发     * @param parent     * @param child     * @param dependency     * @return     */    @Override    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {        ///当dependency 的状态改变时,就会触发这个方法执行,在这个方法你可以拿到主题View和观察者View,所以想要实现什么样的效果,完全由你决定,彻彻底底地解耦        if(!isFirst){            if(child.getId()==R.id.txt_demo2) {                child.setX(dependency.getX() + 20);                child.setY(dependency.getY() + 150);                child.setTextColor(Color.GREEN);            }else if(child.getId()==R.id.txt_demo){                child.setX(dependency.getX() + 20);                child.setY(dependency.getY() + 350);                child.setTextColor(Color.BLUE);            }//当然你可以同时改变dependency的状态//            dependency.setX(child.getX()+20);//            dependency.setY(child.getY()+150);           /// dependency.setBackgroundColor(Color.RED);        }        isFirst=false;        return true;    }}

在CoordinatorLayout中一般对于一个主题View来说一次只能设置绑定一个Behavior(且绑定的Behavior并不是被独享,其他主题View也可以绑定同一个Behavior),但可以被多个观察者所监听,而一个观察者可以同时监听多个主题View

package com.crazymo.coordinator;import android.content.Context;import android.graphics.Color;import android.util.AttributeSet;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.TextView;import androidx.annotation.NonNull;import androidx.coordinatorlayout.widget.CoordinatorLayout;/** * @author : Crazy.Mo */public class TextBehavior extends CoordinatorLayout.Behavior<TextView> {    private boolean isFirst=true;    /**     * 必须重写,否则肯定报错     * @param context     * @param attrs     */    public TextBehavior(Context context, AttributeSet attrs) {        super(context, attrs);    }    /**     * 这个方法是响应layout请求的,将至少调用一次,返回true时,child和dependency建立了依赖联系     * 即child是依赖dependency的     * @param parent     * @param child     * @param dependency     * @return     */    @Override    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {        if(dependency instanceof Button){            return true;        }        return false;    }    @Override    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull View dependency) {        if(!isFirst){            child.setX(dependency.getX()+20);            child.setY(dependency.getY()+250);            child.setTextColor(Color.RED);        }        isFirst=false;        return true;    }}

上面定义了两个Behavior。

3、使用CoordinatorLayout作为顶层布局,同时给对应的View配置Behavior使之变成主题View

使用CoordinatorLayout一定要给对应的主题View配置app:layout_behavior属性,否则就没有必要使用CoordinatorLayout。

<?xml version="1.0" encoding="utf-8"?><androidx.coordinatorlayout.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"    tools:context=".MainActivity">    <TextView        android:id="@+id/txt_demo"        app:layout_behavior=".TextColorBehavior"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="top"        android:text="我是主题View(被观察者)" />    <TextView        android:id="@+id/txt_demo2"        app:layout_behavior=".TextColorBehavior"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="left|center"        android:text="我是主题View2(被观察者)" />    <TextView        android:id="@+id/txt_demo3"        app:layout_behavior=".TextBehavior"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="right|center_horizontal"        android:text="我是主题View3(被观察者)" />    <Button        android:id="@+id/btn_demo"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="bottom"        android:text="观察者" />androidx.coordinatorlayout.widget.CoordinatorLayout>

4、在MainActivity中触发dependency View状态改变进行测试

此处模拟的是当dependency View状态改变时,其他主题View跟随者改变的简单效果,当观察者移动时主题TextView紧跟其下方移动。

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        findViewById(R.id.btn_demo).setOnTouchListener(new View.OnTouchListener() {            @Override            public boolean onTouch(View v, MotionEvent event) {                if(event.getAction()==MotionEvent.ACTION_MOVE){                    v.setX(event.getRawX()-v.getWidth()/2);                    v.setY(event.getRawY()-v.getHeight()/2);                }                return true;            }        });    }}

如下图所示当我按着观察者Button移动时候,三个主题TextView都显示在Button的下方且跟着移动:

通俗总结就是:CoordinatorLayout里的任何直接子View具有随时监听到对方的状态改变的能力

四、CoordinatorLayout的核心流程解析

未完待续,篇幅问题见下篇文章。

更多相关文章

  1. 收藏Android下bitmap内存限制OUT OF MEMORY的方法
  2. 【Android】Android SDK下载和更新失败的解决方法!!!
  3. 彻底解决Android 应用方法数不能超过65K的问题
  4. 系出名门Android(8) - 控件(View)之TextSwitcher, Gallery, Imag
  5. Android Wear 控件——WearableListView(附Demo)
  6. android 获取路径目录方法以及判断目录是否存在,创建目录
  7. 一个Demo让你掌握所有的android控件

随机推荐

  1. Android(安卓)8.0目录介绍
  2. Android(安卓)- 多线程 - AsyncTask
  3. #Android源代码#android:onClick属性的底
  4. 详解Android(安卓)TextView属性ellipsize
  5. 配置并使用Android支持的库
  6. android 传感器使用与开发----方向传感器
  7. Android(安卓)开发学习 HelloAndroid例子
  8. android解析xml文件的方式
  9. Google 告诉你 Android(安卓)4.0 的新功
  10. Android中外部程序调用方法总结