鱼眼索引控件详解之二 —— 快速索引实现

前言:迷茫,本就是青春该有的样子 ,但不要让未来的你,讨厌现在的自己

 

相关文章:

1、《鱼眼索引控件详解之一 —— 自定义索引器》
2、《鱼眼索引控件详解之二 —— 快速索引实现》

 

上篇给大家讲了索引条的实现方式,这篇我们就利用我们做出来的索引条来做一个简单的索引效果,看最终效果图:

起初我们并不实现精确定位到listview的item的功能,我们先做一个简单的listview能跟着我们索引条滚动的效果,效果图是这样的:

从效果图中,我们可以看到,ListView确实是跟着索引条滚动了,但滚动的位置不准,这节我们就先讲讲如何让listview跟着索引条的选中位置滚动,至于精确度的问题,我们放到本篇最后再讲。

一、填充listview

1、添加Listview控件

我们先要在上篇的基础上,在主布局中main.xml添加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了

2、填充listview

在填充listview时,我们使用系统的布局android.R.layout.simple_expandable_list_item_1;这是系统自带的一个布局,它的布局非常简单,从效果图中也可以看到,它只有一个TextView,它的代码为:

<?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));

…………
}

其中:
先看OnCreate()中,我们很容易理解,通过getData()函数得到listview的数据源,然后利用构造一个ArrayAdapter实例,然后将其做为listview的Adapter设置到Listview中;这部分很容易理解,有点难度的就是在getData()函数中,构造数据源的过程;

 

 

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后的效果图是这样的:

 

从效果图中也可以看到,当前listview只是完成了填充,还不能根据索引字母改变位置。

二、初步索引listView

1、构造CustomIndexer

首先,我们知道,listview中有一个函数:

 

 

void setSelection(int position);

这个函数就可以使listview滚动到指定的position位置。但问题出来了,我们的IndexSideBar只能返回当前选中的索引位置:

mIndexSideBar.setChoosedListener(new IndexSideBar.ChooseListner() {
    @Override
    public void onChoosed(int pos,String text) {
        …………
    }
});

我们需要知道根据索引位置来找到对应的第一个item在listview中的位置!
我们写一个类辅助类CustomIndexer来专门处理根据索引位置得到所对应的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表示索引条中索引的字符数组。
我们知道他们俩的值应该分别对应getData()与IndexSideBar中的字符数组b;
另外getPositionForSection是我们构造的一个根据索引条中字母的位置来找到listview中对应Item位置的函数。

public int getPositionForSection(int section) {
    return section + 10;
}

但由于根据索引字母在索引条中的位置,来找到listview中对应item的位置,是比较难的,所以我们目前先返回一个假的索引,直接将当前字母在索引条中的位置加上10个位置返回,来当做对应的item在listview中的位置,至于精确查找到对应item的位置的问题,我们会在文章结尾讲述。

2、在MyActivity中使用CustomIndexer

使用代码如下:

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以外,额外添加了两行代码:
第一,构造CustomAdapter:

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);
    }
});

到这里,我们开篇时的效果就已经完成了!

三、使用SectionIndexer

其实,为了提示开发人员有哪些函数要写,Android给我们提供了一个接口:SectionIndexer,它里面有三个函数要求我们实现:

public interface SectionIndexer {
    /**
     * 返回索引字符数组
     */
    Object[] getSections();
    /**
     * 根据索引返回当前首项位置
     */
    int getPositionForSection(int section);
    /**
     * 根据item的位置返回当前索引的位置
     */
    int getSectionForPosition(int position);    
}

这个类中,总共有三个接口,其中
 

  • Object[] getSections():表示返回索引条中的索引字符的数组。
  • int getPositionForSection(int section):表示根据传进去的字符的位置,返回该字符在listview中第一次出现的位置。这个函数就是我们所需要的,传进去索引字符的位置,返回该索引字符在listview中的位置。
  • int getSectionForPosition(int position):根据listview中的位置,返回该位置所对应的首字母在索引条的位置。

注意,这个接口SectionIndexer只是用来提醒开发人员一般要实现哪几个函数来实现索引条的索引功能。再次强调,只是提醒!我们完全可以不使用SectionIndexer来实现,使用SectionIndexer只是更方便的知道,我们为了实现索引功能一般需要实现哪几个函数而已。当然我们并不需要将这三个函数中的每一个函数都实现,需要用到的自己实现就好,用不到的就不用实现了。
所以如果我们将CustomIndexer派生自SectionIndexer之后,它的代码应该就是这样的:

 

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来返回。
再次强调一遍:我们完全可以不用SectionIndexer来实现索引功能,使用SectionIndexer的目的就在于,Android的开发者用来提醒我们一般需要实现哪几个功能的函数来实现索引功能!

 

四、实现getPositionForSection(int section)

这部分,我们主要给大家讲讲如何实现getPositionForSection(int section)函数,即根据索引字母在索引条中的位置,精确找到该字母所对应的项在listview中的位置。
效果图如下:

 

 

 

 

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);
}

这段代码看起来比较吓人,但把握住一点:就是利用的二分查找法。下面我们仔细来看看它是如何实现二分查找法的。

1、找到start,end

前面我们说了,为了方便下一次快速找到位置,我们定义了一个Map来保存索引位置与已经找到的对应item的位置。
定义方法如下:

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。
接下来就是找到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;
这段话的思想其实就是,我们一般而言是要将二分查找的start位置定为0,但如果尽量往后的话那查找起来会更快些。所以,既然在map中没有section字符所对应item的索引,但如果能找到它前一个字母所对应索引的话,我们就不必从头开始找了,直接从它上一个字母所对应的item位置往后找即可。

int prevLetter = mIndexStrings[section-1].charAt(0);

这句话就是找到section前一个字符所对应item位置。
如果找到就将其赋值给start,即:start = Math.abs(prevLetterPos);

2、使用start、end开始二分查找

接下来就是二分查找的算法了,对应代码为:

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;

这段代码就是二分查找算法的具体实现了,没什么好讲的了,如果对二分查找不是很清楚的同学可以查一下资料。

源码在文章底部给出


这个系列到这里就结束了,文中涉及到的AlphabetIndexer,平时不怎么用到,所以就不再讲了,如果大家有兴趣可以查下相关资料
另外,这里只是非常简单的讲述了实现索引滚动的原理,对于上一篇开篇时实现中文国家索引的实现,是需要通过额外的jar包来提取中文中的首字母的,拿到首字母以后,再根据我们讲的原理修改下getPositionForSection函数即可。

 

 

 

参考文章:

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 谢谢

 

如果你喜欢我的文章,你可能更喜欢我的公众号

启舰杂谈

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值