博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running
好久没写博文了,最近的东西都写在笔记上了,上班了,时间也不太多,就写一点在项目中遇到的问题吧。
废话不多说,我在开发中,遇到一个需求,一个类似饿了么App的城市选择器,界面的话,差不多就这样子
其实,开起来这个功能不难,但是有很多坑需要踩,下面我就来介绍一下我踩过的坑吧。
由于博主之前也写过类似的demo,只能算demo,而且当时还是用到 ListView 来处理的,所以在项目中,肯定要改为 RecyclerView。这样一改的话,之前没遇到的坑,就扑面而来了。实现细节我就不多说了,可以看我之前写的一篇文章,详细的介绍了如何实现这种效果。自定义 View 之联系人字母索引及定位效果
首先呢,要从这个demo中,将ListView转为RecyclerView的方式,第一个需要踩的坑是字母分类,就是城市列表头顶的一个字母 A…Z
按之前 adapter 的布局文件来处理,如这样的
我们适配器布局,其实是两个 TextView,然后顶部用于显示字母,如果顶部的字母都是 A,或者相同的,我们就将后面的 A 隐藏,直到这个字母 A 开头的城市结束,然后显示 B 分类。
这样在 ListView 的适配器中设置,可能没什么问题,我们获取前一个 prePosition 和当前 curPosition 的字母,对比一下是否相同,如果相同,就隐藏 curPosition 的字母,就是将他的 visible 设置为 GONE 即可。
而在 RecyclerView 中,这样设置是会出现问题的,而导致的原因就是 RecyclerView 的复用机制,我给大家画一张图就明白了,代码例如这样的(伪代码):
String preLetter
public void bindData(){
String curLetter = item.getLetter()
if(preLetter.eq(curLetter)){
// ...
tv.setVisible(GONE)
}else{
preLetter = item.getLetter()
}
}
我们往下滚动列表,上面这样写的逻辑是没有任何问题的。但是,如果列表往上滚动的话,就会产生问题,大家可以想象一下数据倒过来的情况,它造成的问题是这样的:
Item 的复用,导致数据需要重新绑定,在 B 类字母开头的城市过渡到 A 类时,直接将 A 类的最后一个城市头上标记字母 A,而不是第一个。
所以呢,这就判断逻辑肯定就不行,因为逻辑复用问题,我的解决方案就是从数据源入手,在数据源进行区分数据,这里就提供一个参考,我是这样做的:在 CityBean 类中,添加一个属性变量,用于区分类别,比如添加一个 private string letter 变量,我们在数据源的时候,就将数据做一个区分。例如,A 类字母开头的城市,为第一个 letter 属性赋值为 “A”,其他同一A类的城市,就赋值为 “” 或者 NuLL 都可以,这样的话,我们就将数据源给定死了,我就不管你 item 爱怎么复用,我 CityBean 中含 letter 属性有值的,就将 TextView 设置 Visiable,没有的话,就直接 GONE 掉即可。
这个方法想必大家都熟悉,将 RecyclerView item 定位到某一个 position 显示,那这个方法怎么就变成坑了呢。其实,还是从 listView 的setSelection(int position) 这个方法说起。setSelection(10) 的话,其实就是将 item 显示在第 10 条,并且 item 处于当前屏幕的顶部。然而,recyclerView 的 scrollToPosition(int position)方法并不是这样子,直接看图吧:
仔细看图,列表往下滑动的时候,字母分类跑到了底部去了,往上滑动是正常的。仔细观察,往下滑动的时候,它每次其实是偏移了一个屏幕可见的 item 数量,这就想起来 LayoutManager 中的 findFirstVisible 和 findLastVisible 方法了,既然你偏移了一个可见区域的 item 数量,那我就用
findLastVisible - findFirstVisible 不就行了吗。可是,实时真的可以吗?
因为我是在模拟器上运行的,发现这样一减的操作,别说,还真管用。后来,由于要发布到手机上测试一下,发现就崩了。
什么情况,我明明算好了可是区域的 item 数量,怎么又这样了?
很疑惑,我又看了代码,是这样写的
cityIndex.setOverlayTextView(tvLetter)
var prePosition = 0
cityIndex.setOnIndexChangedListener { letter, position ->
cityData.forEachIndexed { index, cityPicker ->
if (letter.equals(cityPicker.letter)) {
if (prePosition > position) {
rvCityPicker.scrollToPosition(index + 1)
} else if (prePosition < position) {
rvCityPicker.scrollToPosition(index + (manager.findLastVisibleItemPosition() - manager.findFirstVisibleItemPosition()))
}
prePosition = position
}
}
}
一脸懵逼,然后就去 google 了一下,发现 findFirstVisibleItemPosition 和 findLastVisibleItemPosition 居然存在在偏差问题
具体我是看了这篇文章作者写的 https://www.jianshu.com/p/ff6082c0867e
后来我觉得这种决解方式太麻烦了,于是又找到了一种更好的解决方法。什么?LayoutManager 已经为我们处理了这种情况了!只需要一行代码即可:
cityIndex.setOverlayTextView(tvLetter)
cityIndex.setOnIndexChangedListener { letter, position ->
cityData.forEachIndexed { index, cityPicker ->
if (letter.equals(cityPicker.letter)) {
manager.scrollToPositionWithOffset(index + 1, 0)
}
}
}
好吧,还真没用过这个方法。下面来介绍一下这个方法吧
public void scrollToPositionWithOffset(int position, int offset) {
mPendingScrollPosition = position;
mPendingScrollPositionOffset = offset;
if (mPendingSavedState != null) {
mPendingSavedState.invalidateAnchor();
}
requestLayout();
}
第一个 position 就很容易懂了,就是我们要移动的 item position,第二个参数是:item 项的开始边缘与 recyclerview 之间的距离(以像素为单位),我们设置为 0 即可。
通过这个方法,RecyclerView 可以愉快的让我们滚动到 position 并且显示在顶部了,也解决了通过自己计算 findFirstVisibleItemPosition 和 findLastVisibleItemPosition 偏移问题。来看看效果吧
好了。本篇文章基本就结束了,目前已经踩的坑基本就这些了。