bux-git / MeterialDesignStudy

Design常用的新控件 DrawerLayout NavigationView AppbarLayout CollapsingToolbarLayout TextInputLayout SnackBar FloatingActionButton 学习 详细记录

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Design新控件

谷歌在推出Android5.0的同时推出了全新的设计Material Design,谷歌为了给我们提供更加规范的MD设计风格的控件,在2015年IO大会上推出了Design支持包,Design常用的新控件有下面几种。

目录

1

官方侧滑菜单DrawerLayout

imgs

一.概念
DrawerLayout其实是一个布局控件,跟LinearLayout等控件是一种东西,但是drawerLayout带有滑动的功能。只要按照drawerLayout的规定布局方式写完布局,就能有侧滑的效果

二.使用
DrawerLayout分为侧边菜单和主内容区两部分,侧边菜单可以根据手势展开与隐藏(drawerLayout自身特性),主内容区的内容可以随着菜单的点击而变化(这需要使用者自己实现)

 <android.support.v4.widget.DrawerLayout
        android:id="@+id/dl_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">


        <android.support.v7.widget.RecyclerView
            android:id="@+id/rl_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/holo_red_light">
        </android.support.v7.widget.RecyclerView>


        <android.support.design.widget.NavigationView
            android:id="@+id/ng_view"
            android:layout_width="200dp"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            app:headerLayout="@layout/main_header_layout"
            app:menu="@menu/main_nav_menu">
        </android.support.design.widget.NavigationView>
    </android.support.v4.widget.DrawerLayout>
其中:DrawerLayout最好为界面的根布局,官网是这样说的,否则可能会出现触摸事件被屏蔽的问题;
主内容区的布局代码要放在侧滑菜单布局的前面, 因为 XML 顺序意味着按 z 序(层叠顺序)排序,并且抽屉式导航栏必须位于内容顶部;
侧滑菜单部分的布局(NavigationView)必须设置layout_gravity属性,他表示侧滑菜单是在左边还是右边,而且如果不设置在打开关闭抽屉的时候会报错,
设置了layout_gravity="start/left"的视图才会被认为是侧滑菜单

三.DrawerLayout常用方法

/**
  * Adds the specified listener to the list of listeners that will be notified of drawer events.
  *将指定的监听添加到DrawerLayout监听列表中
  * @param listener Listener to notify when drawer events occur.
  * @see #removeDrawerListener(DrawerListener)
*/
addDrawerListener(@NonNull DrawerListener listener) 

//移除对应的监听
removeDrawerListener(DrawerListener)

//关闭指定的抽屉视图
closeDrawer()

//打开指定的抽屉视图
openDrawer()

检查给定的抽屉视图当前是否处于打开状态。 *被认为是“开放”的抽屉必须已经处于完全可见的状态。检查部分可见性使用情况isDrawerVisible()
isDrawerOpen()


       drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
            /**
             * 当抽屉被滑动的时候调用月此方法
             *
             * @param drawerView
             * @param slideOffset 表示滑动的幅度(0-1)
             */
            @Override
            public void onDrawerSlide(View drawerView, float slideOffset) {

            }

            /**
             * 当抽屉完全打开时调用
             * @param drawerView
             */
            @Override
            public void onDrawerOpened(View drawerView) {

            }

            /**
             * 当抽屉完全关闭时调用
             * @param drawerView
             */
            @Override
            public void onDrawerClosed(View drawerView) {

            }

            /**
             * 当抽屉运动状态改变的时候被调用
             * 状态值分别为 STATE_IDLE  0 抽屉处于闲置状态。没有动画正在进行中。处于打开或者关闭状态
             *  STATE_DRAGGING 1 表示用户当前正在拖动抽屉。
             *  STATE_SETTLING 2 表示抽屉滑动到了关闭或者打开的位置
             *  当用户滑动抽屉时 抽屉此时处于STATE_DRAGGING 滑动到刚好关闭或者打开后是STATE_SETTLING状态 随后状态会变为STATE_IDLE 闲置状态
             * @param newState
             */
            @Override
            public void onDrawerStateChanged(int newState) {
                Log.d(TAG, "onDrawerStateChanged: "+newState);
            }
        });

四.ActionBarDrawerToggle与ToolBar

ActionBarDrawerToggle实现了DrawerListener,所以他能做DrawerListener可以做的任何事情,同时他还能将drawerLayout的展开和隐藏与Toolbar的app 图标关联起来,
点击图标的时候还能展开或者隐藏菜单

将抽屉滑动与Toolbar图标关联
1. mToolbar = (Toolbar) findViewById(R.id.toolbar);
   setSupportActionBar(mToolbar);
2.  
 初始化ActionBarDrawerToggle
 ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawerLayout, mToolbar, R.string.open_string, R.string.close_string)

