ListView 或者 RecycleView 滚动时隐藏 Toolbar (1)
今天我打算写一篇博文给大家介绍 Google+ App 的一个酷炫效果——向上/向下滚动 ListView/RecyclerView 时,Toolbar 和 FAB(屏幕右下方的小按钮)会隐藏/出现。这个效果也被 Google 视为符合 Material Design 规范的效果哦,详情参见: Material Design Checklist 。
如果可以的话,我们希望当屏幕向下滚动时,App 的 Toolbar 和 FAB 将离开屏幕,从而在垂直方向上为可显示区域腾出更大的空间来显示我们的内容。而当我们的屏幕向上滚动时,App 的 Toolbar 和 FAB 会再次出现。
在这篇博文的讲解中,我们将会用 RecyclerView 作为我们的 list,但这不代表其他可滚动的容器就不能实现这样的效果了,只是其他的可滚动容器(如:ListView)需要要多花一些功夫才能实现这个效果。那么我们要怎么去实现呢?我想到了两个办法:
- 对 list 容器加一个 Padding
- 对 list 容器加一个 Header
就我个人而言,我更想用第二种方法去实现,因为在设计代码的过程中我发现:为 RecyclerView 添加 Header 会产生几个问题,这给了我很好的机会去思考如何解决它,与此同时,在这个思考和解决问题的过程中我还能学习到更多的知识,何乐而不为呢?不过我还是会给大家简要地介绍如何使用第一种方法实现的啦!
那就让我们开始今天的讲解吧!
首先,我们需要创建一个工程和添加必要的工具库:
接着我们需要定义 styles.xml ,以确保我们的 App 没有添加 Actionbar(因为我们将会使用 Toolbar),同时我们 App 的设计风格要符合 Google 的 Material Design 规范。
最后我们就要创建我们 activity 中显示的布局:
事实上,我们只需要一个简单的布局,其中包含了:RecyclerView,Toolbar 和 ImageButton。但需要注意的是:我们需要把它们放在一个 FrameLayout 里,否则当我们隐藏 Toolbar 时 list 的上方将会出现一个空白区域,这显然不会是我们想要的效果。我们理想的效果应该是:当 Toolbar 被隐藏,list 能在屏幕的可见区域中显示出一整个列表,而这就需要通过使 Toolbar 覆盖在 RecycleView 上面来实现。
接着来看看我们 MainActivity 代码吧:
就像你看到的那样,这是一个只重写了 OnCreate() 方法,非常非常非常简单的 Activity。而且 OnCreate() 方法也只做了下面三件事情:
- 初始化 Toolbar
- 获取 mFabButton 的引用(mFabButton 是 FAB 的对象哦,也就是屏幕右下方的小按钮)
- 初始化 RecyclerView
为了在 list 中显示出内容,我们现在就要为 RecyclerView 创建一个 Adapter 啦。但在此之前,我们应该为我们 list 中的 item 添加相应的子布局以及对应的 ViewHolder:
list 的每一个 item 只需要一个 text 用来显示文字,非常简单!
那 RecyclerAdapter 该怎么实现呢:
就像你看到的这样,这是一个非常普通的 RecycleView.Adapter,没有任何特别的地方,是不是感觉被骗了呐 23333~(如果你想要了解更多有关 RecyclerView 知识,我强烈建议你去看大牛 Mark Allison's 巨巨写的这些优秀文章 series of posts )
经过上面的努力,我们已经把车子的小零件组装的七七八八啦,是时候让小车子上路跑一跑,展现真正的技术了!
WTF,谁能告诉我这是什么鬼……?
我相信只要不是瞎子都会发现,App 的 Toolbar 竟然把我们的 item 挡住了!!!或许部分机智的小伙伴已经发现了问题所在:出现这样的问题是因为我们在 activity_main.xml 里使用了 FrameLayout,正是 FrameLayout 导致了这样的问题,而这就是我开头所提到的问题之一了。
第一个解决办法是为我们的 RecyclerView 添加一个 PaddingTop,并将 PaddingTop 的值设置为 Toolbar 的高度。但有一个细节我们不能忽略,那就是 RecyclerView 会默认裁剪到子 View 的 Padding 区域,所以为了我们伟大的事业,我们必须把这个特性关掉。
经过这些修改之后,我们就能实现我们想要的效果,这就是我所说的第一种方法。但如我所说,我写这篇博文的目的,不仅仅只是教你实现这个效果,然后就完了。我想教给你实现同一个效果各种各样的方法,并且为你介绍其中的思想,让你接触到平常很难接触到的问题并教你如何解决它。有些方法固然会更加复杂(在本文中是为 list 添加一个 Header),但你在实现过程中也能学到更多的知识,毕竟授人以鱼不如授人以渔嘛。
为 RecycleView 添加一个 Header
要用第二种方法去实现这个效果,首先我们要做的就是稍微修改一下我们的 Adapter:
下面是其实现原理:
我们需要定义一个常量来区分 Recycler 展现的 item 的类型。这里我需要为你介绍的是:RecyclerView 是一个非常灵活的组件,RecyclerView 完全能实现你想要让 list 的 item 具有各种各样不同的布局的愿望,而此时,我们定义来区分 item 类型的常量就会被利用到。而这样的特性正是我们想要的——让 Header 成为 RecyclerView 的第一个 item,显然这会与其余的 item 不一样。(第 3-4 行)
因而我们需要让 Recycler 知道它需要展示的子布局是什么类型的,在本文中我们用作类型区分的常量则是 TYPE_ITEM 和 TYPE_HEADER。(第 49-54 行)
接着,我们需要修改 onCreateViewHolder() 和 onBindViewHolder() 方法,如果 item 的类型是 TYPE_ITEM 的话,使它们返回和绑定一个普通的 item;如果 item 的类型是 TYPE_HEADER 的话,则返回 Header。(第 14-34 行)
此外,由于我们的 list 并不仅仅只有普通的 item,我们还在 list 中添加了 Header,因此我们需要修改 getItemCount() 方法的返回值,让我们的返回值是普通 item 的总数量 + 1(第 43-45 行)
最后让我们来为 Header 布局创建一个 layout 和一个 ViewHolder,但出乎意料的是,我们需要为 Header 创建的 layout 和 ViewHolder 都非常简单,唯一需要注意的是:Header 的高度必须和 Toolbar 的高度一致。
所以总的来说,我们为 RecyclerView 添加了一个和 Toolbar 有相同高度的 Header,而现在我们的 Toolbar 把 header 隐藏起来了(因为 header 现在是一个空的 view),同时,我们所有的普通 item 都是可见的。那么现在就让我们来实现滚动时改变 Toolbar 和 FAB 的出现和隐藏吧!
滚动时控制 Toolbar 和 FAB 的出现和隐藏
为了实现这个效果,我们为 RecyclerView 再创建一个——OnScrollListener 类就够了你敢信?
我现在还要告诉你,在 OnScrollListener 类里,我们只需要重载 onScrolled() 方法就能让这个酷炫的效果成为 App 中秒杀用户的黑魔法!其中,onScrolled() 方法的参数——dx,dy 分别是水平和垂直方向上的滚动距离。但大家需要注意的是:这里 dx,dy 并不是代表屏幕上的物理距离,而是两个事件的相对距离。
具体的实现算法大体如下:
只有当 list 向上滚动且 Toolbar 和 FAB 被隐藏,抑或是当 list 向下滚动且 Toolbar 和 FAB 出现,我们才会计算总的滚动距离,因为这两种情况下的滚动距离才是我们实现这个效果所需要关心的。
总的滚动距离需要超过我们展现/隐藏 Toolbar 和 FAB 所在的方向的极限值才能将其展现/隐藏(你把极限值调整的越大,通过滚动展示/隐藏 Toolbar 和 FAB 需要的距离就越大)(dy>0 意味着我们在向下滚动,dy<0 意味着我们在向上滑动)
实际上我们并没有在我们的滚动监听类里面展现/隐藏 Toolbar 和 FAB,我们是通过调用 show()/hide() 方法来展现/隐藏 Toolbar 和 FAB 的,所以调用者可以通过接口实现它。
现在我们需要为 RecyclerView 添加它的监听:
下面是我们通过动画改变我们的视图的方法:
当我们隐藏 Toolbar 和 FAB 的时候,我们必须把 Padding 也考虑进去,不然的话视图并不能够完全被隐藏。
是骡子是马,让我们拉出来溜一溜!
虽然现在看起来已经很 nice 了,但其实这里有一个小小的 bug——如果你在 list 的顶部,此时临界值非常小,因而你能隐藏 Toolbar,但你在 list 的顶部会看到有一个空白的区域。不过幸好这里有一个很简单的方法可以解决这个 bug:我们可以通过检测当前 list 的第一个 item 是否为可见的,只有当它不可见,才使用我们设计的展示/隐藏逻辑。
在这样的修改后,即使第一个 item 是可见的并且有 item 被它挡住了,我们也在展示它们,除此以外的情况我们都像之前说的那样实现我们的效果。
太棒了思密达!感觉之前的失败都如雨后甘霖温润我脆弱的心灵呐~
其实羞羞地说一句……这篇文章是我人生中的第一篇博文呐,如果你觉得很无聊或者我有哪里讲解错误的话千万不要喷我哦。我会在未来变得更棒哒,然后尽我所能为大家贡献更多的文章!
如果你看到这里还是通过添加 Header 来实现这个效果很恼火的话,用第一种方法结合 HidingScrolllistener 也是可以实现这个效果的~
如果你有什么疑问的话,可以在评论区问我哦,我都会尽我所能为你解答的!
源码
整个项目的源码在 GitHub 上面都有,大家可以在这看 repo
感谢 Mirek Stanek 帮我校对文章,么么哒!爱你的好基友 Michal Z~
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论