GitHub 地址已更新: unixzii / android-FancyBehaviorDemo

CoordinatorLayout 是 Google 在 Design Support 包中提供的一个十分强大的布局视图,它本质是一个 FrameLayout,然而它允许开发者通过制定 Behavior 从而实现各种复杂的 UI 效果。

本文就通过一个具体的例子来讲解一下 Behavior 的开发思路,首先我们看效果(GIF 图效果一般,大家就看看大概意思吧):

效果图

我们先归纳一下整个效果的细节:

  • 界面分为上下两部分,上部分随列表滑动而折叠与展开;
  • 头部视图背景随折叠状态而缩放和渐变;
  • 浮动搜索框随折叠状态改变位置和 margins;
  • 滑动结束前会根据滑动速度动画到相应的状态:
    • 如果速度达到一定阈值,则按速度方向切换状态
    • 如果速度未达到阈值,则切换到距离当前状态最近的状态;

主要的细节就是这些,下面我们来一步步实现它!

编写布局文件

首先我们将所有的控件在 xml 写好,由于是 Demo,我这里就用一些很简单的控件了。

activity_main.xml:

 <?xml version="1.0" encoding="utf-8"?>
<android.support.design.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"
android:fitsSystemWindows="false"
android:background="#fff"
tools:context="com.example.cyandev.androidplayground.ScrollingActivity"> <ImageView
android:id="@+id/scrolling_header"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:background="@drawable/bg_header" /> <LinearLayout
android:id="@+id/edit_search"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/colorInitFloatBackground"
app:layout_behavior="@string/header_float_behavior">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:textColor="#90000000"
android:text="搜索关键字" />
</LinearLayout> <android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
app:layout_behavior="@string/header_scrolling_behavior"
app:layoutManager="LinearLayoutManager" /> </android.support.design.widget.CoordinatorLayout>

这里需要注意的是 CoordinatorLayout 子视图的层级关系,如果想在子视图中使用 Behavior 进行控制,那么这个子视图一定是 CoordinatorLayout 的直接孩子,间接子视图是不具有 behavior 属性的,原因当然也很简单,behavior 是 LayoutParams 的一个属性,而间接子视图的 LayoutParams 根本不是 CoordinatorLayout 类型的。

通过分解整个效果,我们可以将 Behavior 分为两个,分别应用于 RecyclerView (或者其他支持 Nested Scrolling 的滚动视图)和搜索框。

Behavior 基本概念

不要其被表面吓到了,Behavior 实际就是将一些布局的过程以及 Nested Scrolling 的过程暴露了出来,利用代理和组合模式,可以让开发者为 CoordinatorLayout 添加各种效果插件。

依赖视图

一个 Behavior 能够将指定的视图作为一个依赖项,并且监听这个依赖项的一切布局信息,一旦依赖项发生变化,Behavior 就可以做出适当的响应。很简单的例子就是 FABSnackBar 的联动,具体表现就是 FAB 会随 SnackBar 的弹出而上移,从而不会被 SnackBar 遮挡,这就是依赖视图的最简单的一个用法。

Nested Scrolling

这是 Google 开发的一种全新嵌套滚动方案,由 NestedScrollingParentNestedScrollingChild 组成,一般来讲我们都会围绕  NestedScrollingParent 来进行开发,而 NestedScrollingChild 相比来说较为复杂,本文也不赘述其具体用法了。NestedScrollingParent(下文简称 NSP) 和 NestedScrollingChild(下文简称 NSC) 有一组相互配对的事件方法,NSC 负责派发这些方法到 NSPNSP 可以对这些方法做出响应。同时 Google 也提供了一组 Helper 类来帮助开发者使用 NSPNSC,其中 NestedScrollingParentHelper 较为简单,仅是记录一下滚动的方向。对于 Nested Scrolling 的具体用法,我在下文中会详细讲解。

案例 Behavior 实现思路

我们最终需要实现两个 Behavior 类: HeaderScrollingBehavior 负责协调 RecyclerView 与 Header View 的关系,同时它依赖于 Header View,因为它要根据 Header View 的位移调整自己的位置。 HeaderFloatBehavior 负责协调搜索框与 Header View 的关系,也是依赖于 Header View,相对比较简单。