3.将ActionBarDrawerToggle添加到DrawerLayout监听列表中
  drawerLayout.addDrawerListener(toggle);
  
4.将 ActionBarDrawerToggle与Toolbar图标关联
  toggle.syncState();
  这样在抽屉打开关闭时 Toolbar图标将会变动,且点击图标时可以切换抽屉的打开关闭状态
  
  同时一下主题可以设置图标的颜色
    <style name="AppTheme">
          <item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
      </style>
  
      <style name="DrawerArrowStyle" parent="Widget.AppCompat.DrawerArrowToggle">
          <item name="spinBars">true</item>
          <item name="color">@android:color/holo_red_light</item>
      </style>

2

NavigationView

一.概念
NavigationView顾名思义是指导航菜单栏,一般配合DrawerLayout使用作为侧滑菜单栏
二.使用
NavigationView需要接收几个必要的参数、一个用于显示头部的布局app:headerLayout="@layout/main_header_layout"(可选) 以及用于建立导向选项的菜单app:menu="@menu/main_nav_menu",这些都设置完之后,你就只添加监听选中事件的listener就行了。
其中app:menu配置的是一个菜单文件

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:id="@+id/group1"
           android:checkableBehavior="single">
        <item
            android:id="@+id/app_bar"
            android:icon="@android:drawable/ic_delete"
            android:title="AppbarActivity"></item>
    </group>

    <group android:id="@+id/group2"
           android:checkableBehavior="single">
        <item
            android:icon="@android:drawable/ic_menu_save"
            android:title="Start"></item>
    </group>
<item android:id="@+id/sub1" android:title="sub item">
    <menu>
        <item
            android:icon="@android:drawable/ic_menu_save"
            android:title="Start"></item>
    </menu>
</item>
</menu>

其中:
1.group表示分组 checkableBehavior用来设置item选中模式 有3个值 表示选中状态 single单选 all多选 none 默认
item可以表示一个子项 也可在其中加入menu添加子菜单来实现带有头部的分组效果
每个group 和menu子菜单都会在顶部产生移到横线

2.设置item选中事件

     mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
                @Override
                public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                    item.setChecked(true);
                    drawerLayout.closeDrawer(Gravity.LEFT);
                    return true;
                }
            });

注意:这样只可以设置group中的item选中状态
item子菜单设置选中状态

        1.首先设置子菜单item android:checkable="true"
        2.public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                item.setChecked(true);
                 if (mPreMenuItem != null) mPreMenuItem.setChecked(false);
                 item.setChecked(true);
                  drawerLayout.closeDrawers();
                  mPreMenuItem = item;
                  return true;
         }

三.常用属性

app:itemIconTint="" 修改图标颜色
app:itemBackground="" item背景颜色
app:itemTextColor=""  item 文字颜色

3

AppBarLayout

imgs
一.概念

AppBarLayout继承自LinearLayout,布局方向为垂直方向。所以你可以把它当成垂直布局的LinearLayout来使用。
AppBarLayout是在LinearLayou上加了一些材料设计的概念,它可以让你定制当某个可滚动View的滚动手势发生变化时,其内部的子View实现何种动作   

解释:
1.AppBarLayout内部子View可以和一个可滚动的View的滑动事件产生关联,从而使AppBarLayout内部的子View执行相关联的滑动动作,
内部子View可以是Toolbar、任何View或者布局

注意:AppbarLayout 严重依赖于CoordinatorLayout,必须用于CoordinatorLayout 的直接子View,如果你将AppbarLayout 放在其他的ViewGroup 里面,那么它的这些功能是无效的

2.AppBarLayout如何与可滚动的View关联:

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    
    app:layout_behavior="@string/appbar_scrolling_view_behavior">
   <!--将你的内容放在这里-->
</android.support.v4.widget.NestedScrollView>

属性:app:layout_behavior="@string/appbar_scrolling_view_behavior"是CoordinatorLayout的布局属性,它能够将此View与AppBarLayout关联,指定Behavior的,appbar_scrolling_view_behavior对应的类的名称是:android.support.design.widget.AppBarLayout$ScrollingViewBehavior

3.AppBarLayout可以与那些View关联

3.1,根据概念首先这个View必须是可以滚动的如ScrollView RecyclerView等
3.2,还有一个就是此View必须实现NestedScrollingChild接口

所以ScrollView ListView GridView是不能直接和AppBarLayout联合使用的,需要自己实现NestedScrollingChild接口后才行
已经实现此接口的如:RecyclerView,NestedScrollView等

4.AppBarLayout内部子View如何执行动作

