最近在公司项目中看到用 SectionIndexer 来实现 ListView 的分组滚动实现通讯录导航的效果,这里我单独的写个 demo 出来。

SectionIndexer:根据官方文档说,它是一个给 Adapter 去实现、用来在 AbsListView 中实现 section 之间的快速滚动的接口(翻译拙计见谅)。一个 section 相当于一组具有共同特性的 list 数据。比如,它们有相同的首字母。

知道它的定义了,那怎么使用它呢?本人口拙,咱们还是上代码说话~

先看要实现的效果图:

SectionIndexerDemo.gif

  1. activity 的布局文件
activity_main.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ListView
android:id="@+id/main_lv"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<!--显示在屏幕中央的字母-->
<TextView
android:id="@+id/main_mask_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="24dp"
android:textSize="40sp"
android:textColor="@android:color/white"
android:background="#33000000"
android:visibility="gone"
tools:visibility="visible"
tools:text="A"/>

<!--字母导航栏-->
<com.example.shiyan.sectionindexerdemo.SideBar
android:id="@+id/main_sb"
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true" />
</RelativeLayout>
  1. ListView 的 item 布局
main_listview_item.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<!--section,表示组-->
<TextView
android:id="@+id/section_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="22sp"
android:textColor="@android:color/white"
android:background="@android:color/holo_green_light"
tools:text="section"/>

<TextView
android:id="@+id/normal_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:textSize="20sp"
tools:text="text"/>
</LinearLayout>
  1. 页面右侧的字母导航栏,继承了 View,核心部分在 onTouchEvent 方法中
SideBar.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public class SideBar extends View {

private final char[] letters = new char[]{'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'};
private Paint paint;//绘制字母的画笔
private SectionIndexer sectionIndexer;
private ListView mListView;
private int drawWidth;//要绘制的单个字母的宽度
private int drawHeight;//要绘制的单个字母的高度
private int focusedIndex = -1;//点击选中的索引

//监听当前点击的字母的接口,供外部实现
private OnTouchChangedListener listener;
public void setOnTouchChangedListener(OnTouchChangedListener listener) {
this.listener = listener;
}
public interface OnTouchChangedListener {
void onTouchDown(char c);
void onTouchUp();
}

public void setListView(ListView mListView) {
this.mListView = mListView;
sectionIndexer = (SectionIndexer) mListView.getAdapter();
}

public SideBar(Context context) {
super(context);
init();
}

public SideBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public SideBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}

private void init() {
//设置字母的画笔属性
paint = new Paint();
paint.setColor(Color.GRAY);
paint.setTextSize(DisplayUtil.sp2px(getContext(), 18f));
paint.setTextAlign(Paint.Align.CENTER);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
drawWidth = getMeasuredWidth() / 2;//宽度为整个SideBar宽度的一半
drawHeight = getMeasuredHeight() / letters.length;//高度为整个SideBar高度除以索引的个数
}

@Override
public void onDraw(Canvas canvas) {
for (int i = 0; i < letters.length; i++) {
canvas.drawText(String.valueOf(letters[i]), drawWidth, drawHeight + (i * drawHeight), paint);
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
int pointerY = (int) event.getY();
//点击的y坐标 / 整体高度 * 数组的长度 = 数组的某一索引
int selectedIndex = pointerY / drawHeight;
if (selectedIndex >= letters.length) {
selectedIndex = letters.length - 1;
} else if (selectedIndex < 0) {
selectedIndex = 0;
}

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//点击时设置半透明的背景色
setBackgroundColor(Color.parseColor("#33000000"));
case MotionEvent.ACTION_MOVE:
if (sectionIndexer == null) {
sectionIndexer = (SectionIndexer) mListView.getAdapter();
}
//根据数组中的元素获取对应的组位置
int position = sectionIndexer.getPositionForSection(letters[selectedIndex]);
if (position == -1) {
return true;
}
//改变当前ListView的所处的位置
mListView.setSelection(position);
//重绘SideBar
invalidate();

//供外部实现,监听当前点击的字母
if (null != listener) {
listener.onTouchDown(letters[selectedIndex]);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//松开手指时取消背景色
setBackgroundResource(android.R.color.transparent);
//重绘SideBar
invalidate();

if (null != listener) {
listener.onTouchUp();
}
break;
}

return true;
}

}
  1. ListView 的 Adapter,在这里我们实现了 SectionIndexer 接口,需要实现接口中的三个方法,见代码