可以看到,整个视图体系都是围绕 Header View 展开的,Recycler View 通过 Nested Scrolling 机制调整 Header View 的位置,进而因 Header View 的改变而影响自身的位置。搜索框也是随 Header View 的位置变化而改变自己的位置、大小与背景颜色,这里只需要依赖视图这一个概念就可以完成。

实现 HeaderScrollingBehavior

首先继承自 Behavior,这是一个范型类,范型类型为被 Behavior 控制的视图类型:

 public class HeaderScrollingBehavior extends CoordinatorLayout.Behavior<RecyclerView> {

     private boolean isExpanded = false;
private boolean isScrolling = false; private WeakReference<View> dependentView;
private Scroller scroller;
private Handler handler; public HeaderScrollingBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
scroller = new Scroller(context);
handler = new Handler();
} ... }

解释一下这几个实例变量的作用,Scroller 用来实现用户释放手指后的滑动动画,Handler 用来驱动 Scroller 的运行,而  dependentView 是依赖视图的一个弱引用,方便我们后面的操作。剩下的是几个状态变量,不多解释了。

我们先看这几个方法:

 @Override
public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
if (dependency != null && dependency.getId() == R.id.scrolling_header) {
dependentView = new WeakReference<>(dependency);
return true;
}
return false;
}

负责查询该 Behavior 是否依赖于某个视图,我们在这里判读视图是否为 Header View,如果是则返回 true,那么之后其他操作就会围绕这个依赖视图而进行了。

 @Override
public boolean onLayoutChild(CoordinatorLayout parent, RecyclerView child, int layoutDirection) {
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
if (lp.height == CoordinatorLayout.LayoutParams.MATCH_PARENT) {
child.layout(0, 0, parent.getWidth(), (int) (parent.getHeight() - getDependentViewCollapsedHeight()));
return true;
}
return super.onLayoutChild(parent, child, layoutDirection);
}

负责对被 Behavior 控制的视图进行布局,就是将 ViewGrouponLayout 针对该视图的部分抽出来给 Behavior 处理。我们判断一下如果目标视图高度要填充父视图,我们就自己将其高度减去 Header View 折叠后的高度。为什么要这么做呢?因为 CoodinatorLayout 就是一个 FrameLayout,不像 LinearLayout 一样能自动分配各个 View 的高度,因此我们要自己实现大小控制。

 @Override
public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency)
{
Resources resources = getDependentView().getResources();
final float progress = 1.f -
Math.abs(dependency.getTranslationY() / (dependency.getHeight() - resources.getDimension(R.dimen.collapsed_header_height))); child.setTranslationY(dependency.getHeight() + dependency.getTranslationY()); float scale = 1 + 0.4f * (1.f - progress);
dependency.setScaleX(scale);
dependency.setScaleY(scale); dependency.setAlpha(progress); return true;
}

这段就是根据依赖视图进行调整的方法,当依赖视图发生变化时,这个方法就会被调用。这里我把相关的尺寸数据写到了 dimens.xml 中,通过当前依赖视图的位移,计算出一个位移因数(取值 0 - 1),对应到依赖视图的缩放和透明度。

在这个例子中,依赖视图的属性影响到了依赖视图自己的属性,这也是可以的,因为我们主要依赖的就是 translateY 这个属性,其他依赖视图属性本质就是一个 Computed Property。最后别忘了设置目标视图的位移,让其始终跟在 Header View 下面。
还有两个便利函数,比较简单:

 private float getDependentViewCollapsedHeight() {
return getDependentView().getResources().getDimension(R.dimen.collapsed_header_height);
} private View getDependentView() {
return dependentView.get();
}

下面我们主要来看看 Nested Scrolling 怎么实现。

本例子中我们需要 NSP (Behavior 就是 NSP 的一个代理) 的这几个回调方法:

  • onStartNestedScroll
  • onNestedScrollAccepted
  • onNestedPreScroll
  • onNestedScroll
  • onNestedPreFling
  • onStopNestedScroll

onStartNestedScroll

用户按下手指时触发,询问 NSP 是否要处理这次滑动操作,如果返回 true 则表示“我要处理这次滑动”,如果返回 false 则表示“我不 care 你的滑动,你想咋滑就咋滑”,后面的一系列回调函数就不会被调用了。它有一个关键的参数,就是滑动方向,表明了用户是垂直滑动还是水平滑动,本例子只需考虑垂直滑动,因此判断滑动方向为垂直时就处理这次滑动,否则就不 care。

onNestedScrollAccepted

NSP 接受要处理本次滑动后,这个回调被调用,我们可以做一些准备工作,比如让之前的滑动动画结束。

onNestedPreScroll

NSC 即将被滑动时调用,在这里你可以做一些处理。值得注意的是,这个方法有一个参数 int[] consumed,你可以修改这个数组来表示你到底处理掉了多少像素。假设用户滑动了 100px,你做了 90px 的位移,那么就需要把 consumed[1] 改成 90(下标 0、1 分别对应 x、y 轴),这样 NSC 就能知道,然后继续处理剩下的 10px。

onNestedScroll

上一个方法结束后,NSC 处理剩下的距离。比如上面还剩 10px,这里 NSC 滚动 2px 后发现已经到头了,于是 NSC 结束其滚动,调用该方法,并将 NSC 处理剩下的像素数作为参数(dxUnconsumeddyUnconsumed)传过来,这里传过来的就是 8px。参数中还会有 NSC 处理过的像素数(dxConsumeddyConsumed)。这个方法主要处理一些越界后的滚动。

onNestedPreFling

用户松开手指并且会发生惯性滚动之前调用。参数提供了速度信息,我们这里可以根据速度,决定最终的状态是展开还是折叠,并且启动滑动动画。通过返回值我们可以通知 NSC 是否自己还要进行滑动滚动,一般情况如果面板处于中间态,我们就不让 NSC 接着滚了,因为我们还要用动画把面板完全展开或者完全折叠。

onStopNestedScroll

一切滚动停止后调用,如果不会发生惯性滚动,fling 相关方法不会调用,直接执行到这里。这里我们做一些清理工作,当然有时也要处理中间态问题。

思路有了,我们直接看代码就很容易理解了:

 @Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View directTargetChild, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
} @Override
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, RecyclerView child, View directTargetChild, View target, int nestedScrollAxes) {
scroller.abortAnimation();
isScrolling = false;
super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
} @Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View target, int dx, int dy, int[] consumed) {
if (dy < 0) {
return;
}
View dependentView = getDependentView();
float newTranslateY = dependentView.getTranslationY() - dy;
float minHeaderTranslate = -(dependentView.getHeight() - getDependentViewCollapsedHeight());
if (newTranslateY > minHeaderTranslate) {
dependentView.setTranslationY(newTranslateY);
consumed[1] = dy;
}
} @Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if (dyUnconsumed > 0) {
return;
}
View dependentView = getDependentView();
float newTranslateY = dependentView.getTranslationY() - dyUnconsumed;
final float maxHeaderTranslate = 0;
if (newTranslateY < maxHeaderTranslate) {
dependentView.setTranslationY(newTranslateY);
}
} @Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, RecyclerView child, View target, float velocityX, float velocityY) {
return onUserStopDragging(velocityY);
} @Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, RecyclerView child, View target) {
if (!isScrolling) {
onUserStopDragging(800);
}
}

值得注意的是展开和折叠两个动作我分别分配到 onNestedPreScrollonNestedScroll 中处理了,为什么这么做呢。我来解释一下,当 Header 完全展开时,用户只能向上滑动,此时 onNestedPreScroll 会先调用,我们判断滚动方向,如果是向上滚动,我们再看面板的位置,如果可以被折叠,那么我们就改变 Header 的 translateY,并且消耗掉相应的像素数。如果 Header 完全折叠了,NSC 就可以继续滚动了。

