很多情况下, 我们想要ListView上面展示的东西是可以分组的,比如联系人列表,国家列表啊,这样看起来数据的展现比较有层次感,而且也有助于我们快速定位到某一个具体的条目上,具体效果请看下图:
这是前面TodoList小demo的MainActivity,主要是来展现用户添加的任务的,在原来的基础上添加了分组的效果。
接下来我们具体来讲一下这个效果是怎么实现的。
这是利用开源库StickyListHeaders(传送门:https://github.com/emilsjolander/StickyListHeaders)来实现的,这个实现的效果是基于ListView的,而其实也有关于GridView而实现的分组的效果,大家可以参考一下xiaanming的博客(他的文章名字都很长。。。):
Android 使用开源库StickyGridHeaders来实现带sections和headers的GridView显示本地图片效果
0)关于如何导进开源库,大家请参考:如何导进开源库StickyListHeaders
1)然后,我们要想清楚一件事情,即分组的ListView,是包含两部分:Header 和 Item,所以相对应的我们也要为其定义两个Layout,如下:
1.1)task_header.xml
<div>
<embed id="ZeroClipboardMovie_1" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_1">
</embed>
</div>
</div>
- <span class="tag"><?</span><span class="tag-name">xml</span> <span class="attribute">version</span>=<span class="attribute-value">“1.0”</span> <span class="attribute">encoding</span>=<span class="attribute-value">“utf-8”</span><span class="tag">?></span>
- <span class="tag"><</span><span class="tag-name">RelativeLayout</span> <span class="attribute">xmlns:android</span>=<span class="attribute-value">“http://schemas.android.com/apk/res/android”</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:background</span>=<span class="attribute-value">“@drawable/header_selector”</span> <span class="tag">></span>
-
- <span class="tag"><</span><span class="tag-name">TextView</span>
- <span class="attribute">android:id</span>=<span class="attribute-value">“@+id/tvHeader”</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“wrap_content”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:layout_gravity</span>=<span class="attribute-value">“start|left”</span>
- <span class="attribute">android:padding</span>=<span class="attribute-value">“5dp”</span>
- <span class="attribute">android:textColor</span>=<span class="attribute-value">“@android:color/white”</span>
- <span class="attribute">android:textSize</span>=<span class="attribute-value">“17sp”</span>
- <span class="attribute">android:textStyle</span>=<span class="attribute-value">“bold”</span> <span class="tag">/></span>
-
- <span class="tag"></</span><span class="tag-name">RelativeLayout</span><span class="tag">></span>
因为我们在Header上面只是展现一个日期,所以我们只需要一个TextView即可。
1.2)task_item.xml
<div>
<embed id="ZeroClipboardMovie_2" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_2">
</embed>
</div>
</div>
- <span class="tag"><?</span><span class="tag-name">xml</span> <span class="attribute">version</span>=<span class="attribute-value">“1.0”</span> <span class="attribute">encoding</span>=<span class="attribute-value">“utf-8”</span><span class="tag">?></span>
- <span class="tag"><</span><span class="tag-name">RelativeLayout</span> <span class="attribute">xmlns:android</span>=<span class="attribute-value">“http://schemas.android.com/apk/res/android”</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“32dp”</span>
- <span class="attribute">android:descendantFocusability</span>=<span class="attribute-value">“blocksDescendants”</span>
- <span class="attribute">android:padding</span>=<span class="attribute-value">“5dip”</span><span class="tag">></span>
-
- <span class="tag"><</span><span class="tag-name">ImageView</span>
- <span class="attribute">android:padding</span>=<span class="attribute-value">“5dp”</span>
- <span class="attribute">android:layout_centerVertical</span>=<span class="attribute-value">“true”</span>
- <span class="attribute">android:id</span>=<span class="attribute-value">“@+id/ivComplete”</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“wrap_content”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:layout_alignParentLeft</span>=<span class="attribute-value">“true”</span>
- <span class="attribute">android:layout_alignParentTop</span>=<span class="attribute-value">“true”</span>
- <span class="attribute">android:contentDescription</span>=<span class="attribute-value">“@string/imageview_contentdesc”</span>
- <span class="attribute">android:src</span>=<span class="attribute-value">“@drawable/handdraw_tick”</span>
- <span class="attribute">android:visibility</span>=<span class="attribute-value">“gone”</span> <span class="tag">/></span>
-
- <span class="tag"><</span><span class="tag-name">TextView</span>
- <span class="attribute">android:id</span>=<span class="attribute-value">“@+id/tvTitle”</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“wrap_content”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:layout_toRightOf</span>=<span class="attribute-value">“@+id/ivComplete”</span>
- <span class="attribute">android:gravity</span>=<span class="attribute-value">“left|center_vertical”</span>
- <span class="attribute">android:padding</span>=<span class="attribute-value">“5dp”</span>
- <span class="attribute">android:textSize</span>=<span class="attribute-value">“20sp”</span> <span class="tag">/></span>
- <span class="tag"></</span><span class="tag-name">RelativeLayout</span><span class="tag">></span>
在这里面,我们定义了每一个item要展现的布局,跟平常我们经常用的layout其实是一样的,大家接下来自定义的Adapter也就理解了。
2)第二步,跟平常绑定ListView一样,我们也需要自定义一个Adapter,称之为StickyListTaskAdapter。
我们来看一下 StickListTaskAdapter 完整的代码,如下:
<div>
<embed id="ZeroClipboardMovie_3" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_3">
</embed>
</div>
</div>
- <span class="keyword">public</span> <span class="keyword">class</span> StickListTaskAdapter <span class="keyword">extends</span> BaseAdapter
- <span class="keyword">implements</span> SectionIndexer, StickyListHeadersAdapter{
-
- <span class="keyword">private</span> LayoutInflater layoutInflater;
- <span class="keyword">private</span> List<TodoTask> tasks;
- <span class="keyword">private</span> <span class="keyword">int</span>[] sectionIndices;
- <span class="keyword">private</span> String[] sectionHeaders;
-
- <span class="keyword">public</span> StickListTaskAdapter(Context context, List<TodoTask> tasks) {
- layoutInflater = LayoutInflater.from(context);
-
- <span class="keyword">this</span>.tasks = tasks;
- sectionIndices = getSectionIndices();
- sectionHeaders = getSectionHeaders();
- }
-
- <span class="keyword">public</span> <span class="keyword">void</span> refresh(List<TodoTask> tasks){
- <span class="keyword">this</span>.tasks = tasks;
- sectionIndices = getSectionIndices();
- sectionHeaders = getSectionHeaders();
- notifyDataSetChanged();
- }
-
- <span class="keyword">private</span> <span class="keyword">int</span>[] getSectionIndices() {
- List<Integer> sectionIndices = <span class="keyword">new</span> ArrayList<Integer>();
- String lastCreateDate = Helper.getFormatDate(tasks.get(<span class="number"></span>).getCreateTime());
- sectionIndices.add(<span class="number"></span>);
- <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i < tasks.size(); i++) {
- String createDate = Helper.getFormatDate(tasks.get(i).getCreateTime());
- <span class="keyword">if</span> (!createDate.equals(lastCreateDate)) {
- lastCreateDate = createDate;
- sectionIndices.add(i);
- }
- }
- <span class="keyword">int</span>[] sections = <span class="keyword">new</span> <span class="keyword">int</span>[sectionIndices.size()];
- <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number"></span>; i < sectionIndices.size(); i++) {
- sections[i] = sectionIndices.get(i);
- }
- <span class="keyword">return</span> sections;
- }
-
- <span class="keyword">private</span> String[] getSectionHeaders() {
- String[] sectionHeaders = <span class="keyword">new</span> String[sectionIndices.length];
- <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number"></span>; i < sectionIndices.length; i++) {
- sectionHeaders[i] = Helper.getFormatDate(tasks.get(sectionIndices[i]).getCreateTime());
- }
- <span class="keyword">return</span> sectionHeaders;
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">int</span> getCount() {
- <span class="keyword">return</span> tasks.size();
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> Object getItem(<span class="keyword">int</span> position) {
- <span class="keyword">return</span> tasks.get(position);
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">long</span> getItemId(<span class="keyword">int</span> position) {
- <span class="keyword">return</span> tasks.get(position).getId();
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> View getView(<span class="keyword">int</span> position, View convertView, ViewGroup parent) {
- ViewHolder viewHolder;
- <span class="keyword">if</span> (convertView == <span class="keyword">null</span>) {
- viewHolder = <span class="keyword">new</span> ViewHolder();
- convertView = layoutInflater.inflate(R.layout.task_item, <span class="keyword">null</span>);
- viewHolder.ivComplete = (ImageView)convertView.findViewById(R.id.ivComplete);
- viewHolder.tvTitle = (TextView) convertView.findViewById(R.id.tvTitle);
- viewHolder.tvCreateTime = (TextView) convertView.findViewById(R.id.tvCreateTime);
- convertView.setTag(viewHolder);
- } <span class="keyword">else</span> {
- viewHolder = (ViewHolder) convertView.getTag();
- }
- <span class="keyword">if</span>(<span class="string">“Y”</span>.equals(tasks.get(position).getFlagCompleted())){
- viewHolder.ivComplete.setVisibility(View.VISIBLE);
- viewHolder.tvCreateTime.setText(Helper.getFormatDate(tasks.get(position).getCompleteTime()));
- }<span class="keyword">else</span>{
- viewHolder.ivComplete.setVisibility(View.GONE);
- viewHolder.tvCreateTime.setText(Helper.getFormatDate(tasks.get(position).getCreateTime()));
- }
- viewHolder.tvTitle.setText(tasks.get(position).getTitle());
-
- <span class="keyword">return</span> convertView;
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> View getHeaderView(<span class="keyword">int</span> position, View convertView, ViewGroup parent) {
- HeaderViewHolder hvh;
- <span class="keyword">if</span>(convertView == <span class="keyword">null</span>){
- hvh = <span class="keyword">new</span> HeaderViewHolder();
- convertView = layoutInflater.inflate(R.layout.task_header, <span class="keyword">null</span>);
- hvh.tvHeader = (TextView) convertView.findViewById(R.id.tvHeader);
- convertView.setTag(hvh);
- }<span class="keyword">else</span>{
- hvh = (HeaderViewHolder)convertView.getTag();
- }
- hvh.tvHeader.setText(Helper.getFormatDate(tasks.get(position).getCreateTime()));
- <span class="keyword">return</span> convertView;
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">long</span> getHeaderId(<span class="keyword">int</span> position) {
- <span class="keyword">return</span> Helper.changeStringDateToLong(Helper.getFormatDate(tasks.get(position).getCreateTime()));
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> Object[] getSections() {
- <span class="comment">// TODO Auto-generated method stub</span>
- <span class="keyword">return</span> sectionHeaders;
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">int</span> getPositionForSection(<span class="keyword">int</span> sectionIndex) {
- <span class="keyword">if</span> (sectionIndex >= sectionIndices.length) {
- sectionIndex = sectionIndices.length – <span class="number">1</span>;
- } <span class="keyword">else</span> <span class="keyword">if</span> (sectionIndex < <span class="number"></span>) {
- sectionIndex = <span class="number"></span>;
- }
- <span class="keyword">return</span> sectionIndices[sectionIndex];
- }
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">int</span> getSectionForPosition(<span class="keyword">int</span> position) {
- <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number"></span>; i < sectionIndices.length; i++) {
- <span class="keyword">if</span> (position < sectionIndices[i]) {
- <span class="keyword">return</span> i – <span class="number">1</span>;
- }
- }
- <span class="keyword">return</span> sectionIndices.length – <span class="number">1</span>;
- }
-
- <span class="keyword">class</span> ViewHolder {
- ImageView ivComplete;
- TextView tvTitle;
- TextView tvCreateTime;
- }
-
- <span class="keyword">class</span> HeaderViewHolder{
- TextView tvHeader;
- }
- }
<div>
<embed id="ZeroClipboardMovie_4" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_4">
</embed>
</div>
</div>
- <span class="keyword">private</span> <span class="keyword">int</span>[] sectionIndices;
- <span class="keyword">private</span> String[] sectionHeaders;
通过构造函数,我们可以发现,我们传到这个Adapter的数据源只有一个ArrayList
但是我们要展现Header的,那么Header的数据是从哪里来的呢?所以我们在初始化的时候,就要去获得Header的数据。
大家可以看一下两个getSectionXXX的函数,可以看到在里面做了下面两件事情:
1)sectionIndices数组用来存放每一轮分组的第一个item的位置。
2)sectionHeaders数组用来存放每一个分组要展现的数据,因为能够分到同一组的item,它们肯定有一个相同且可以跟其它section区别开来的值,比如在上面,我是利用create_time来分成不同的组的,所以sectionHeaders存放的只是一个create_time。
不过大家在这里千万要注意:基于某个字段的分组,这个数据源必须是在这个字段上是有序的!
如果不是有序的,那么属于相同分组的数据就会被拆成几段了,而这个分组就没有意义了。
所以如果数据源不是有序的,那么我们在初始化获取分组的时候,也需要先将其变成有序的。
接下来,在我们平常继承BaseAdapter的情况下,我们都要去实现getView等功能,在上面也是一样的,但是我们这个Adapter还必须要实现另外两个接口:
1)StickyListHeadersAdapter
2)SectionIndexer
我们先来看看StickyListHeaderAdapter的定义:
<div>
<embed id="ZeroClipboardMovie_5" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_5">
</embed>
</div>
</div>
- <span class="keyword">public</span> <span class="keyword">interface</span> StickyListHeadersAdapter <span class="keyword">extends</span> ListAdapter {
-
- View getHeaderView(<span class="keyword">int</span> position, View convertView, ViewGroup parent);
-
- <span class="keyword">long</span> getHeaderId(<span class="keyword">int</span> position);
- }
所以在getHeaderView里面就会用到我们一开始新定义的那个task_header.xml了,同样的,为了实现优化,也会利用一个HeaderViewHolder。
另外一个接口就是SectionIndexer了,它有三个方法要实现,如下:
<div>
<embed id="ZeroClipboardMovie_6" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_6">
</embed>
</div>
</div>
- <span class="keyword">public</span> <span class="keyword">interface</span> SectionIndexer {
-
- Object[] getSections();
-
- <span class="keyword">int</span> getPositionForSection(<span class="keyword">int</span> sectionIndex);
-
- <span class="keyword">int</span> getSectionForPosition(<span class="keyword">int</span> position);
- }
看代码的实现,可以发现:
getSections:返回的其实就是Header上面要展示的数据,在这里其实就是sectionHeaders了,存放的是create_time的数据。
getPositionForSection:返回的是这个section数据在List
getSectionForPosition:则是通过在基础数据源List
那么上面这两个函数的作用在哪?
大家有没有发现,当同一个分组的数据在滚动的时候,最上面的分组并不会变化,只有当滑到其它分组的时候,这个分组才会被新的分组给替换掉。这个效果实现的原理就在这里了,虽然我没有看过源代码,但是我认为,在每一个item滚动的时候,都会找出其对应的分组,然后显示在最上方,如果都是属于同一个分组的话,那么最上面的显示的当然一直都是这个分组对应的Header了。
综上所述,为了实现Sticky和分组的效果,我们就要在原来继承BaseAdapter的基础上再实现多两个接口,并实现对应的逻辑。
那么如何在Activity中使用呢?请看下面的代码:
在xml中定义:
<div>
<embed id="ZeroClipboardMovie_7" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_7">
</embed>
</div>
</div>
- <span class="tag"><</span><span class="tag-name">se.emilsjolander.stickylistheaders.StickyListHeadersListView</span>
- <span class="attribute">android:id</span>=<span class="attribute-value">“@+id/lvTasks”</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:background</span>=<span class="attribute-value">“@drawable/todo_bg”</span>
- <span class="attribute">android:clipToPadding</span>=<span class="attribute-value">“false”</span>
- <span class="attribute">android:divider</span>=<span class="attribute-value">“#44FFFFFF”</span>
- <span class="attribute">android:dividerHeight</span>=<span class="attribute-value">“1dp”</span>
- <span class="attribute">android:drawSelectorOnTop</span>=<span class="attribute-value">“true”</span>
- <span class="attribute">android:fastScrollEnabled</span>=<span class="attribute-value">“true”</span>
- <span class="attribute">android:overScrollMode</span>=<span class="attribute-value">“never”</span>
- <span class="attribute">android:padding</span>=<span class="attribute-value">“16dp”</span>
- <span class="attribute">android:scrollbarStyle</span>=<span class="attribute-value">“outsideOverlay”</span> <span class="tag">/></span>
在MainActivity中使用:
<div>
<embed id="ZeroClipboardMovie_8" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_8">
</embed>
</div>
</div>
- lvTasks = (StickyListHeadersListView) findViewById(R.id.lvTasks);
- taskAdapter = <span class="keyword">new</span> StickListTaskAdapter(<span class="keyword">this</span>, tasks);
- lvTasks.setAdapter(taskAdapter);
- lvTasks.setDrawingListUnderStickyHeader(<span class="keyword">true</span>);
- lvTasks.setAreHeadersSticky(<span class="keyword">true</span>);
- lvTasks.setOnItemLongClickListener(onItemLongClickListener);
- lvTasks.setOnItemClickListener(onItemClickListener);
<div>
<embed id="ZeroClipboardMovie_9" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_9">
</embed>
</div>
</div>
- <span class="keyword">public</span> <span class="keyword">class</span> StickyListHeadersListView <span class="keyword">extends</span> FrameLayout {
-
- <span class="keyword">public</span> <span class="keyword">interface</span> OnHeaderClickListener {
- <span class="keyword">public</span> <span class="keyword">void</span> onHeaderClick(StickyListHeadersListView l, View header,
- <span class="keyword">int</span> itemPosition, <span class="keyword">long</span> headerId, <span class="keyword">boolean</span> currentlySticky);
- }
-
- <span class="comment">/**</span>
- <span class="comment"> * Notifies the listener when the sticky headers top offset has changed.</span>
- <span class="comment"> */</span>
- <span class="keyword">public</span> <span class="keyword">interface</span> OnStickyHeaderOffsetChangedListener {
- <span class="comment">/**</span>
- <span class="comment"> * @param l The view parent</span>
- <span class="comment"> * @param header The currently sticky header being offset.</span>
- <span class="comment"> * This header is not guaranteed to have it’s measurements set.</span>
- <span class="comment"> * It is however guaranteed that this view has been measured,</span>
- <span class="comment"> * therefor you should user getMeasured* methods instead of</span>
- <span class="comment"> * get* methods for determining the view’s size.</span>
- <span class="comment"> * @param offset The amount the sticky header is offset by towards to top of the screen.</span>
- <span class="comment"> */</span>
- <span class="keyword">public</span> <span class="keyword">void</span> onStickyHeaderOffsetChanged(StickyListHeadersListView l, View header, <span class="keyword">int</span> offset);
- }
-
- <span class="comment">/**</span>
- <span class="comment"> * Notifies the listener when the sticky header has been updated</span>
- <span class="comment"> */</span>
- <span class="keyword">public</span> <span class="keyword">interface</span> OnStickyHeaderChangedListener {
- <span class="comment">/**</span>
- <span class="comment"> * @param l The view parent</span>
- <span class="comment"> * @param header The new sticky header view.</span>
- <span class="comment"> * @param itemPosition The position of the item within the adapter’s data set of</span>
- <span class="comment"> * the item whose header is now sticky.</span>
- <span class="comment"> * @param headerId The id of the new sticky header.</span>
- <span class="comment"> */</span>
- <span class="keyword">public</span> <span class="keyword">void</span> onStickyHeaderChanged(StickyListHeadersListView l, View header,
- <span class="keyword">int</span> itemPosition, <span class="keyword">long</span> headerId);
-
- }
转自:http://blog.csdn.net/linmiansheng/article/details/20747775
💬 评论