1.内部的子View通过在布局中加app:layout_scrollFlags设置执行的动作
layout_scrollFlagsyou 有如下几个值:

  • 1、 scroll ,子View 添加layout_scrollFlags属性 的值scroll 时,这个View将会随着可滚动View(如:NestedScrollView,NestedScrollView 来代替可滚动的View )一起滚动,就好像子View 是属于ScrollView的一部分一样。
  • 2、 enterAlways ,子View 添加layout_scrollFlags属性 的值有enterAlways 时, NestedScrollView 滑动时,子View将剥夺滑动事件先执行动作后 ,NestedScrollView 再滑动。 注意:要与scroll 搭配使用,否者是不能滑动的。
  • 3、 enterAlwaysCollapsed ,必须配合scroll|enterAlways 一起使用, enterAlwaysCollapsed 是对enterAlways 的补充, NestedScrollView 向上滑动和Scroll,enterAlways效果一样 ,向下滑动时 滑动View先下滑到最小高度 minHeight(最小高度)指定的,然后让NestedScrollView滑动到顶点后,滑动View再继续下滑到最大值。
  • 4、exitUntilCollapsed,必须配合scroll 当NestedScrollView向上滑动时,滑动View先响应滑动事件,滑动至最小高度,(也就是通过minHeight 设置的最小高度)后,就固定不动了,再把滑动事件交给 NestedScrollView 继续滑动。
  • 5、snap,意思是:会给滑动view的滑动事件添加一个自动滚动属性,
    在滚动结束后,view滑动到部分可见时,如果隐藏区域比显示区域大则它将滚动离开屏幕,显示区域比影藏区域大,它将自动滚动到完全显示。必须配合scroll使用

5.app:layout_scrollInterpolator属性指定滚动动画效果的插值器.

6.AppBarLayout常用方法

  • addOnOffsetChangedListener 当AppbarLayout 的偏移发生改变的时候回调,也就是子View滑动。
  • getTotalScrollRange 返回AppbarLayout 所有子View的滑动范围
  • removeOnOffsetChangedListener 移除监听器
  • setExpanded (boolean expanded, boolean animate)设置AppbarLayout 是展开状态还是折叠状态,animate 参数控制切换到新的状态时是否需要动画
  • setExpanded (boolean expanded) 设置AppbarLayout 是展开状态还是折叠状态,默认有动画

特殊场景

禁止单独触摸AppBarLayout而滑动展开AppBarLayout

CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
 AppBarLayout.Behavior behavior = (AppBarLayout.Behavior) params.getBehavior();
 
 behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
     @Override
     public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
         return false;
     }
 });
 通过总是返回 false ,您滚动 view 不会再由 ABL 控制。
 
 注︰之前调用此你应该检查, ViewCompat.isLaidOut(appBarLayout) ,否则为 params.getBehavior() 将返回 null。

4

CollapsingToolbarLayout

