很多情况下, 我们想要ListView上面展示的东西是可以分组的,比如联系人列表,国家列表啊,这样看起来数据的展现比较有层次感,而且也有助于我们快速定位到某一个具体的条目上,具体效果请看下图:

Android 形如TODO android todolist_数据

这是前面TodoList小demo的MainActivity,主要是来展现用户添加的任务的,在原来的基础上添加了分组的效果。

接下来我们具体来讲一下这个效果是怎么实现的。

Android 使用开源库StickyGridHeaders来实现带sections和headers的GridView显示本地图片效果

0)关于如何导进开源库,大家请参考:如何导进开源库StickyListHeaders

1)然后,我们要想清楚一件事情,即分组的ListView,是包含两部分:Header 和 Item,所以相对应的我们也要为其定义两个Layout,如下:

1.1)task_header.xml

 

[html] view plain copy

 

1. <?xml version="1.0" encoding="utf-8"?>  
2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
3. android:layout_width="match_parent"  
4. android:layout_height="match_parent"  
5. android:background="@drawable/header_selector" >  
6.   
7. <TextView  
8. android:id="@+id/tvHeader"  
9. android:layout_width="wrap_content"  
10. android:layout_height="match_parent"  
11. android:layout_gravity="start|left"  
12. android:padding="5dp"  
13. android:textColor="@android:color/white"  
14. android:textSize="17sp"  
15. android:textStyle="bold" />  
16.   
17. </RelativeLayout>

因为我们在Header上面只是展现一个日期,所以我们只需要一个TextView即可。

 

1.2)task_item.xml

 

[html] view plain copy

 

1. <?xml version="1.0" encoding="utf-8"?>  
2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
3. android:layout_width="match_parent"  
4. android:layout_height="32dp"  
5. android:descendantFocusability="blocksDescendants"  
6. android:padding="5dip">  
7.   
8. <ImageView  
9. android:padding="5dp"  
10. android:layout_centerVertical="true"  
11. android:id="@+id/ivComplete"  
12. android:layout_width="wrap_content"  
13. android:layout_height="match_parent"  
14. android:layout_alignParentLeft="true"  
15. android:layout_alignParentTop="true"  
16. android:contentDescription="@string/imageview_contentdesc"  
17. android:src="@drawable/handdraw_tick"  
18. android:visibility="gone" />  
19.   
20. <TextView  
21. android:id="@+id/tvTitle"  
22. android:layout_width="wrap_content"  
23. android:layout_height="match_parent"  
24. android:layout_toRightOf="@+id/ivComplete"  
25. android:gravity="left|center_vertical"  
26. android:padding="5dp"  
27. android:textSize="20sp" />  
28. </RelativeLayout>

在这里面,我们定义了每一个item要展现的布局,跟平常我们经常用的layout其实是一样的,大家接下来自定义的Adapter也就理解了。

 

2)第二步,跟平常绑定ListView一样,我们也需要自定义一个Adapter,称之为StickyListTaskAdapter。

我们来看一下 StickListTaskAdapter 完整的代码,如下:

 

[java] view plain copy

 

