这几天用到ListView的FastScroll的功能,没想到啊居然有Bug(BTW: 经过我的研究, 这个问题在android 4.0版本里已经解决了,因此只考虑在4.0以上环境下工作的人可以无视我这篇文章了。其实我没有验证是不是4.0才改的,但反正2.X不行,4.0可以。下面我都会说是4.0怎么样怎么样,可能不一定准确)!滚动的那个倒是正常,就是中间显示的那个当前联系人首字母提示的不正常~!我Curosr都换了,结果那些首字母还是不变。再滚动的时候就跟当前的不匹配了。啊啊怎么会有这种问题啊,逼我这种懒人去研究源码啊研究的郁闷啊有木有啊?!
好了咆哮完了,进入正题。先放个图上来:
这个东东就是快速滚动了,主要由图里右边那个滚动按钮和中间这个字母提示组成。要想使用到这个功能也挺简单的,
首先在layout中给ListView设置android:fastScrollEnabled="true" ,代码里对应的方法是setFastScrollEnabled(boolean b)。然后让你的ListAdaper实现SectionIndexer接口。运行后对List进行滚动时就可出现类似上图的效果。 不过要提醒一下,List的数据不够多时是不会出现FastScroller的,就算按上面设置了实现了也不会有。貌似是要2屏以上的数据,在滚动时才会出现FastScroller。
哎看起来是挺好的,可惜呀,当我切换数据的时候那个FastScroller却死活也刷新不了。我@$$%^#$%@,。。。研究完了源码以后断定是源码的问题,因为它只在初始化的时候获取了这个Section(就是中间显示的那些个字母)的数据~!!拜托工作中的应用是经常要给List更新数据的呀,怎么设计成这样啊。。。太挫了。哎百度谷歌一下看有什么解决办法没,悲剧了,发现有关这个问题的现成资料并不多,只好自己研究了。下面是讲源码的,源码完了是讲我的曲线救国的解决方案的。不关心源码的可直接跳去解决方案。
源码篇:
先扔两个查看源码的链接
http://www.oschina.net/code/explore/android-2.2-froyo/android/widget/FastScroller.java
http://www.oschina.net/code/explore/android-2.2-froyo/android/widget/AbsListView.java
ListView中的快速滑动其实是它由的父类:AbsListView来处理的,在这个类里保存了一个FastScroller类型的变量,FastScroller类封装了快速滑动的所有逻辑, 而AbsListView主要是创建FastScroller类型的实例并传给它一些数据和事件。让我们来看看这个FastScrolle.java,因为这里我主要关心的是中间那个当前section的显示和刷新,因此会围绕这个来讲。
private Object [] mSections; //存放Section数据,对于我的联系人应用来说,就是一堆首字母。
private String mSectionText; //存放当前要显示的section文本。
private SectionIndexer mSectionIndexer; //这个其实是我们的实现了SectionIndexer的Adapter。
FastScroller实例被创建的时候会调用init(Context context)函数。然后在这个函数里会去调用getSectionsFromIndexer()函数。这个函数就是从我们的Adapter里获取那些section数据并保存到自己的变量中的。当然了,悲催的是它就只调用了这么一次,因此哪怕你的Adapter更新数据了,它也不会更新section.(代码中还有另一个地方会调用,但是我表示判断语句的那种情况真的制造不出来,因此就当没有好了)。
void
onScroll(AbsListView view,
int
firstVisibleItem,
int
visibleItemCount,//这是很重要的一个函数。如果你没有得到想要的效果,来这里找原因。
至于说4.0是怎么解决的,基本就是在AbsListView增加了一个AdapterDataSetObserver,当数据变化时调用FastScroller的onSectionsChanged()函数(这货也是新增的)。因此实现了数据更新时同步更新FastScroller的数据。感兴趣的朋友可以去看4.0的代码。
解决篇:
先放送一个链接,我是从这里得到别人的启发的。
http://stackoverflow.com/questions/3898749/re-index-refresh-a-sectionindexer
思路就是用一个由一定数量的对象(重要!)组成的固定长度的数组,FastScroller会在初始化的时候把这个数组获取过去。当我们的数据刷新时,就去刷这个数组里的数据,先贴代码吧。
private SectionTitle[] mSectionDatas = new SectionTitle[27];
for(int i = 0; i < 27; i++){
mSectionDatas[i] = new SectionTitle();
mSectionDatas[i].title = "#";
}
/**
* Process cursor to extract useful information into some data structure
*/
private ContactSectionMapper processCursor(Cursor c) {
if (c == null || c.getCount() == 0 || c.isClosed())
return null;
/** define some variables */
SparseIntArray sectionPositionMap = new SparseIntArray();
SparseIntArray positionSectionMap = new SparseIntArray();
for(int i = 0; i < mSectionDatas.length; i++){
mSectionDatas[i].title = "";
}
String curtitle = "";
int i = 0;
int position = 0;
while (c.moveToNext()) {
position = c.getPosition();
String curLetter = getTitle(getDisplayName(c));
/** check if it is needed to insert a section title */
if (TextUtils.isEmpty(curtitle) || !TextUtils.equals(curLetter, curtitle)) {
mSectionDatas[i].title = curLetter;
sectionPositionMap.put(i, position);
curtitle = curLetter;
i++;
}
positionSectionMap.put(position, i - 1);
}
for(; i < mSectionDatas.length; i++){
mSectionDatas[i].title = curtitle;
sectionPositionMap.put(i, position);
}
ContactSectionMapper sectionMapper = new ContactSectionMapper(mSectionDatas,
sectionPositionMap, positionSectionMap);
return sectionMapper;
}
SectionTitle类:
private class SectionTitle{
public String title;
public String toString(){
return title;
}
}
Adapter: protected final class ContactsAdapter extends BaseContactsAdapter implements SectionIndexer{
private ContactSectionMapper mSectionMapper;
@Override
public Object[] getSections() {
return mSectionMapper.getSections();
}
@Override
public int getPositionForSection(int section) {
return mSectionMapper.getPositionForSection(section);
}
@Override
public int getSectionForPosition(int position) {
return mSectionMapper.getSectionForPosition(position);
}
}
ContactSectionMapper是一个实现了SectionIndexer的内部类
private class ContactSectionMapper implements SectionIndexer {
private final SparseIntArray mSectionPositionMap;
private final SparseIntArray mPositionSectionMap;
public ContactSectionMapper(SparseIntArray sectionPositionMap,
SparseIntArray positionSectionMap) {
mSectionPositionMap = sectionPositionMap;
mPositionSectionMap = positionSectionMap;
}
@Override
public Object[] getSections() {
return mSectionDatas;
}
@Override
public int getPositionForSection(int section) {
if (mSectionPositionMap == null)
return -1;
return mSectionPositionMap.get(section, -1);
}
@Override
public int getSectionForPosition(int position) {
if (mPositionSectionMap == null)
return -1;
return mPositionSectionMap.get(position, -1);
}
public boolean isSection(int position) {
int sectionIdx = getSectionForPosition(position);
int sectionPosition = getPositionForSection(sectionIdx);
return (position == sectionPosition);
}
public String getSection(int position) {
if (mSectionDatas == null)
return NONE_ENGLISH_LETTER_TITLE;
int sectionIndex = getSectionForPosition(position);
if(sectionIndex < 0 || sectionIndex >= mSectionDatas.length)
return NONE_ENGLISH_LETTER_TITLE;
return mSectionDatas[sectionIndex].toString();
}
}
SectionTitle就是一个很简单的类,主要的好处就是它的实例是一个对象,可以通过设置title来改变它包含的字符串,这样也就间接地解决了FastScroller不刷新SectionTitles列表的问题。
ProcessCursor 是一个很重要的函数,
1.直接在类变量mSectionDatas上操作。
2.设置title要按顺序来,最后一个有效的title(比如你只有A,B,F,T开头的联系人。那T就是最后一个有效的)。记得要把之后的对象的值也都设为这个title。代码里是这一段:
for(; i < mSectionDatas.length; i++){
mSectionDatas[i].title = curtitle;
sectionPositionMap.put(i, position);
}