[images](https://github.com/bux-git/MeterialDesignStudy/raw/master/imges/collspasingtoolbarlayout01.gif1) 一.概念
CollapsingToolbarLayout是用来对Toolbar进行再次包装的ViewGroup,主要是用于实现折叠的App Bar效果。
它需要作为AppBarLayout的直接子View,并且需要作为AppBarLayout的关联滑动View

CollapsingToolbarLayout包含以下功能:

  • 1.Collapsing title(折叠标题) 当布局全部可见的时候,title 是最大的,当布局开始滑出屏幕,title 将变得越来越小,你可以通过setTitle(CharSequence) 来设置要显示的标题

    注意:Toolbar 和CollapsingToolbarLayout 同时设置了title时,不会显示Toolbartitle而是显示CollapsingToolbarLayout 的title,如果要显示Toolbar 的title,你可一在代码中添加如下代码:collapsingToolbarLayout.setTitle("");

  • 2.Content scrim(内容纱布) 当CollapsingToolbarLayout滑动到一个确定的阀值时将显示或者隐藏内容纱布,可以通过setContentScrim(Drawable)来设置纱布的图片。

提醒:纱布可以是图片也可以是颜色色值,如果要显示颜色,在xml 布局文件中用contentScrim属性添加,代码如下: app:contentScrim="@color/colorPrimary"

  • 3.Status bar scrim(状态栏纱布) 当CollapsingToolbarLayout滑动到一个确定的阀值时,状态栏显示或隐藏纱布,你可以通过setStatusBarScrim(Drawable)来设置纱布图片。

注意:同内容纱布一样,状态栏纱布可以是图片也可以是一个颜色值,如果要显示颜色值,在xml 中用statusBarScrim 属性指定。

  • 4.Parallax scrolling children(有视差地滚动子View) 让CollapsingToolbarLayout 的子View 可以有视差的滚动,需要在xml中用 添加如下代码:

app:layout_collapseMode="parallax"

  • 5.Pinned position children(固定子View的位置)子View可以固定在全局空间内,这对于实现了折叠并且允许通过滚动布局来固定Toolbar 这种情况非常有用。在xml 中将collapseMode设为pin,代码如下:

app:layout_collapseMode="pin"

  • 其他属性:

    collapsedTitleGravity:折叠时Toolbar标题位置
    expandedTitleGravity:展开时Toolbar标题位置
    titleEnabled:滑动时,设置是否应该显示自己的标题。标题将根据滚动偏移而收缩和增长。默认为true

CollapsingToolbarLayout使用时

1.作为AppBarLayout的子View并设置滑动关联动作如: app:layout_scrollFlags="scroll|exitUntilCollapsed"
2.根据需求设置自身标题等一些相关属性
3.添加Toolbar和其他子View

5

TextInputLayout

imgs 1.概念
TextInputLayout 将EditText包裹起来能够辅助EditText实现一些如hint 以浮动标签的形式显示出来,同时可以通过setErrorEnabled(boolean)和setError(CharSequence)来显示错误信息等
每一个TextInputLayout中只能有一个EditText

XML属性&常用方法

  • counterEnabled 对应方法 setCounterEnabled(boolean)

    用于设置字符计数器的显示与隐藏,会在布局右下角显示输入字符的进度:1/10这样

  • counterMaxLength 对应方法 setCounterMaxLength(int)

    设置字符计数器的最大长度。(仅用于设置计数器最大值,并不影响文本实际能输入的最大长度)

  • errorEnabled 对应方法 setErrorEnabled(boolean)

    用于设置错误提示是否显示

  • hint 对应方法 setHint(CharSequence)

    设置输入框的提示语

  • hintAnimationEnabled 对应方法 setHintAnimationEnabled(boolean)

    开启或关闭hint浮动成标签的动画效果

  • hint 对应方法 setHint(CharSequence)

    设置输入框的提示语

  • hintEnabled 对应方法 setHintEnabled(boolean)

    开启或关闭hint浮动的功能,设为false的话就和之前的EditText一样,在输入文字后,提示语就消失了

  • hintTextAppearance 对应方法 setHintTextAppearance(int)

    设置hint的style,字体颜色,字体大小等,可引用系统自带的也可以自定义。若要使用请统一使用,以保证APP体验的统一性

当文本输入类型为密码时,系统提供了一个开关来控制密码是否可见(默认为眼睛👁)。此为design包24.0.2新提供的功能。

  • passwordToggleEnabled 对应方法 setPasswordVisibilityToggleEnabled(boolean)

    控制密码可见开关是否启用。设为false则该功能不启用,密码输入框右侧也没有控制密码可见与否的开关

  • passwordToggleDrawable 对应方法 setPasswordVisibilityToggleDrawable(Drawable)

    设置密码可见开关的图标。通常我们会在不同的情况下设定不同的图标,可通过自定义一个selector,根据“state_checked”属性来控制图标的切换

  • passwordToggleTint 对应方法 setPasswordVisibilityToggleTintList(ColorStateList)

    控制密码可见开关图标的颜色。在开启或关闭的状态下我们可以设定不同的颜色,可通过自定义一个color的selector,根据“state_checked”和“state_selected”属性来控制颜色的切换

  • hintEnabled 对应方法 setHintEnabled(boolean)

    开启或关闭hint浮动的功能,设为false的话就和之前的EditText一样,在输入文字后,提示语就消失了

TextInputLayout详解

6

SnackBar

一.概念

Snackbar 是一种针对操作的轻量级反馈机制,常以一个小的弹出框的形式,出现在手机屏幕下方或者桌面左下方。它们出现在屏幕所有层的最上方,包括浮动操作按钮。 它们会在超时或者用户在屏幕其他地方触摸之后自动消失。Snackbar 可以在屏幕上滑动关闭。当它们出现时,不会阻碍用户在屏幕上的输入,并且也不支持输入。屏幕上同时最多只能现实一个 Snackbar。

综上所述: 1.SnackBar可以自动消失,也可以手动取消
2.SnackBar可以通过setAction()来与用户进行交互
3.通过CallBack我们可以获取SnackBar的状态

二.使用方法

Snackbar用法:Snackbarmake(@NonNull View view, @NonNull CharSequence text,@Duration int duration).show();
View:

SnackBar显示,它需要有一个View来承载SnackBar会遍历整个View Tree来找到一个合适的View承载SnackBar的View,
如果你想要实现上面的动画交互效果的话最好是在布局中包括CoordinatorLayout,假如你的布局中不包括CoordinatorLayout是不会有动画效果的

text:

SnackBar显示文字

duration

有三种状态:
Snackbar.LENGTH_SHORT// 短时间显示,然后自动取消
Snackbar.LENGTH_LONG// 长时间显示,然后自动取消
Snackbar.LENGTH_INDEFINITE// 不消失显示,除非手动取消

make方法源码:

public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
        @Duration int duration) {
        
    Snackbar snackbar = new Snackbar(findSuitableParent(view));
    snackbar.setText(text);
    snackbar.setDuration(duration);
    return snackbar;
    
}

其实这里面的重点就是Snackbarsnackbar =newSnackbar(findSuitableParent(view));
我们可以看到我们传入的view经过了
findSuitableParent()方法的包装。

这个方法主要的作用是:

1.当传入的View不为空时,如果我们在布局中发现了CoordinatorLayout布局,那么返回的View就是CoordinatorLayout;

2.如果没有CoordinatorLayout布局,我们就先找到一个id为android.R.id.content的FrameLayout(这个布局是最底层的根布局),
将View设置为该FrameLayout;

3.其他情况下就使用View的Parent布局一直到这个View不为空。

            //获取实例
            Snackbar snackbar= Snackbar.make(mBtnShow,"测试",Snackbar.LENGTH_LONG);
            //设置右侧点击事件
            snackbar.setAction("编辑", new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(SnackBarActivity.this,"点击",Toast.LENGTH_SHORT).show();
                }
            });
            //设置点击文字颜色
            snackbar.setActionTextColor(getResources().getColor(R.color.black));
            //设置SnackBar 背景颜色
            snackbar.getView().setBackgroundColor(getResources().getColor(R.color.colorAccent));
            //添加显示与消失监听
            snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
                @Override
                public void onDismissed(Snackbar transientBottomBar, int event) {
                    super.onDismissed(transientBottomBar, event);
                }

                @Override
                public void onShown(Snackbar transientBottomBar) {
                    super.onShown(transientBottomBar);
                }
            });
            //显示SnackBar
            snackbar.show();

Snackbars 与 Toasts
SnackBar使用详解

7

FloatingActionButton

浅谈FloatingActionButton(悬浮按钮)
浮动操作按钮详解

8

CoordinatorLayout

一.概念
译为:协调者布局

1.首先可以理解Coordinatorlayout是一个FrameLayout升级版本

2.重要功能:CoordinatorLayout可以用来协调其子view之间动作的交互
如:协调滑动控件和AppBarLayout之间的交互等等,
CoordinatorLayout 实现子View之间的交互是靠Behavior来实现的

二.使用
CoordinatorLayout子View之间是如何协调的:

1.根据前面与:
3.AppbarLayout
4.CollapsingToolbarLayout
6.SnackBar
6.FloatingActionButton
等使用的情况,子View之间相互协调是通过CoordinatorLayout的布局属性app:layout_behavior来设置的
layout_behavior 属性定义了这个View如何和其他View互相交互的行为, 其值填写的是一个class的名字(全称带包名) 这个值指定的类必须是 CoordinatorLayout.Behavior 的子类, 我们也可以自定义一个该类继承于它, 以此来写自己想要的交互效果.

三.Behavior

1.概述

CoordinatorLayout的诸多功能全部依赖与CoordinatorLayout.Behavior来实现
通过为CoordinatorLayout的直接子view设置一个Behavior,CoordinatorLayout会遍历一遍自己的直接子View,
一个一个的调用子view中的Behavio就可以拦截touch events, window insets, measurement, layout, 和 nested scrolling等动作。
Design Library大量利用了Behaviors来实现你所看到的功能

2.Behavior的创建

2.1、创建behavior,需要继承 CoordinatorLayout.Behavior 或其子View

public class FollowUpDownBehavior extends CoordinatorLayout.Behavior {

  public FollowUpDownBehavior(Context context, AttributeSet attrs) {
      super(context, attrs);

    }
}

这样就可以将这个Behavior设置给任何View,如果只想设置给某一些特定类型的View则可以传入泛型如:

FollowUpDownBehavior extends CoordinatorLayout.Behavior<Button>

3.设置Behavior
设置Behavior一共有3种方式:

3.1 在代码中设置Behavior

CoordinatorLayout.LayoutParam中可以存储Behavior布局属性,所以在代码中:

    FollowUpDownBehavior behavior = new FollowUpDownBehavior();
    CoordinatorLayout.LayoutParams layoutParams= (CoordinatorLayout.LayoutParams) view.getLayoutParams();
    view.setLayoutParams(layoutParams);

3.2 在XML中设置

在xml布局中直接设置属性值

<View android:layout_width="50dp"
      android:layout_height="20dp"
      android:background="@color/black"
      app:layout_behavior="com.dqr.www.meterialdesignstudy.coordinatorlayout.behavior.FollowUpDownBehavior"
      app:target="@id/moveView"></View>

在XML设置属性,初始化Behavior时,是使用的FollowUpDownBehavior(Context context, AttributeSet attrs)
两个参数的构造函数,有传入AttributeSet所以可以自定义一些属性然后在xml中设置,Behavior中接收并使用,
如app:target属性设置一个目标ID