1. public class StickListTaskAdapter extends BaseAdapter   
2. implements SectionIndexer, StickyListHeadersAdapter{  
3.       
4. private LayoutInflater layoutInflater;  
5. private List<TodoTask> tasks;  
6. private int[] sectionIndices;  
7. private String[] sectionHeaders;  
8.   
9. public StickListTaskAdapter(Context context, List<TodoTask> tasks) {  
10.         layoutInflater = LayoutInflater.from(context);  
11.           
12. this.tasks = tasks;  
13.         sectionIndices = getSectionIndices();  
14.         sectionHeaders = getSectionHeaders();  
15.     }  
16.       
17. public void refresh(List<TodoTask> tasks){  
18. this.tasks = tasks;  
19.         sectionIndices = getSectionIndices();  
20.         sectionHeaders = getSectionHeaders();  
21.         notifyDataSetChanged();  
22.     }  
23.       
24. private int[] getSectionIndices() {  
25. new ArrayList<Integer>();  
26. 0).getCreateTime());  
27. 0);  
28. for (int i = 1; i < tasks.size(); i++) {  
29.             String createDate = Helper.getFormatDate(tasks.get(i).getCreateTime());  
30. if (!createDate.equals(lastCreateDate)) {  
31.                 lastCreateDate = createDate;  
32.                 sectionIndices.add(i);  
33.             }  
34.         }  
35. int[] sections = new int[sectionIndices.size()];  
36. for (int i = 0; i < sectionIndices.size(); i++) {  
37.             sections[i] = sectionIndices.get(i);  
38.         }  
39. return sections;  
40.     }  
41.       
42. private String[] getSectionHeaders() {  
43. new String[sectionIndices.length];  
44. for (int i = 0; i < sectionIndices.length; i++) {  
45.             sectionHeaders[i] = Helper.getFormatDate(tasks.get(sectionIndices[i]).getCreateTime());  
46.         }  
47. return sectionHeaders;  
48.     }  
49.   
50. @Override  
51. public int getCount() {  
52. return tasks.size();  
53.     }  
54.   
55. @Override  
56. public Object getItem(int position) {  
57. return tasks.get(position);  
58.     }  
59.   
60. @Override  
61. public long getItemId(int position) {  
62. return tasks.get(position).getId();  
63.     }  
64.   
65. @Override  
66. public View getView(int position, View convertView, ViewGroup parent) {  
67.         ViewHolder viewHolder;  
68. if (convertView == null) {  
69. new ViewHolder();  
70. null);  
71.             viewHolder.ivComplete = (ImageView)convertView.findViewById(R.id.ivComplete);  
72.             viewHolder.tvTitle = (TextView) convertView.findViewById(R.id.tvTitle);  
73.             viewHolder.tvCreateTime = (TextView) convertView.findViewById(R.id.tvCreateTime);  
74.             convertView.setTag(viewHolder);  
75. else {  
76.             viewHolder = (ViewHolder) convertView.getTag();  
77.         }             
78. if("Y".equals(tasks.get(position).getFlagCompleted())){  
79.             viewHolder.ivComplete.setVisibility(View.VISIBLE);  
80.             viewHolder.tvCreateTime.setText(Helper.getFormatDate(tasks.get(position).getCompleteTime()));  
81. else{  
82.             viewHolder.ivComplete.setVisibility(View.GONE);  
83.             viewHolder.tvCreateTime.setText(Helper.getFormatDate(tasks.get(position).getCreateTime()));  
84.         }  
85.         viewHolder.tvTitle.setText(tasks.get(position).getTitle());  
86.           
87. return convertView;  
88.     }  
89.       
90. @Override  
91. public View getHeaderView(int position, View convertView, ViewGroup parent) {  
92.         HeaderViewHolder hvh;  
93. if(convertView == null){  
94. new HeaderViewHolder();  
95. null);  
96.             hvh.tvHeader = (TextView) convertView.findViewById(R.id.tvHeader);  
97.             convertView.setTag(hvh);  
98. else{  
99.             hvh = (HeaderViewHolder)convertView.getTag();  
100.         }  
101.         hvh.tvHeader.setText(Helper.getFormatDate(tasks.get(position).getCreateTime()));  
102. return convertView;  
103.     }  
104.   
105. @Override  
106. public long getHeaderId(int position) {  
107. return Helper.changeStringDateToLong(Helper.getFormatDate(tasks.get(position).getCreateTime()));  
108.     }  
109.   
110. @Override  
111. public Object[] getSections() {  
112. // TODO Auto-generated method stub  
113. return sectionHeaders;  
114.     }  
115.   
116. @Override  
117. public int getPositionForSection(int sectionIndex) {  
118. if (sectionIndex >= sectionIndices.length) {  
119. 1;  
120. else if (sectionIndex < 0) {  
121. 0;  
122.             }  
123. return sectionIndices[sectionIndex];  
124.     }  
125.   
126. @Override  
127. public int getSectionForPosition(int position) {  
128. for (int i = 0; i < sectionIndices.length; i++) {  
129. if (position < sectionIndices[i]) {  
130. return i - 1;  
131.             }  
132.         }  
133. return sectionIndices.length - 1;  
134.     }  
135.   
136. class ViewHolder {  
137.         ImageView ivComplete;  
138.         TextView tvTitle;  
139.         TextView tvCreateTime;  
140.     }  
141.       
142. class HeaderViewHolder{  
143.         TextView tvHeader;  
144.     }  
145. }

首先我们定义了下面两个数组,并且需要在构造的时候初始化它们:

 

 

[java] view plain copy

 


1. private int[] sectionIndices;  
2. private String[] sectionHeaders;

通过构造函数,我们可以发现,我们传到这个Adapter的数据源只有一个ArrayList<TodoTask>,因为这才是真正的数据,我们分组也是基于这个数据源的。

 

但是我们要展现Header的,那么Header的数据是从哪里来的呢?所以我们在初始化的时候,就要去获得Header的数据。

大家可以看一下两个getSectionXXX的函数,可以看到在里面做了下面两件事情:

1)sectionIndices数组用来存放每一轮分组的第一个item的位置。

2)sectionHeaders数组用来存放每一个分组要展现的数据,因为能够分到同一组的item,它们肯定有一个相同且可以跟其它section区别开来的值,比如在上面,我是利用create_time来分成不同的组的,所以sectionHeaders存放的只是一个create_time。

不过大家在这里千万要注意:基于某个字段的分组,这个数据源必须是在这个字段上是有序的!

如果不是有序的,那么属于相同分组的数据就会被拆成几段了,而这个分组就没有意义了。

所以如果数据源不是有序的,那么我们在初始化获取分组的时候,也需要先将其变成有序的。

接下来,在我们平常继承BaseAdapter的情况下,我们都要去实现getView等功能,在上面也是一样的,但是我们这个Adapter还必须要实现另外两个接口:

1)StickyListHeadersAdapter

2)SectionIndexer 

