专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
鸿洋  ·  Android | ... ·  2 天前  
stormzhang  ·  生活真的经不起考验 ·  2 天前  
鸿洋  ·  Android 复杂项目崩溃率收敛至0.01%实践 ·  5 天前  
stormzhang  ·  特斯拉画的饼,圆上了? ·  5 天前  
鸿洋  ·  裁员在家如何保持高效学习 ·  1 周前  
51好读  ›  专栏  ›  郭霖

为RecyclerView打造右侧索引导航栏IndexBar

郭霖  · 公众号  · android  · 2016-11-03 08:00

正文

今日科技快讯

针对于11月1日开始实施的网约车新规,滴滴出行今日发表声明称,滴滴将进一步与各级主管部门沟通交流,争取使民众出行得到最大满足。滴滴表示,目前各地细则正式实施,不少地方的细则在广纳民意的基础上做了很多修改和完善,滴滴将积极配合有关部门,加强规范管理,提升安全和体验。

作者简介

本篇是 张旭童 的第二篇投稿,紧跟他的上一篇文章《悬停头部分组列表》(点击可看),上篇的反响还是很不错的,今天除了补充上篇,还新增了字母索引栏功能,相当实用。与上篇一样,本文很充实,所以你可能要花费一定时间阅读了,让我们快开始吧!

张旭童 的博客地址:

http://blog.csdn.net/zxt0601

概述

《悬停头部分组列表》里面,我们用 ItemDecoration RecyclerView 打造了带悬停头部的分组列表。其实Android版微信的通讯录界面,它的分组title也不是悬停的,我们已经领先了微信一小步(认真脸)~ 

再看看市面上常见的分组列表(例如饿了么点餐商品列表),不仅有悬停头部,悬停头部在切换时,还会伴有切换动画。 

关于 ItemDecoration 还有一个问题,简单布局还好,我们可以draw出来,如果是复杂的头部呢?能否写个 xml,inflate进来,这样使用起来才简单,即另一种简单使用 onDraw 和 onDrawOver 的姿势。 

so,本文开头我们就先用两节完善一下我们的 ItemDecoration。然后进入正题:自定义View实现右侧索引导航栏IndexBar,对数据源的排序字段按照拼音排序,最后将 RecyclerView 和 IndexBar 联动起来,触摸 IndexBar 上相应字母,RecyclerView 滚动到相应位置。(在屏幕中间显示的其实就是一个TextView,我们set个体IndexBar即可) 

由于大部分使用右侧索引导航栏的场景,都需要这几个固定步骤,对数据源排序,set给IndexBar,和RecyclerView联动等,所以最后再将其封装一把,成一个高度封装,因此扩展性不太高的控件,更方便使用,如果需要扩展的话,反正看完本文再其基础上修改应该很简单~

最终版预览:

本文摘要: 

  • 用ItemDecoration实现悬停头部切换动画 

  • 另一种 简单使用onDraw()和onDrawOver() 的姿势 

  • 自定义View实现右侧**索引导航栏**IndexBar 

  • 使用 TinyPinyin 对数据源排序 

  • 联动 IndexBarRecyclerView

  • 封装重复步骤,方便二次使用,并可 定制导航数据源

悬停头部"切换动画"

实现了两种, 第一种就是仿饿了么点餐时,商品列表的悬停头部切换“动画效果”,如下:

第二种是一种头部折叠起来的视效,个人觉得也还不错~如下:(估计没人喜欢) 

果然比上部残篇里的效果好看多了,那么代码多不多呢,看我的 Git show 记录:

绿色部分的不到十行代码就搞定~先上这个图是为了让大家安心,代码不多,分分钟看完。 

下面放上文字版代码,江湖人称 注释张 的我,已经写满了注释。 再简单说下吧, 滑动时,在判断头部即将切换(当前pos的tag和pos+1的tag不等)的时候:

  • 计算出当前悬停头部应该上移的位移, 利用 Canvas 的画布移动方法 Canvas.translate(),即可实现“饿了么”悬停头部切换效果。

  • 计算出当前悬停头部应该在屏幕上还剩余的空间高度,作为头部绘制的高度,利用 Canva s的 Canvas.clipRect() 方法,剪切画布,即可实现“折叠”的视效。


这份代码核心处 c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);实现的是饿了么效果,被注释掉的:


实现的是效果二。

onDraw与onDrawOver新姿势

之前我们使用 onDraw(),onDrawOver(),都是用 canvas 的方法活生生的绘制一个出View,这对于很多人(包括我)来说都不容易,xy坐标的确认,尺寸都较难把握,基本上调UI效果时间都很长。尤其是 canvas.drawText() 方法的y坐标,其实是 baseLine 的位置,不了解的童鞋肯定要踩很多坑。

