背景
项目开发中,我们经常有滑动控件固定某一个部分在顶部的需求,效果类似这样:
在Material Design
出来之前,我们可能会有方案一:
给 ListView C 添加一个HeadView(包含A、B),然后另外准备一个外部的B在屏幕顶部,一开始不可见。ListView当前滚动高度超过A的高度时,显示外部的B;滚动高度小于A时隐藏外部的B。
正如我此时的项目中的一样,但是项目中的B是一个搜索框,类似这样:
可以看到 B 的构成是相对复杂的,B 相关的事件操作也会写 2 遍,很显然的会导致整个代码结构非常臃肿,所以需要寻找方案二。
嵌套滚动机制
在我早些的一篇文章使用Android新特性:Material Design中有说起一个控件:CoordinatorLayout
。它是一个增强型的 FrameLayout。它的作用有两个:
- 作为一个布局的根布局
- 最后一个为子视图之间相互协调手势效果的一个协调布局
为子视图协调手势效果主要是基于 Android 的嵌套滚动机制。
所谓嵌套滚动其实就是界面布局中包含一个可滚动的列表和一个不可滚动的View,这样在滚动列表时,首先将不可滚动View移出屏幕或移进屏幕,待不可滚动View固定时,才会继续滚动滚动列表的内容。
关于嵌套滚动机制更详细的一些说明有很多文章都说的不错,稍后会在参考中放出链接。
实例
我们知道可以通过Behavior
来实现各种嵌套滑动效果。最为典型的就是AppBarLayout中的ScrollingViewBehavior
。很经典的示例代码:
1 |
|
通过给滑动控件设置app:layout_behavior="@string/appbar_scrolling_view_behavior"
来实现控制 ToolBar 隐藏或消失的效果。但是这个 Behavior 是依赖于 AppBarLayout 的,换成其他的控件将会失效。
尝试一
基于我的情况,我的Header可能是这样的:
1 | <LinearLayout> |
所以我期初的做法是定义 Behavior ,然后通过依赖让 LinearLayout 与 RecyclerView 联动。
布局代码如下:
1 |
|
HeaderBehavior:
1 | public class HeaderBehavior extends CoordinatorLayout.Behavior<View> { |
RecyclerBehavior:
1 | public class RecyclerBehavior extends CoordinatorLayout.Behavior<RecyclerView> { |
最后实现的效果可以说基本满足了。但是当发生Fling
滑动时,便会很容易出现问题。因为上面的Header LinearLayout并没有处理Fling的操作。后面我自定义LinearLayoutWithFling
利用OverScroller
来实现Fling
但是结果并不如意。
尝试二
自定义实现NestedScrollingParent
接口的 LinearLayout ,然后内部来处理 RecyclerView 的滑动。布局代码如下:
1 | <com.study.lijia.coordinatorlayoutdemo.StickyNavLayout |
StickyNavLayout如下:
1 | public class StickyNavLayout extends LinearLayout implements NestedScrollingParent { |
达到的效果姑且不错,但是当我想要加入下拉刷新SwipeRefreshLayout
的时候,感觉不是很好加。SwipeRefreshLayout
也是基于嵌套滑动机制的一个下拉刷新类,StickyNavLayout
与SwipeRefreshLayout
要共同处理 RecyclerView 的滑动事件,会冲突。
方案三–回归本质
当几经尝试后,感觉还是对原理没摸清,于是回过头来看 AppBarLayout 的 Behavior 。结果我特喵的发现AppBarLayout 就是 LinearLayout,很符合我的项目实情。然后利用layout_scrollFlags
来控制Header A
的显示、隐藏,并且加入下拉刷新也很方便,最后便采用了原生的方法了。
1 | <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" |
下拉刷新加载 RecyclerView 那里实际效果看起来很怪,最后调整将 SwipeRefreshLayout 作为根节点, CoordinatorLayout 作为其子 View ,这个时候下拉刷新会有问题,因为2者都实现了 NestedScrollParent 接口,采取的方法是监听 AppBarLayout 的 offset,根据这个值来是否禁用 SwipeRefreshLayout。
1 | mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() { |
参考
demo为了速成,很多代码都是从以下参考文章中直接拿的-。-