使用 CoordinatorLayout 实现复杂联动效果
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 就可以做出适当的响应。很简单的例子就是 FAB 和 SnackBar 的联动,具体表现就是 FAB 会随 SnackBar 的弹出而上移,从而不会被 SnackBar 遮挡,这就是依赖视图的最简单的一个用法。
Nested Scrolling
这是 Google 开发的一种全新嵌套滚动方案,由 NestedScrollingParent
和 NestedScrollingChild
组成,一般来讲我们都会围绕 NestedScrollingParent
来进行开发,而 NestedScrollingChild
相比来说较为复杂,本文也不赘述其具体用法了。NestedScrollingParent
(下文简称 NSP) 和 NestedScrollingChild
(下文简称 NSC) 有一组相互配对的事件方法,NSC 负责派发这些方法到 NSP,NSP 可以对这些方法做出响应。同时 Google 也提供了一组 Helper 类来帮助开发者使用 NSP 和 NSC,其中 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 控制的视图进行布局,就是将 ViewGroup
的 onLayout
针对该视图的部分抽出来给 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 处理剩下的像素数作为参数(dxUnconsumed
、dyUnconsumed
)传过来,这里传过来的就是 8px。参数中还会有 NSC 处理过的像素数(dxConsumed
、dyConsumed
)。这个方法主要处理一些越界后的滚动。
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);
}
}
值得注意的是展开和折叠两个动作我分别分配到 onNestedPreScroll
和 onNestedScroll
中处理了,为什么这么做呢。我来解释一下,当 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
相信大家有了上面的经验,这个类写起来就很简单了。我们只需要实现 layoutDependsOn
和 onDependentViewChanged
就行了。 下面是 onDependentViewChanged
的代码:
到这里两个 Behavior 就都写完了,直接在布局 xml 中引用就可以了,Activity 或 Fragment 中不需要做任何设置,是不是很方便。
总结
CoordinatorLayout
和 Behavior
结合可以做出十分复杂的界面效果,本文也只是介绍了冰山一角,很难想象没有它,这些效果的实现将是一件多么复杂的事情 :-)
- EOF -
使用 CoordinatorLayout 实现复杂联动效果的更多相关文章
- Json 基于jQuery+JSON的省市联动效果
helloweba.com 作者:月光光 时间:2012-09-12 21:57 标签: jQuery JSON Ajax 省市联动 省市区联动下拉效果在WEB中应用非常广泛,尤其在一些 ...
- JQuery打造下拉框联动效果
做联动效果,若是用纯JavaScript来做,往往须要辅助页面保存须要刷新的结果集,然后渲染到原页面.考虑将须要动态刷新的内容自己主动拼接到前一个下拉框之后,当前一个下拉框onchange后,同级的后 ...
- ASPxComboBox控件联动效果bug改进
原文:ASPxComboBox控件联动效果bug改进 在应用第三方控件DevExpress控件的时候,大家应该对ASPxComboBox控件应该不是很陌生吧,尤其在做多级联动效果的时候,有着它独特的地 ...
- jquery.cityselect.js基于jQuery+JSON的省市或自定义联动效果
一.插件介绍 最早做省市联动的时候都特别麻烦,后来在helloweba的一篇文章中看到这个插件,很不错的,后来就一直用了. 省市区联动下拉效果在WEB中应用非常广泛,尤其在一些会员信息系统.电商网站最 ...
- 用echartsjs 实现动态绘制折线、柱状等图形,并实现多图联动效果
echarts对于大数据处理后绘制折线图,柱形图等等的效果和速度都很好.下面我们介绍 怎么把封装的数据列表解析出来,动态绘图,并且实现鼠标联动效果引入js文件: <script type=&qu ...
- PyQt5--基础篇:用eric6工具实现三级联动效果
今天给大家介绍下python gui界面的三级联动效果,我们用工具eric6来实现,先看下效果图. 首先我们先创建项目linkage,再新建窗体进入到Qt设计师工具开始设计界面,完成后保存并退出. 在 ...
- 实现select联动效果,数据从后台获取
效果如下: 当type值选择完后,amount值会自动相应填入. 1. 从后台获取数据,为一个数组,里面包含多个对象. <select id="scholarshipTypeSelec ...
- vue实现二级联动效果
你如城市与省份间的二级联动效果 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"&g ...
- js之省市区(县)三级联动效果
省市区(县)三级联动效果,是我们软件开发比较常用的,特别是对一些crm,erp之类,当然也包括其他的后台管理系统,基本都涉及到,今天贴出这个常用的,方便个人复用和大家使用 <!DOCTYPE h ...
随机推荐
- 浅谈Struts2
学过SSH框架很长一段时间了,一直没有很系统的总结一下,这里先简单谈谈Struts2. 为什么要用Struts2? 这里列举一些Servlet的缺点: 1.每写一个servlet在web.xml中都要 ...
- php面向对象编程(一)
类与对象关系: 类就像一个人类的群体 我们从类中实例化一个对象 就像是制定一个人. 面向对象程序的单位就是对象,但对象又是通过类的实例化出来的,所以我们首先要做的就是如何来声明类, 做出来一个类很容易 ...
- C#-WinForm-Winform TextBox中只能输入数字的几种常用方法(C#)
方法一: private void tBox_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == 0x20) e.KeyCh ...
- [转]win 10 开始菜单(Win 7风格)增强工具 StartIsBack++ v1.3.4 简体中文特别版
Windows10开始菜单增强工具StartIsBack++现已更新至v1.3.4,最近主要修复在Win10周年更新版上恢复睡眠后任务栏通知中心按钮消失的问题.升级版对StartIsBack+全新构建 ...
- Android开发常见问题小结
1.布局文件 自己写的布局文件无得使用 原因:导入了系统的R文件 修改:将android.R 这句注释 或者删除 2.Activity作为主窗口设置 Activity无法启动,或者要设置Activit ...
- C#中时间的比较
项目中需求,要求一个线程必须待够一定时间才允许停止,那么就涉及到一个时间的比较与线程的sleep var threadTimeOut= DateTime.Now.AddMinutes(timeOutN ...
- 挣值管理(PV、EV、AC、SV、CV、SPI、CPI) 记忆
挣值管理法中的PV.EV.AC.SV.CV.SPI.CPI这些英文简写相信把大家都搞得晕头转向的.在挣值管理法中,需要记忆理解的有三个参数:PV.AC.EV. PV:计划值,在即定时间点前计划 ...
- 10月28日下午MySQL数据库的增加、删除、查询(匹配数据库登录和可以增、删、查的显示数据库内容的页面))
一.匹配数据库登录 步骤: 1.做一个普通的登录界面,注意提交方式为post. <!--登录界面--> <form action="chuli.php" meth ...
- Linux硬链接和软链接
1.Linux链接概念Linux链接分两种,一种被称为硬链接(Hard Link),另一种被称为符号链接(Symbolic Link).默认情况下,ln命令产生硬链接. [硬连接]硬连接指通过索引节点 ...
- Windows 7 激活时的坑
前段时间,桌面上有两个文件用各种方法删除不了. 然后今天终于进了PE系统,使用DG把这两货干掉了. 重启进入Windows,提示我 不是正版,今天必须激活,桌面变成了一片黑... 打开小马激活工具OE ...