当我们想要绘制的分类title、悬停头部复杂一点时,我都不敢想象要调试多久了,这个时候我们还敢用ItemDecoration吗。

有没有一种方法,就像我们平时使用的那样,在Layout布局xml里画好View,然后inflate出来就可以了呢。

这个问题开始确实也把我难住了,难道又要从入门到放弃了吗?

于是我又搜寻资料,功夫不负有心人。

解决问题的办法就是,View类的:public void draw(Canvas canvas) 方法 

下面我们就看一个用法Demo吧: 

布局 layout:header_complex.xml(注意有个ProgressBar哦)


onDrawOver 代码如下:简单讲解下,先 inflate 这个复杂的Layout,然后拿到它的 LayoutParams,利用这个 lp 拿到宽和高的 MeasureSpec,然后依次调用 measure,layout,draw方法,将复杂头部显示在屏幕上。


这里还有个有趣的地方,某些需要不断调用 onDraw() 更新绘制自己最新状态的View,例如 ProgressBar,由于在屏幕上显示的并不是真正的View,只是我们手动的调用了一次draw方法,进而调用View 的 onDraw() 显示的一次“残影”,所以 ProgressBar 只会显示 onDraw() 当时的样子,并不会主动刷新了。 

看图说话,还是很容易理解的: 

滑动时,由于会回调 onDrawOver() 方法,所以ProgressBar又被手动调用了 draw(),开始变化,滑动的快的话,progressBar 会有动画效果。

停止不动时,ProgressBar 也是静止的,保持 draw() 时绘制的状态。

右侧索引IndexBar

不管是自定义 ItemDecoration 还是实现 右侧索引导航栏,都有大量的自定义View知识在里面 ,这里简单复习一下。

步骤1-4是自定义View的必须套路,步骤5+是IndexBar特殊定制

1. 自定义View首先要确定这个View需要在xml里接受哪些属性?

IndexBar 里,我们先需要两个属性,每个索引的文字大小和手指按下时整个View的背景, 即在 attrs.xml 如下定义:


2. 在View的构造方法中获得我们自定义的属性

套路代码如下,都是套路,记得使用完最后要将typeArray对象 recycle()。


3. 重写onMesure()方法(可选)

onMeasure() 方法里,主要就是遍历一遍 indexDatas,得到index最大宽度和高度。然后根据三种测量模式,分配不同的值给View

  • EXACLTY 就分配具体的测量值(match_parent,确定数值)

  • AT_MOST 就分配父控件能给的最大值和自己需要的值之间的最小值。(保证不超过父控件限定的值)

  • UNSPECIFIED 则分配自己需要的值。(随心所欲)


4. 重写onDraw()方法

整理一下需求和思路:

利用 index数据源 的size,和控件可绘制的高度(高度-paddingTop-paddingBottom),求出每个 index区域 的高度 mGapHeight

每个 index 在绘制时,都是处于水平居中,竖直方向上在 mGapHeigh t区域高度内居中。

思路整理清楚,代码很简单如下:


onSizeChanged 方法里,获取控件的宽高,并计算出 mGapHeight:


最后在 onDraw()方法 里绘制。

如果对于竖直居中 baseLine 的计算不太理解可以先放置,这块的确挺绕人,后面应该会写一篇 canvas.drawText()x y坐标计算的小短文. 

可记住重点就是 Paint 默认的 TextAlign 是 Left,即x方向,左对齐,所以x坐标决定绘制文字的左边界。

y坐标是绘制文字的 baseLine 位置。


以上四步基本完成了IndexBar的绘制工作,下面我们为它添加一些行为的响应。

5. 重写onTouchEvent()方法

我们需要重写 onTouchEvent() 方法, 以便处理手指按下时的View背景变色,抬起时恢复原来颜色 ,并根据手指触摸的落点坐标,判断当前处于哪个index区域,回调给相应的监听器处理(显示当前index的值,滑动RecyclerView至相应区域等。。)

代码如下:


6. 联动IndexBar和RecyclerView

具体的操作交由监听器处理,定义和实现如下: 

值得一提的就是,滑动RecyclerView 到 指定postion,我们使用的是LinearLayoutManager的scrollToPositionWithOffset(int position, int offset) 方法,offset 传入0,postion 即目标 postion 即可。如果使用 RecyclerView.scrollToPosition();等方法,滑动会很飘~定位不准。

mPressedShowTextView 就是在屏幕中间显示的当前处于哪个index的TextView。



封装,方便二次使用