我们先来看看StickyListHeaderAdapter的定义:

 

[java] view plain copy

 

1. public interface StickyListHeadersAdapter extends ListAdapter {  
2.       
3. int position, View convertView, ViewGroup parent);  
4.       
5. long getHeaderId(int position);  
6. }

这是开源库提供的接口,因为我们需要添加Header,所以我们必须在Adapter中也返回一个Header的View,这其实跟实现getView是一样的道理的,都挺好理解的。

 

所以在getHeaderView里面就会用到我们一开始新定义的那个task_header.xml了,同样的,为了实现优化,也会利用一个HeaderViewHolder。

另外一个接口就是SectionIndexer了,它有三个方法要实现,如下:

 

[java] view plain copy

 

1. public interface SectionIndexer {  
2.   
3.     Object[] getSections();  
4.   
5. int getPositionForSection(int sectionIndex);  
6.   
7. int getSectionForPosition(int position);  
8. }

看代码的实现,可以发现:

 

getSections:返回的其实就是Header上面要展示的数据,在这里其实就是sectionHeaders了,存放的是create_time的数据。

getPositionForSection:返回的是这个section数据在List<TodoTask>这个基础数据源中的位置,因为section中的数据其实也是从List<TodoTask>中获取到的。

getSectionForPosition:则是通过在基础数据源List<TodoTask>中的位置找出对应的Section中的数据,原因同上。

那么上面这两个函数的作用在哪?

大家有没有发现,当同一个分组的数据在滚动的时候,最上面的分组并不会变化,只有当滑到其它分组的时候,这个分组才会被新的分组给替换掉。这个效果实现的原理就在这里了,虽然我没有看过源代码,但是我认为,在每一个item滚动的时候,都会找出其对应的分组,然后显示在最上方,如果都是属于同一个分组的话,那么最上面的显示的当然一直都是这个分组对应的Header了。

综上所述,为了实现Sticky和分组的效果,我们就要在原来继承BaseAdapter的基础上再实现多两个接口,并实现对应的逻辑。

那么如何在Activity中使用呢?请看下面的代码:

在xml中定义:

 

[html] view plain copy

 


1. <se.emilsjolander.stickylistheaders.StickyListHeadersListView  
2. android:id="@+id/lvTasks"  
3. android:layout_width="match_parent"  
4. android:layout_height="match_parent"  
5. android:background="@drawable/todo_bg"  
6. android:clipToPadding="false"  
7. android:divider="#44FFFFFF"  
8. android:dividerHeight="1dp"  
9. android:drawSelectorOnTop="true"  
10. android:fastScrollEnabled="true"  
11. android:overScrollMode="never"  
12. android:padding="16dp"  
13. android:scrollbarStyle="outsideOverlay" />
  1.   

在MainActivity中使用:

 

 

[java] view plain copy

 

1. lvTasks = (StickyListHeadersListView) findViewById(R.id.lvTasks);  
2. taskAdapter = new StickListTaskAdapter(this, tasks);  
3. lvTasks.setAdapter(taskAdapter);  
4. lvTasks.setDrawingListUnderStickyHeader(true);  
5. lvTasks.setAreHeadersSticky(true);  
6. lvTasks.setOnItemLongClickListener(onItemLongClickListener);  
7. lvTasks.setOnItemClickListener(onItemClickListener);

而开源库中StickyListHeadersListView还提供了几个接口,可以让我们在Activity中去实现,不过这些就有待大家自己去慢慢学习了。

 

 

[java] view plain copy

 

1. public class StickyListHeadersListView extends FrameLayout {  
2.   
3. public interface OnHeaderClickListener {  
4. public void onHeaderClick(StickyListHeadersListView l, View header,  
5. int itemPosition, long headerId, boolean currentlySticky);  
6.     }  
7.   
8. /** 
9.      * Notifies the listener when the sticky headers top offset has changed. 
10.      */  
11. public interface OnStickyHeaderOffsetChangedListener {  
12. /** 
13.          * @param l      The view parent 
14.          * @param header The currently sticky header being offset. 
15.          *               This header is not guaranteed to have it's measurements set. 
16.          *               It is however guaranteed that this view has been measured, 
17.          *               therefor you should user getMeasured* methods instead of 
18.          *               get* methods for determining the view's size. 
19.          * @param offset The amount the sticky header is offset by towards to top of the screen. 
20.          */  
21. public void onStickyHeaderOffsetChanged(StickyListHeadersListView l, View header, int offset);  
22.     }  
23.   
24. /** 
25.      * Notifies the listener when the sticky header has been updated 
26.      */  
27. public interface OnStickyHeaderChangedListener {  
28. /** 
29.          * @param l             The view parent 
30.          * @param header        The new sticky header view. 
31.          * @param itemPosition  The position of the item within the adapter's data set of 
32.          *                      the item whose header is now sticky. 
33.          * @param headerId      The id of the new sticky header. 
34.          */  
35. public void onStickyHeaderChanged(StickyListHeadersListView l, View header,  
36. int itemPosition, long headerId);  
37.   
38.     }

结束。