3.3 在View类上添加默认的Behavior

在自定义View时我们希望这个View自带一个Behavior,而不需要去另外设置,我们可以在自定义View类上设置
注释:

@CoordinatorLayout.DefaultBehavior(BtnTestBehavior.class)
public class TempView extends View {
}

Behavior的功能

1.拦截Touch Events

CoordinatorLayout会在他的onInterceptTouchEvent()中将事件MotionEvent传递到子View的
Behavior.onInterceptTouchEvent()中,让Behavior也可以拦截触摸事件,
如果Behavior.onInterceptTouchEvent()返回true,则Behavior.onTouchEvent()将会收到
后续触摸事件,而View将不会收到后续的触摸事件

CoordinatorLayout的事件分发过程

首先ViewGroup/View 的事件分派, 事件分派是有两个过程的: 深入理解CoordinatorLayout原文

捕获过程:从根元素到子元素依次调用onInterceptTouchEvent,检测是否有View要拦截触摸事件 如果有View拦截了立即进入冒泡过程,否则一直传递到最末尾的元素再进入到冒泡过程.

冒泡过程:从底层往上冒泡,一次调用onTouchEvent,如果有View消耗了事件,则不再继续向上传递,否则一直传递 到根元素.

CoordinatorLayout事件分发

CoordinatorLayout在他的onInterceptTouchEvent中去遍历所有的子View,并调用子View的Behavior.onInterceptTouchEvent方法 如果在Behavior.onInterceptTouchEvent方法中返回了true拦截了该事件,则该Behavior就可以在onTouchEvent
中处理触摸事件,而这个Behavior对应的View将不会收到触摸时间

这样的设置可以使得处理例如手势的逻辑可以完全从具体的某个View解耦出来,可以给不同的View设置相同的
Behavior来获得处理相同手势的功能,代码复用率极高.

2.子View之间的依赖

Behaviors的强大之处在于在View之间建立依赖关系-当另一个View改变的时候,你的Behavior会得到一个callback,根据外部条件改变它的功能

在 CoordinatorLayout.LayoutParams 中定义了一个View是否依赖( dependsOn ) 另一个View:

boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency == mAnchorDirectChild
            || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}

Behaviors依赖于View有两种形式:

1.在Behavior.layoutDependsOn()中返回true
2.使用CoordinatorLayout的layout_anchor 属性之时。它和layout_anchorGravity 属性结合,可以让你有效的把两个View捆绑在一起。比如,你可以把一个FloatingActionButton锚定在一个AppBarLayout上,那么如果AppBarLayout滚动出屏幕,FloatingActionButton.Behavior将使用隐式的依赖去隐藏FAB

View之间关联之后,当依赖View被移除的时候,将会回调Behavior.onDependentViewRemoved() 当依赖的View发生变化的时候(比如:调整大小或者重置自己的position),得到回调 onDependentViewChanged()
我们可以在这2个方法中处理关于自身View的一些事情

这个把View绑定在一起的能力正是Design Library那些酷炫功能的工作原理
-以FloatingActionButton与Snackbar之间的交互为例。FAB的 Behavior依赖于被添加到CoordinatorLayout的Snackbar,. 然后它使用onDependentViewChanged() callback来将FAB向上移动,以避免和Snackbar重叠。

CoordinatorLayout的三个子View A B C之间的依赖可以有如下几种:

  • 可以 多个View同时依赖同一个View: B C 同时依赖A
  • 被依赖的View可以继续依赖其他View A 依赖B B 依赖C
  • 也可以单独依赖 A 依赖 B
  • 但是不能循环依赖 :A 依赖B B 依赖C C依赖A

3.嵌套滚动
NestedScrolling 是Android提供的一套父View和子View交互滑动机制.
完成这样的交互 需要父View实现NestedScrollingParent接口,子View实现NestedScrollingChild接口
同时系统也提供了2辅助类来帮助处理子View和父View交互的大部分逻辑:

NestedScrollingParent--->NestedScrollingParentHelper
NestedScrollingChild--->NestedScrollingChildHelper

NestedScrollingChild & NestedScrollingChildHelper

public interface NestedScrollingChild {  
  
    /** 
     * 设置嵌套滑动是否可用 
     * 
     * @param enabled 
     */  
    public void setNestedScrollingEnabled(boolean enabled);  
  
    /** 
     * 嵌套滑动是否可用 
     * 
     * @return 
     */  
    public boolean isNestedScrollingEnabled();  
  
    /** 
     * 开启整个嵌套滑动流程,通知父View一起处理TouchEvent事件
     * 
     * @param axes 表示方向 有一下两种值 
     *             ViewCompat.SCROLL_AXIS_HORIZONTAL 水平方向滑动 
     *             ViewCompat.SCROLL_AXIS_VERTICAL 纵向滑动 
     * @return 父View是否支持嵌套滑动
     */  
    public boolean startNestedScroll(int axes);  
  