在我个人的理解里,程序过多的封装是会导致扩展性的降低(也是因为我水平有限),然而我们今天要封装的这个 IndexBar,由于使用场景和套路还是挺固定的(城市分组列表,商品分类列表)所以值得将相关的操作都聚合起来,二次使用更方便。毕竟,一个项目里同样的代码写第二遍的程序员都不是好的圣斗士。(其实是我的leader不想写第二遍,让我封装一下给他秒用)

梳理一下固定的操作:

  • 都是先对原始数据sourceDatas源按照排序字段拼音排序。

  • 然后将屏幕中hint的TextView ,以及索引数据源indexDatas(通过sourceDatas获得),通过set方法传给IndexBar。

  • 联动IndexBar和RecyclerView,使得触摸IndexBar相应区域RecyclerView会滚动(借助sourceDatas获得对应postion)。

根据上述,我的设想在使用时,只需要给 IndexBar 设置 原始数据sourceDatas,HintTextView,和 RecyclerView 的 LinearLayoutManager,在 IndexBar内部 对sourceDatas排序,并获得索引数据源 indexDatas,然后设置一个默认的index触摸监听器,在手指按下滑动时,由于 IndexBar 持有 HintTextView 和 LayoutManager,则 HintTextView 的s how hide,以及 LayoutManager 的滚动 都在 IndexBar内部 完成。

最终使用预览:


布局xml:


其中,setNeedRealIndex(true)//设置需要真实的索引,是指索引栏的数据不是固定的A-Z,#。而是根据真实的sourceDatas生成。

因为链式调用用起来很爽,所以在这些set方法里都 return 了 this。

1. 抽象两个实体类和一个接口

先把 tag 抽象出来,放在顶层,这里存放的就是IndexBar显示的每个index值(A-Z,#)(本例是城市的汉语拼音首字母),而且在联动滑动时,根据tag获取postion时,也需要用到tag。它是导航分组列表的基础。


然后抽象一个接口和一个实体类, 接口定义一个方法getTarget(),它返回 需要被转化成拼音,并取出首字母 索引排序的 字段。(本例就是城市的名字)

实体类继承BaseIndexTagBean,并实现以上接口,且额外存放 需要排序的字段的拼音值,(本例是城市的拼音)。它根据getTarget()返回的值利用 TinyPinyin库 得到拼音。

public interface IIndexTargetInterface {
   //需要被转化成拼音,并取出首字母 索引排序的 字段    String getTarget(); }


有了以上两个类一个接口,我们就可以将 对原始数据源sourceDatas按照拼音排序,并取出索引数据源indexDatas的操作封装起来。

2. 封装原始数据源初始化(利用TinyPinyin获取全拼音),取出索引数据源indexDatas的操作。

使用时,我们先让具体的实体bean,继承自BaseIndexPinyinBean ,在getTarget()方法返回排序目标字段。本例如下:


IndexBar类 内代码:使用时会调用 IndexBar.setmSourceDatas() 方法传入原始数据源,在方法内对数据源初始化,并取出索引数据源。



3. 封装对原始数据源sourceDatas,索引数据源indexDatas的排序操作。


4. 是否需要真实的索引数据源

相关变量定义:


初始化init时,判断不需要真实的索引数据源,就用默认值(A-Z,#)

//不需要真实的索引数据源
if (!isNeedRealIndex) {    mIndexDatas = Arrays.asList(INDEX_STRING); }

使用时,如果如果真实索引数据源,调用这个方法,传入true,一定要在设置数据源setmSourceDatas(List)之前调用。


在initSourceDatas() 里,会根据这个变量往mIndexDatas里增加index。

5. IndexBar和外部联动的相关(HintTextView,和RecyclerView的LinearLayoutManager)

set方法很简单:


它们两最终都是在index触摸监听器里用到,代码上文已提及,只不过这次挪到IndexBar内部init里。

init函数如下:



总结

不管是自定义ItemDecoration还是实现右侧索引导航栏,其实大量的自定义View知识在里面。

so 要想自定义ItemDecoration玩得好,自定义View少不了。

对数据源的排序字段按照拼音排序,我们使用

TinyPinyin

https://github.com/promeG/TinyPinyin

帮助我们排序,它的特性很适合Android平台。

  • 生成的拼音不包含声调,也不处理多音字,默认一个汉字对应一个拼音;

  • 拼音均为大写;

  • 无需初始化,执行效率很高(Pinyin4J的4倍);

  • 很低的内存占用(小于30KB)。(介绍来源于其项目github)

其实不仅仅是IndexBar以及它和RecyclerView,HintTextView的联动可以封装在一起。

悬停头部ItemDecoration也可以利用 BaseIndexTagBean 类来抽象一下,不与具体的实体类耦合,将:

private List mDatas;

替换成:

private List mDatas;

点击最后 阅读原文 查看源码。

更多

每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都会有好心情。

如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。

欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: