自定义Behavior —— 仿知乎,FloatActionButton隐藏与展示
ViewPager,ScrollView 嵌套ViewPager滑动冲突解决
自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页
重磅消息:小编我开始运营自己的公众号了, 目前从事于 Android 开发,除了分享 Android开发相关知识,还有职场心得,面试经验,学习心得,人生感悟等等。希望通过该公众号,让你看到程序猿不一样的一面,我们不只会敲代码,我们还会。。。。。。
有兴趣的话可以关注我的公众号 徐公码字(stormjun94),或者拿起你的手机扫一扫,期待你的参与
效果图
我们先来看一下新浪微博发现页的效果:
接下来我们在来看一下我们仿照新浪微博实现的效果
实现思路分析
我们这里先定义两种状态,open 和 close 状态。
- open 状态指 Tab+ViewPager 还没有滑动到顶部的时候,header 还 没有被完全移除屏幕的时候
- close 状态指 Tab+ViewPager 滑动到顶部的时候,Header 被移除屏幕的时候
从效果图,我们可以看到 在 open 状态下,我们向上滑动 ViewPager 里面的 RecyclerView 的 时候,RecyclerView 并不会向上移动(RecyclerView 的滑动事件交给 外部的容器处理,被被全部消费掉了),而是整个布局(指 Header + Tab +ViewPager)会向上偏移 。当 Tab 滑动到顶部的时候,我们向上滑动 ViewPager 里面的 RecyclerView 的时候,RecyclerView 可以正常向上滑动,即此时外部容器没有拦截滑动事件。
同时我们可以看到在 open 状态的时候,我们是不支持下拉刷新的,这个比较容易实现,监听页面的状态,如果是 open 状态,我们设置 SwipeRefreshLayout setEnabled 为 false,这样不会 拦截事件,在页面 close 的时候,设置 SwipeRefreshLayout setEnabled 为 TRUE,这样就可以支持下拉刷新了。
基于上面的分析,我们这里可以把整个效果划分为两个部分,第一部分为 Header,第二部分为 Tab+ViewPager。下文统一把第一部分称为 Header,第二部分称为 Content 。
需要实现的效果为:在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移,ViewPager 里面的 RecyclerView 向上滑动的时候,消费其滑动事件,并整体向上移动。在页面状态为 close 的时候,不消耗 RecyclerView 的 滑动事件。
在上一篇博客 一步步带你读懂 CoordinatorLayout 源码 中,我们有提到在 CoordinatorLayout中,我们可以通过 给子 View 自定义 Behavior 来处理事件。它是一个容器,实现了 NestedScrollingParent 接口。它并不会直接处理事件,而是会尽可能地交给子 View 的 Behavior 进行处理。因此,为了减少依赖,我们把这两部分的关系定义为 Content 依赖于 Header。Header 移动的时候,Content 跟着 移动。所以,我们在处理滑动事件的时候,只需要处理好 Header 部分的 Behavior 就oK了,Content 部分的 Behavior 不需要处理滑动事件,只需依赖于 Header ,跟着做相应的移动即可。
Header 部分的实现
Header 部分实现的两个关键点在于
- 在页面状态为 open 的时候,ViewPager 里面的 RecyclerView 向上滑动的时候,消费其滑动事件,并整体向上移动。在页面状态为 close 的时候,不消耗 RecyclerView 的 滑动事件
- 在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。
第一个关键点的实现
这里区分页面状态是 open 还是 close 状态是通过 Header 是否移除屏幕来区分的,即 child.getTranslationY() == getHeaderOffsetRange() 。
1 | private boolean isClosed(View child) { |
在NestedScrolling 机制深入解析博客中,我们对 NestedScrolling 机制做了如下的总结。
- 在 Action_Down 的时候,Scrolling child 会调用 startNestedScroll 方法,通过 childHelper 回调 Scrolling Parent 的 startNestedScroll 方法。
- 在 Action_move 的时候,Scrolling Child 要开始滑动的时候,会调用dispatchNestedPreScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否要先于 Child 进行 滑动,若需要的话,会调用 Parent 的 onNestedPreScroll 方法,协同 Child 一起进行滑动
- 当 ScrollingChild 滑动完成的时候,会调用 dispatchNestedScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否需要进行滑动,需要的话,会 调用 Parent 的 onNestedScroll 方法
- 在 Action_down,Action_move 的时候,会调用 Scrolling Child 的stopNestedScroll ,通过 ChildHelper 询问 Scrolling parent 的 stopNestedScroll 方法。
- 如果需要处理 Fling 动作,我们可以通过 VelocityTrackerCompat 获得相应的速度,并在 Action_up 的时候,调用 dispatchNestedPreFling 方法,通过 ChildHelper 询问 Parent 是否需要先于 child 进行 Fling 动作
在 Child 处理完 Fling 动作时候,如果 Scrolling Parent 还需要处理 Fling 动作,我们可以调用 dispatchNestedFling 方法,通过 ChildHelper ,调用 Parent 的 onNestedFling 方法
而 RecyclerView 也是 Scrolling Child (实现了 NestedScrollingChild 接口),RecyclerView 在开始滑动的 时候会先调用 CoordinatorLayout 的 startNestedScroll 方法,而 CoordinatorLayout 会 调用子 View 的 Behavior 的 startNestedScroll 方法。并且只有 boolean startNestedScroll 返回 TRUE 的 时候,才会调用接下里 Behavior 中的 onNestedPreScroll 和 onNestedScroll 方法。
所以,我们在 WeiboHeaderPagerBehavior 的 onStartNestedScroll 方法可以这样写,可以确保 只拦截垂直方向上的滚动事件,且当前状态是打开的并且还可以继续向上收缩的时候还会拦截
1 | @Override |
拦截事件之后,我们需要在 RecyclerView 滑动之前消耗事件,并且移动 Header,让其向上偏移。
1 | @Override |
当然,我们也需要处理 Fling 事件,在页面没有完全关闭的 时候,消费所有 fling 事件。
1 | @Override |
至于滑动到顶部的动画,我是通过 mOverScroller + FlingRunnable 来实现的 。完整代码如下。
1 | public class WeiboHeaderPagerBehavior extends ViewOffsetBehavior { |
第二个关键点的实现
在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。
在第一个关键点的实现上,我们是通过自定义 Behavior 来处理 ViewPager 里面 RecyclerView 的移动的,那我们要怎样监听整个 Header 的滑动了。
那就是重写 LinearLayout,将滑动事件交给 ScrollingParent(这里是CoordinatorLayout) 去处理,CoordinatorLayout 再交给子 View 的 behavior 去处理。
1 | public class NestedLinearLayout extends LinearLayout implements NestedScrollingChild { |
Content 部分的实现
Content 部分的实现也主要有两个关键点
- 整体置于 Header 之下
- Content 跟着 Header 移动。即 Header 位置发生变化的时候,Content 也需要随着调整位置。
第一个关键点的实现
整体置于 Header 之下。这个我们可以参考 APPBarLayout 的 behavior,它是这样处理的。
1 | /** |
这个基类的代码还是很好理解的,因为之前就说过了,正常来说被依赖的 View 会优先于依赖它的 View 处理,所以需要依赖的 View 可以在 measure/layout 的时候,找到依赖的 View 并获取到它的测量/布局的信息,这里的处理就是依靠着这种关系来实现的.
我们的实现类,需要重写的除了抽象方法 findFirstDependency 外,还需要重写 getScrollRange,我们把 Header
的 Id id_weibo_header 定义在 ids.xml 资源文件内,方便依赖的判断.
至于缩放的高度,根据 结果图 得知是 0,得出如下代码
1 | private int getFinalHeight() { |
第二个关键点的实现:
Content 跟着 Header 移动。即 Header 位置发生变化的时候,Content 也需要随着调整位置。
主要的逻辑就是 在 layoutDependsOn 方法里面,判断 dependcy 是不是 HeaderView ,是的话,返回TRUE,这样在 Header 位置发生变化的时候,会回调 onDependentViewChanged 方法,在该方法里面,做相应的偏移。TranslationY 是根据比例算出来的 translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) * getScrollRange(dependency));
完整代码如下:
1 | public class WeiboContentBehavior extends HeaderScrollingViewBehavior { |
题外话
- NestedScrolling 机制,对比传统的事件分发机制真的很强大。这种仿新浪微博发现页效果, 如果用传统的事件分发机制来做,估计很难实现,处理起来会有一大堆坑。
- 看完了这种仿新浪微博发现页的效果,你是不是学到了什么?如果让你 模仿 仿 QQ 浏览器首页效果,你能实现话。
最后,特别感谢写这篇博客 自定义Behavior的艺术探索-仿UC浏览器主页 的开发者,没有这篇博客作为参考,这种效果我很大几率是实现 不了的。大家觉得效果还不错的话,顺手到 github 上面给我 star,谢谢。github 地址
参考文章:
最后的最后,卖一下广告,欢迎大家关注我的微信公众号 徐公码字,扫一扫下方二维码或搜索微信号 stormjun94,即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。