MyAdapter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
public class MyAdapter extends BaseAdapter implements SectionIndexer {

private Context context;
private LayoutInflater layoutInflater;
private List<String> stringList;

public MyAdapter(Context context, List<String> stringList) {
layoutInflater = LayoutInflater.from(context);
this.stringList = stringList;
this.context = context;
}

@Override
public int getCount() {
return stringList.size();
}

@Override
public Object getItem(int position) {
return stringList.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (null == convertView) {
convertView = layoutInflater.inflate(R.layout.main_listview_item, parent, false);

viewHolder = new ViewHolder();
viewHolder.normalTv = (TextView) convertView.findViewById(R.id.normal_tv);//普通项view
viewHolder.sectionTv = (TextView) convertView.findViewById(R.id.section_tv);//组view

convertView.setTag(viewHolder);
}
else {
viewHolder = (ViewHolder) convertView.getTag();
}

String label = stringList.get(position);
setSectionTv(position, viewHolder, label);
setNormalTv(viewHolder, label);

return convertView;
}

private void setNormalTv(ViewHolder viewHolder, String label) {
viewHolder.normalTv.setText(label);
}

private void setSectionTv(int position, ViewHolder viewHolder, String label) {
//获取每个item字符串的头一个字符
char firstChar = label.toUpperCase().charAt(0);
//若为第一个位置直接设置组view就行
if (position == 0) {
viewHolder.sectionTv.setVisibility(View.VISIBLE);
viewHolder.sectionTv.setText(label.substring(0, 1).toUpperCase());
}
//若不是,需判断当前item首字母与上一个item首字母是否一致,再设置组view
else {
String preLabel = stringList.get(position - 1);
//获取上一个item的首字母
char preFirstChar = preLabel.toUpperCase().charAt(0);
if (firstChar != preFirstChar) {
viewHolder.sectionTv.setVisibility(View.VISIBLE);
viewHolder.sectionTv.setText(label.substring(0, 1).toUpperCase());
} else {
//若与上一个item首字母一致则不需要重复设置组view
viewHolder.sectionTv.setVisibility(View.GONE);
}
}
}

@Override
public int getPositionForSection(int section) {
//根据组信息获取索引
for (int i = 0; i < stringList.size(); i++) {
String str = stringList.get(i);
char firstChar = str.toUpperCase().charAt(0);
if (firstChar == section) {
return i;
}
}
return 0;
}

@Override
public int getSectionForPosition(int position) {
//根据索引获取组信息,这里不做处理
return 0;
}

@Override
public Object[] getSections() {
//获取组信息的数组,比如这里可以返回char[]{'A','B',...}
return null;
}

private final class ViewHolder {
TextView normalTv;
TextView sectionTv;
}
}
  1. 提供的列表数据若未按字母排序,需要按字母排序。
MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class MainActivity extends AppCompatActivity {

//要显示的列表数据,未按字母顺序排列
final String[] strings = {"ooo","abb","zzz","ppp","bcc","ppq","eee","eff","fgg","sss","ghh","hhh","iii","vvv",
"jkk","jkl","kkk","yyy","lll","mmm","nnn","aaa","bbb","bdd","qqq","qrr","rrr","ggg","srr","ttt","tfg","uuu",
"jjj","www","www","wwe","wwg","xxt","xxx","kin","acc","was","wtg","wfg","brg","hqq"};

ListView mListView;
TextView maskTv;//显示在屏幕中央的字母浮层
SideBar mSideBar;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViews();
initData();
}

//初始化列表数据
private List<String> initList() {
List<String> stringList = new ArrayList<>();
for (int i = 0; i < strings.length; i++) {
stringList.add(strings[i]);
}
//对列表数据进行按字母排序
Collections.sort(stringList);
return stringList;
}

private void initData() {
MyAdapter adapter = new MyAdapter(this, initList());
mListView.setAdapter(adapter);
mSideBar.setListView(mListView);
mSideBar.setOnTouchChangedListener(new SideBar.OnTouchChangedListener() {
@Override
public void onTouchDown(char c) {
maskTv.setVisibility(View.VISIBLE);
maskTv.setText(c+"");
}
@Override
public void onTouchUp() {
maskTv.setVisibility(View.GONE);
}
});
}

private void findViews() {
mListView = (ListView) findViewById(R.id.main_lv);
maskTv = (TextView) findViewById(R.id.main_mask_tv);
mSideBar = (SideBar) findViewById(R.id.main_sb);
}

}