    /** 
     * 结束嵌套滑动流程
     */  
    public void stopNestedScroll();  
  
    /** 
     * 是否有父View 支持 嵌套滑动,  会一层层的往上寻找父View 
     * @return 
     */  
    public boolean hasNestedScrollingParent();  
  
    /** 
     * 向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
     * 如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
     * 这个函数一般在子view处理scroll后调用。
       
     * @param dxConsumed x轴上 被父View消费的距离 
     * @param dyConsumed y轴上 被父View消费的距离 
     * @param dxUnconsumed x轴上 未被消费的距离 
     * @param dyUnconsumed y轴上 未被消费的距离 
     * @param offsetInWindow view 的移动距离 
     * @return 
     */  
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,  
                                        int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);  
  
    /** 
     * 在子View的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),
     * 调用该方法将子View此次滑动的距离通知给父View做处理
     * @param dx x 轴上滑动的距离, 相对于上一次事件, 不是相对于 down事件的 那个距离 
     * @param dy y 轴上滑动的距离 
     * @param consumed 数组 输出参数,返回父View消耗掉的滑动长度
     *  如果传入不为null,cosumed[0]表示父View在X方向上消费掉的Scroll距离
     *  cosumes[1]表示父View在Y方向上消费掉的scroll距离
     *  如果这两个值不为0,则子view需要对滚动的量进行一些修正
     * @param offsetInWindow   支持嵌套滑动到额父View 消费 滑动事件后 导致 本 View 的移动距离 
     * @return 如果父View 接收了滚动参数,并进行了消费,则返回true 否则为false
     这个函数一般在子view处理scroll前调用
     */  
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);  
  
    /** 
     * 
     * @param velocityX x 轴上的滑动速度 
     * @param velocityY y 轴上的滑动速度 
     * @param consumed 是否被消费 
     * @return 
     */  
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);  
  
    /** 
     * 
     * @param velocityX x 轴上的滑动速度 
     * @param velocityY y 轴上的滑动速度 
     * @return 
     */  
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);  
} 

使用:

public class Child extends ViewGroup implements NestedScrollingChild {  
  
    private NestedScrollingChildHelper mNestedScrollingChildHelper;  
  
    public Child(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);  
    }  
  
    @Override  
    public void setNestedScrollingEnabled(boolean enabled) {  
        mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);  
    }  
  
    @Override  
    public boolean isNestedScrollingEnabled() {  
        return mNestedScrollingChildHelper.isNestedScrollingEnabled();  
    }  
  
    @Override  
    public boolean startNestedScroll(int axes) {  
        return mNestedScrollingChildHelper.startNestedScroll(axes);  
    }  
  
    @Override  
    public void stopNestedScroll() {  
        mNestedScrollingChildHelper.stopNestedScroll();  
    }  
  
    @Override  
    public boolean hasNestedScrollingParent() {  
        return mNestedScrollingChildHelper.hasNestedScrollingParent();  
    }  
  
    @Override  
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {  
        return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);  
    }  
  
    @Override  
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {  
        return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);  
    }  
  
    @Override  
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {  
        return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);  
    }  
  
    @Override  
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {  
        return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);  
    }  
    
        /** 
         *重写这个方法或者onInterceptTouchEvent
         *在这里面开启嵌套滑动相关方法并处理相关逻辑
         *具体可以参考现有的RecyclerView NestedScrollView 等
         *
         */  
       @Override
        public boolean onTouchEvent(MotionEvent e) { 
            1.startNestedScroll(nestedScrollAxis);
            2.dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset);
            3.stopNestedScroll();
         
            return super.onTouchEvent(e);
        }
    
}  