任何情况下用户向下滑动都不会走 onNestedPreScroll,因为我们在这个方法一开始就短路掉了,因此直接到 onNestedScroll,如果 NSC 还可以滚动,那么 dyUnconsumed 就是 0,我们就什么都不需要做了,此时用户要滚动 NSC,一旦 dyUnconsumed 有数值了,则说明 NSC 滚到头了,而如果此时正向下滚动,我们就有机会再处理 Header 位移了。这里为什么不放到 onNestedPreScroll 处理呢?因为如果 Header 完全折叠了,RecyclerView 又可以向下滚动,这时我们就不能决定是让 Header 位移还是 RecyclerView 滚动了,只有让 RecyclerView 向下滚动到头才能保证唯一性。

这里比较绕,大家要结合效果好好理解一下。

最后这个类还有一个方法:

 private boolean onUserStopDragging(float velocity) {
View dependentView = getDependentView();
float translateY = dependentView.getTranslationY();
float minHeaderTranslate = -(dependentView.getHeight() - getDependentViewCollapsedHeight()); if (translateY == 0 || translateY == minHeaderTranslate) {
return false;
} boolean targetState; // Flag indicates whether to expand the content.
if (Math.abs(velocity) <= 800) {
if (Math.abs(translateY) < Math.abs(translateY - minHeaderTranslate)) {
targetState = false;
} else {
targetState = true;
}
velocity = 800; // Limit velocity's minimum value.
} else {
if (velocity > 0) {
targetState = true;
} else {
targetState = false;
}
} float targetTranslateY = targetState ? minHeaderTranslate : 0;
scroller.startScroll(0, (int) translateY, 0, (int) (targetTranslateY - translateY), (int) (1000000 / Math.abs(velocity)));
handler.post(flingRunnable);
isScrolling = true; return true;
}

用来判断是否处于中间态,如果处于中间态,我们需要根据滑动速度决定最终切换到哪个状态,这里滚动我们使用 Scroller 配合 Handler 来实现。这个函数的返回值将会被作为 onNestedPreFling 的返回值。

方法中向 Handler 添加的 Runnable 如下:

 private Runnable flingRunnable = new Runnable() {
@Override
public void run() {
if (scroller.computeScrollOffset()) {
getDependentView().setTranslationY(scroller.getCurrY());
handler.post(this);
} else {
isExpanded = getDependentView().getTranslationY() != 0;
isScrolling = false;
}
}
};

很简单就不解释了。


OK,以上就是 HeaderScrollingBehavior 的全部内容了。

实现 HeaderFloatBehavior

相信大家有了上面的经验,这个类写起来就很简单了。我们只需要实现 layoutDependsOnonDependentViewChanged 就行了。 下面是 onDependentViewChanged 的代码:

到这里两个 Behavior 就都写完了,直接在布局 xml 中引用就可以了,Activity 或 Fragment 中不需要做任何设置,是不是很方便。

总结

CoordinatorLayoutBehavior 结合可以做出十分复杂的界面效果,本文也只是介绍了冰山一角,很难想象没有它,这些效果的实现将是一件多么复杂的事情 :-)

- EOF -

 

