前言:迷茫,本就是青春该有的样子 ,但不要让未来的你,讨厌现在的自己
相关文章:
1、《鱼眼索引控件详解之一 —— 自定义索引器》
2、《鱼眼索引控件详解之二 —— 快速索引实现》
上篇给大家讲了索引条的实现方式,这篇我们就利用我们做出来的索引条来做一个简单的索引效果,看最终效果图:
起初我们并不实现精确定位到listview的item的功能,我们先做一个简单的listview能跟着我们索引条滚动的效果,效果图是这样的:
从效果图中,我们可以看到,ListView确实是跟着索引条滚动了,但滚动的位置不准,这节我们就先讲讲如何让listview跟着索引条的选中位置滚动,至于精确度的问题,我们放到本篇最后再讲。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#ffffff"> <ListView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollbars="none" android:background="#000000"/> <com.example.BlogSideBar.IndexSideBar android:id="@+id/index_slide_bar" android:layout_width="23dp" android:layout_height="match_parent" android:layout_alignParentRight="true" android:layout_marginRight="10dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:background="@drawable/index_letter_bg"/> <TextView android:id="@+id/index_slide_dialog" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toLeftOf="@+id/index_slide_bar" android:background="@drawable/list_lable_screen" android:gravity="center" android:textColor="#ff5e00" android:textSize="30dp" android:visibility="invisible" android:layout_marginRight="6dp"/> </RelativeLayout>这部分没什么好讲的,也就在上篇布局的基础上添加了一个listview控件,在添加listview控件之后就是填充这个listview了
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" android:layout_width="match_parent" android:layout_height="?android:attr/listPreferredItemHeight" android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft" android:textAppearance="?android:attr/textAppearanceListItem" android:gravity="center_vertical" />下面我们来看看填充Listview的代码:
private List<String> getData(){ String[] indexeStrs = IndexSideBar.b; List<String> data = new ArrayList<String>(); for (String str:indexeStrs){ for (int i = 1;i<=10;i++){ data.add(str + i); } } return data; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //构造listview final ListView listView = (ListView)findViewById(R.id.list); List<String> datas = getData(); listView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1,datas)); ………… }其中:
private List<String> getData(){ String[] indexeStrs = IndexSideBar.b; List<String> data = new ArrayList<String>(); for (String str:indexeStrs){ for (int i = 1;i<=10;i++){ data.add(str + i); } } return data; }首先,IndexSideBar.b是我们在上篇中构造索引条所使用的数组,它的定义为:
public static String[] b = {"#", "A", "B", "C", "D", "E", "F", "G", "H", "I","J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};getData的实现就是根据索引构造出数据源,将索引中的每个字母构造10个数据:
for (String str:indexeStrs){ for (int i = 1;i<=10;i++){ data.add(str + i); } }然后将整体的data返回,将其作为listview的数据源来显示Listview的项目。
从效果图中也可以看到,当前listview只是完成了填充,还不能根据索引字母改变位置。
void setSelection(int position);这个函数就可以使listview滚动到指定的position位置。但问题出来了,我们的IndexSideBar只能返回当前选中的索引位置:
mIndexSideBar.setChoosedListener(new IndexSideBar.ChooseListner() { @Override public void onChoosed(int pos,String text) { ………… } });我们需要知道根据索引位置来找到对应的第一个item在listview中的位置!
public class CustomIndexer implements SectionIndexer { private String[] mIndexStrings; private List<String> mDatas = new ArrayList<>(); public CustomIndexer(List<String> datas,String[] indexStrs){ mDatas.addAll(datas); mIndexStrings = indexStrs; } /** * 根据索引返回当前首项位置 * @param section * @return */ public int getPositionForSection(int section) { return section + 10; } }首先是CustomIndexer的构造函数:
public CustomIndexer(List<String> datas,String[] indexStrs){ mDatas.addAll(datas); mIndexStrings = indexStrs; }其中第一个参数List<String> datas表示listview的数据源,第二个参数String[] indexStrs表示索引条中索引的字符数组。
public int getPositionForSection(int section) { return section + 10; }但由于根据索引字母在索引条中的位置,来找到listview中对应item的位置,是比较难的,所以我们目前先返回一个假的索引,直接将当前字母在索引条中的位置加上10个位置返回,来当做对应的item在listview中的位置,至于精确查找到对应item的位置的问题,我们会在文章结尾讲述。
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //构造listview final ListView listView = (ListView)findViewById(R.id.list); List<String> datas = getData(); listView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1,datas)); final CustomIndexer indexer = new CustomIndexer(datas,IndexSideBar.b); //构造sidebar mIndexSideBar = (IndexSideBar)findViewById(R.id.index_slide_bar); mIndexBlockDialog = (TextView)findViewById(R.id.index_slide_dialog); mIndexSideBar.setTextView(mIndexBlockDialog); mIndexSideBar.show(); mIndexSideBar.setChoosedListener(new IndexSideBar.ChooseListner() { @Override public void onChoosed(int pos, String text) { int ItemPos = indexer.getPositionForSection(pos); listView.setSelection(ItemPos); } }); }在这段代码中,除了填充listview和构造IndexSideBar以外,额外添加了两行代码:
final CustomIndexer indexer = new CustomIndexer(datas,b);第二:在索引条中选中某个字母时,根据该字母在索引条中的位置,通过CustomIndexer的getPositionForSection(pos)函数来得到该字母所对应的item在listview中的位置,然后通过listView.setSelection(ItemPos);将listview中的Item移动到这个位置。
mIndexSideBar.setChoosedListener(new IndexSideBar.ChooseListner() { @Override public void onChoosed(int pos, String text) { int ItemPos = indexer.getPositionForSection(pos); listView.setSelection(ItemPos); } });到这里,我们开篇时的效果就已经完成了!
public interface SectionIndexer { /** * 返回索引字符数组 */ Object[] getSections(); /** * 根据索引返回当前首项位置 */ int getPositionForSection(int section); /** * 根据item的位置返回当前索引的位置 */ int getSectionForPosition(int position); }这个类中,总共有三个接口,其中
public class CustomIndexer implements SectionIndexer { private String[] mIndexStrings; private List<String> mDatas = new ArrayList<>(); public CustomIndexer(List<String> datas,String[] indexStrs){ mDatas.addAll(datas); mIndexStrings = indexStrs; } /** * 返回索引字符数组 * @return */ @Override public String[] getSections() { return mIndexStrings; } /** * 根据索引返回当前首项位置 * @param section * @return */ @Override public int getPositionForSection(int section) { return section + 10; } /** * 根据item的位置返回当前索引的位置 * @param position * @return */ @Override public int getSectionForPosition(int position) { return 0; } }相比没有派生自SectionIndexer的代码,我们这里仍然只需要getPositionForSection这一个函数,所以其它两个函数我们也就用不到,所以我们就没有必要去实现它们。同样,我们在实现getPositionForSection时目前还是返回假的索引,直接将section加上10来返回。
Android中其实已经为我们开发了一个基于Cursor的可以实现SectionIndexer各个接口的类:AlphabetIndexer;但由于这个类是基于Cursor的,不方便使用,如果有同学有兴趣,可以搜一下AlphabetIndexer的使用方法。
我们这里防照AlphabetIndexer中实现getPositionForSection(int section)的方法,我们自己实现一个。
在实现查找时,主要使用的是二分查找法。我们假设listView的数据是排序好的,我们只需要根据当前索引的字符去到listview中去找,第一个出现的首字母是这个字符的item就行,直接把对应的item的索引返回即可。
为了下一次更容易找到,我们引入了一个保存索引位置与对应item位置的map;
下面先列出完整代码,我们再细讲:
private SparseIntArray mAlphaMap; public int getPositionForSection(int section) { final SparseIntArray alphaMap = mAlphaMap; final List<String> items = mDatas; if (items == null || mIndexStrings == null) { return 0; } if (section <= 0) { return 0; } if (section >= mIndexStrings.length) { section = mIndexStrings.length - 1; } int count = items.size(); int start = 0; int end = count; int pos; char letter = mIndexStrings[section].charAt(0); String targetLetter = Character.toString(letter); int key = letter; // 检查map是否已经保存了key为letter的对应索引,如果有的话直接返回,如果没有则进行查找 if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE))) { // Is it approximate? Using negative value to indicate that it's // an approximation and positive value when it is the accurate // position. if (pos < 0) { //如果没有,则将end设为一个极大值,以保证利用二分查找时,肯定能包含listview中所有item的索引 pos = -pos; end = pos; } else { // Not approximate, this is the confirmed start of section, return it return pos; } } //利用前一个字母的位置缩短查找距离 //由于索引条中的字母都是按顺序排列的,所以要找索引为section的字母对应item在listview中的位置,先看看它的前一个字母 //是不是在alphaMap是不是已经有保存对应item的位置了,如果有的话,我们又可以缩短查找距离,直接用前一个字母的位置做为开始即可 if (section > 0) { int prevLetter = mIndexStrings[section-1].charAt(0); int prevLetterPos = alphaMap.get(prevLetter, Integer.MIN_VALUE); if (prevLetterPos != Integer.MIN_VALUE) { start = Math.abs(prevLetterPos); } } // Now that we have a possibly optimized start and end, let's binary search pos = (end + start) / 2; while (pos < end) { // Get letter at pos String item = items.get(pos); String curName = ""; if (item != null) { curName = item.charAt(0)+""; } if (curName == null) { if (pos == 0) { break; } else { pos--; continue; } } int diff = compare(curName, targetLetter); if (diff != 0) { if (diff < 0) { //如果已经到了Listview的末尾,但仍没有索引字符对应的item,就将最末Item的索引返回. start = pos + 1; if (start >= count) { pos = count; break; } } else { end = pos; } } else { // They're the same, but that doesn't mean it's the start if (start == pos) { // This is it break; } else { // Need to go further lower to find the starting row end = pos; } } pos = (start + end) / 2; } alphaMap.put(key, pos); return pos; } /** * 首字母比较规则 * @param firstLetter 第一个字母 * @param secondLetter 第二个字母 * @return 比较返回值 */ protected int compare(String firstLetter, String secondLetter) { if (firstLetter == null||firstLetter.length() == 0) { firstLetter = " "; } else { firstLetter = firstLetter.substring(0, 1); } if (secondLetter == null||secondLetter.length() == 0) { secondLetter = " "; } else { secondLetter = secondLetter.substring(0, 1); } return firstLetter.compareTo(secondLetter); }这段代码看起来比较吓人,但把握住一点:就是利用的二分查找法。下面我们仔细来看看它是如何实现二分查找法的。
private SparseIntArray mAlphaMap;它初始化方法是写在CustomIndexer的构造函数中的,在上面并没有列出来,代码如下:
public CustomIndexer(List<String> datas, String[] indexStrs){ mDatas.addAll(datas); mIndexStrings = indexStrs; mAlphaMap = new SparseIntArray(mIndexStrings.length); }然后我们来看看如何找到二分查找中所需要的start和end位置,这部分所对应的完整代码为:
public int getPositionForSection(int section) { final SparseIntArray alphaMap = mAlphaMap; final List<String> items = mDatas; if (items == null || mIndexStrings == null) { return 0; } if (section <= 0) { return 0; } if (section >= mIndexStrings.length) { section = mIndexStrings.length - 1; } int count = items.size(); int start = 0; int end = count; int pos; char letter = mIndexStrings[section].charAt(0); String targetLetter = Character.toString(letter); int key = letter; // 检查map是否已经保存了key为letter的对应索引,如果有的话直接返回,如果没有则进行查找 if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE))) { if (pos < 0) { //如果没有,则将end设为一个极大值,以保证利用二分查找时,肯定能包含listview中所有item的索引 pos = -pos; end = pos; } else { // Not approximate, this is the confirmed start of section, return it return pos; } } if (section > 0) { int prevLetter = mIndexStrings[section-1].charAt(0); int prevLetterPos = alphaMap.get(prevLetter, Integer.MIN_VALUE); if (prevLetterPos != Integer.MIN_VALUE) { start = Math.abs(prevLetterPos); } } ………… }在函数开始,像其它函数一样,是做一些参数的异常检查:
final SparseIntArray alphaMap = mAlphaMap; final List<String> items = mDatas; if (items == null || mIndexStrings == null) { return 0; } if (section <= 0) { return 0; } if (section >= mIndexStrings.length) { section = mIndexStrings.length - 1; }没什么难度,最后一段if (section >= mIndexStrings.length) 表示如果所要查找到索引字符已经超过了我们所定义的索引字符的最大位置,就把索引条中最大位置赋给section。
char letter = mIndexStrings[section].charAt(0); String targetLetter = Character.toString(letter); int key = letter;由于section对应的是该字符在索引条中的位置,所以直接通过mIndexStrings[section].charAt(0)就可以得到这个字符了。这里非常注意的一句int key = letter;把所要查找的letter做为map的key,那查找后的结果,肯定是做为Map的value了。
if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE))) { if (pos < 0) { //如果没有,则将end设为一个极大值,以保证利用二分查找时,肯定能包含listview中所有item的索引 pos = -pos; end = pos; } else { // Not approximate, this is the confirmed start of section, return it return pos; } }不知道大家能不能理解if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE)))这句话,根据操作符的优先级来分析,首先运行的是小括号里面的pos = alphaMap.get(key, Integer.MIN_VALUE),即先根据key看是不是map中已经保存了该字符所对应的item的索引。如果有,就直接返回,如果没有,那么pos的值就是Integer.MIN_VALUE;
if (pos < 0) { //如果没有,则将end设为一个极大值,以保证利用二分查找时,肯定能包含listview中所有item的索引 pos = -pos; end = pos; }大家知道如果找不到pos的值就是Integer.MIN_VALUE,它的值是:-2147483648,对应是Int的最小值。把end所对应的位置指定为2147483648的用意就是,因为我们不知道listview中总共有多少项,但我们可以指定一个极大值来保证我们指定的end肯定要比listview中所有item多就好了。
if (section > 0) { int prevLetter = mIndexStrings[section-1].charAt(0); int prevLetterPos = alphaMap.get(prevLetter, Integer.MIN_VALUE); if (prevLetterPos != Integer.MIN_VALUE) { start = Math.abs(prevLetterPos); } }我们知道section指的是索引条上选中字母的索引,我们假设这个字母是N;那secion-1就代表的是section所对应的字母在索引条上的前一个字母,如果section对应的是N的话,那section-1所对应的就是字母M;
int prevLetter = mIndexStrings[section-1].charAt(0);这句话就是找到section前一个字符所对应item位置。
pos = (end + start) / 2; while (pos < end) { // Get letter at pos String item = items.get(pos); String curName = ""; if (item != null) { curName = item.charAt(0)+""; } if (curName == null) { if (pos == 0) { break; } else { pos--; continue; } } int diff = compare(curName, targetLetter); if (diff != 0) { if (diff < 0) { //如果已经到了Listview的末尾,但仍没有索引字符对应的item,就将最末Item的索引返回. start = pos + 1; if (start >= count) { pos = count; break; } } else { end = pos; } } else { // They're the same, but that doesn't mean it's the start if (start == pos) { // This is it break; } else { // Need to go further lower to find the starting row end = pos; } } pos = (start + end) / 2; } alphaMap.put(key, pos); return pos;这段代码就是二分查找算法的具体实现了,没什么好讲的了,如果对二分查找不是很清楚的同学可以查一下资料。
参考文章:
1、《Android系统联系人全特效实现(上),分组导航和挤压动画》
2、《 SectionIndexer---App列表之游标ListView(索引ListView)》
3、《Android实现Alphabet ListView》
4、《通讯录-AlphabetIndexer的使用》
如果本文有帮到你,记得加关注哦
源码下载地址:http://download.csdn.net/detail/harvic880925/9391747
请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/50465583 谢谢