NestedScrollingParent & NestedScrollingParentHelper

 public interface NestedScrollingParent {
     /**
      * 当子view的调用NestedScrollingChild的方法startNestedScroll时,会调用该方法 
      * 该方法决定了当前控件是否能接收到其内部View(直接子View或者子View的子View)滑动时的参数
      * @param child ViewParent 的直接子View 该View 或者是他的子View实现了NestedScrollingChild
      * @param target 实现了NestedScrollingChild的View (在这里如果不涉及多层嵌套的话,child和target)
      * @param nestedScrollAxes 嵌套滚动的滚动方向 Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
      *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
      * @return 是否接受此次嵌套滑动
      */
     public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
 
     /**
      * 
      *如果onStartNestedScroll方法返回true,之后就会调用该方法.它是让嵌套滚动在开始滚动之前,让布局容器(viewGroup)或者它的父类执行一些配置的初始化
      * @param child Direct child of this ViewParent containing target
      * @param target View that initiated the nested scroll
      * @param nestedScrollAxes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
      *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
      * @see #onStartNestedScroll(View, View, int)
      * @see #onStopNestedScroll(View)
      */
     public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
 
     /**
      * 当子view调用stopNestedScroll时会调用该方法,停止滚动
      * React to a nested scroll operation ending.
      *
      * <p>Perform cleanup after a nested scrolling operation.
      * This method will be called when a nested scroll stops, for example when a nested touch
      * scroll ends with a {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL} event.
      * Implementations of this method should always call their superclass's implementation of this
      * method if one is present.</p>
      *
      * @param target View that initiated the nested scroll
      */
     public void onStopNestedScroll(View target);
 
     /**
      * 当子view调用dispatchNestedScroll方法时,会调用该方法
      * @param target 实现了NestedScrollingChild的View
      * @param dxConsumed 表示target已经消费的x方向的距离
      * @param dyConsumed 表示target已经消费的y方向的距离
      * @param dxUnconsumed 表示x方向剩下的滑动距离 
      * @param dyUnconsumed 表示y方向剩下的滑动距离 
      */
     public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
             int dxUnconsumed, int dyUnconsumed);
 
     /**
      * 
      *consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离. 
       当子view调用dispatchNestedPreScroll方法是,会调用该方法
      * @param target 实现了NestedScrollingChild的View
      * @param dx 表示target本次滚动产生的x方向的滚动总距离 
      * @param dy 表示target本次滚动产生的y方向的滚动总距离 
      * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
      */
     public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
 
     /**
      * Request a fling from a nested scroll.
      *
      * <p>This method signifies that a nested scrolling child has detected suitable conditions
      * for a fling. Generally this means that a touch scroll has ended with a
      * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
      * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
      * along a scrollable axis.</p>
      *
      * <p>If a nested scrolling child view would normally fling but it is at the edge of
      * its own content, it can use this method to delegate the fling to its nested scrolling
      * parent instead. The parent may optionally consume the fling or observe a child fling.</p>
      *
      * @param target View that initiated the nested scroll
      * @param velocityX Horizontal velocity in pixels per second
      * @param velocityY Vertical velocity in pixels per second
      * @param consumed true if the child consumed the fling, false otherwise
      * @return true if this parent consumed or otherwise reacted to the fling
      */
     public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
 
     /**
      * React to a nested fling before the target view consumes it.
      *
      * <p>This method siginfies that a nested scrolling child has detected a fling with the given
      * velocity along each axis. Generally this means that a touch scroll has ended with a
      * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
      * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
      * along a scrollable axis.</p>
      *
      * <p>If a nested scrolling parent is consuming motion as part of a
      * {@link #onNestedPreScroll(View, int, int, int[]) pre-scroll}, it may be appropriate for
      * it to also consume the pre-fling to complete that same motion. By returning
      * <code>true</code> from this method, the parent indicates that the child should not
      * fling its own internal content as well.</p>
      *
      * @param target View that initiated the nested scroll
      * @param velocityX Horizontal velocity in pixels per second
      * @param velocityY Vertical velocity in pixels per second
      * @return true if this parent consumed the fling ahead of the target view
      */
     public boolean onNestedPreFling(View target, float velocityX, float velocityY);
 
     /**
      * Return the current axes of nested scrolling for this NestedScrollingParent.
      *
      * <p>A NestedScrollingParent returning something other than {@link ViewCompat#SCROLL_AXIS_NONE}
      * is currently acting as a nested scrolling parent for one or more descendant views in
      * the hierarchy.</p>
      *
      * @return Flags indicating the current axes of nested scrolling
      * @see ViewCompat#SCROLL_AXIS_HORIZONTAL
      * @see ViewCompat#SCROLL_AXIS_VERTICAL
      * @see ViewCompat#SCROLL_AXIS_NONE
      */
     public int getNestedScrollAxes();
 }

整个嵌套滑动的流程child对应parent:

子view 父view
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll
未完

自定义Behavior的艺术探索-仿UC浏览器主页
Android嵌套滑动机制(NestedScrolling)
4.拦截Window Insets
5.拦截Measurement 和 layout
拦截一切的CoordinatorLayout Behavior

学习资料

Material Design之 AppbarLayout 开发实践总结
玩转AppBarLayout,更酷炫的顶部栏
CoordinatorLayout, AppBarLayout, CollapsingToolbarLayout使用
深入理解CoordinatorLayout
自定义Behavior的艺术探索-仿UC浏览器主页
一个神奇的控件——Android CoordinatorLayout与Behavior使用指南
CoordinatorLayout的使用如此简单
拦截一切的CoordinatorLayout Behavior

回到顶部

About

Design常用的新控件 DrawerLayout NavigationView AppbarLayout CollapsingToolbarLayout TextInputLayout SnackBar FloatingActionButton 学习 详细记录


Languages

Language:Java 100.0%