使用 CoordinatorLayout 实现复杂联动效果的更多相关文章

  1. Json 基于jQuery+JSON的省市联动效果

    helloweba.com 作者:月光光 时间:2012-09-12 21:57 标签: jQuery  JSON  Ajax  省市联动     省市区联动下拉效果在WEB中应用非常广泛,尤其在一些 ...

  2. JQuery打造下拉框联动效果

    做联动效果,若是用纯JavaScript来做,往往须要辅助页面保存须要刷新的结果集,然后渲染到原页面.考虑将须要动态刷新的内容自己主动拼接到前一个下拉框之后,当前一个下拉框onchange后,同级的后 ...

  3. ASPxComboBox控件联动效果bug改进

    原文:ASPxComboBox控件联动效果bug改进 在应用第三方控件DevExpress控件的时候,大家应该对ASPxComboBox控件应该不是很陌生吧,尤其在做多级联动效果的时候,有着它独特的地 ...

  4. jquery.cityselect.js基于jQuery+JSON的省市或自定义联动效果

    一.插件介绍 最早做省市联动的时候都特别麻烦,后来在helloweba的一篇文章中看到这个插件,很不错的,后来就一直用了. 省市区联动下拉效果在WEB中应用非常广泛,尤其在一些会员信息系统.电商网站最 ...

  5. 用echartsjs 实现动态绘制折线、柱状等图形,并实现多图联动效果

    echarts对于大数据处理后绘制折线图,柱形图等等的效果和速度都很好.下面我们介绍 怎么把封装的数据列表解析出来,动态绘图,并且实现鼠标联动效果引入js文件: <script type=&qu ...

  6. PyQt5--基础篇:用eric6工具实现三级联动效果

    今天给大家介绍下python gui界面的三级联动效果,我们用工具eric6来实现,先看下效果图. 首先我们先创建项目linkage,再新建窗体进入到Qt设计师工具开始设计界面,完成后保存并退出. 在 ...

  7. 实现select联动效果,数据从后台获取

    效果如下: 当type值选择完后,amount值会自动相应填入. 1. 从后台获取数据,为一个数组,里面包含多个对象. <select id="scholarshipTypeSelec ...

  8. vue实现二级联动效果

    你如城市与省份间的二级联动效果 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"&g ...

  9. js之省市区(县)三级联动效果

    省市区(县)三级联动效果,是我们软件开发比较常用的,特别是对一些crm,erp之类,当然也包括其他的后台管理系统,基本都涉及到,今天贴出这个常用的,方便个人复用和大家使用 <!DOCTYPE h ...

随机推荐

  1. Mongodb学习笔记一(Mongodb环境配置)

    Mongodb学习 说明: MongoDB由databases组成,database由collections组成,collection由documents组成,document由fileds组成.Mo ...

  2. 【腾讯GAD暑期训练营游戏程序班】游戏场景管理作业说明文档

    场景管理作业说明文档                              用了八叉树的算法,测出三层时最快,区域范围内物体数量为21块,控制台打印出的结果如图所示: 场景物体:游戏中,所有具有空 ...

  3. c语言之【#ifdef】

    电脑程序语句,我们可以用它区隔一些与特定头文件.程序库和其他文件版本有关的代码. 1 2 3 #ifdef 语句1     // 程序2 #endif 可翻译为:如果宏定义了语句1则程序2. 作用:我 ...

  4. JS正则表达式基本概念

    1.正则表达式(Regular Expression) 在JS里是RegExp 两种字义方式 1. var patt1=new RegExp("e"); 2. var patt1 ...

  5. Linux ubuntu安装

    http://www.ubuntu.org.cn/download/alternative-downloads 下载地址: http://releases.ubuntu.com/ 国内镜像: http ...

  6. Asp.Net MVC<五>:过滤器

    ControllerActionInvoker在执行过程中除了利用ActionDescriptor完成对目标Action方法本身的执行外,还会执行相关过滤器(Filter).过滤器采用AOP的设计,它 ...

  7. css-css权威指南学习笔记5

    第六章 文本属性 1.text-indent只能作用于块级元素(如p或inline-block或block后的span/a/i等). 2.text-align只能作用于块级元素(如p或inline-b ...

  8. 非常提高mac生产力的一些插件归纳整理

    1.cheatsheet   快捷键提示插件,下载后按command键3秒,可以显示当前app的所有快捷键. 比如我现在在chrome的界面,按下command三秒,会弹出一个快捷键提示框. 2.Ba ...

  9. 《JavaScript DOM编程艺术(第二版)》读书总结

    这本书是一本很基础的书,但对于刚入前端不久的我来说是一本不错的书,收获还是很大的,对一些基础的东西理解得更加透彻了. 1.DOM即document object model的缩写,文档对象模型,Jav ...

  10. ArcGIS Server开发教程系列(2)配置ARCMAP和ARCCatalog发布服务

    1.       Arc catalog的配置 打开catalog,如图新增刚刚创建的server 1. Use GIS services: 用户身份连接 使用此种连接,可以浏览.使用站点内发布的所有 ...