0

Android学习小Demo(12)TodoList实现ListView的分组实现

 

很多情况下, 我们想要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

  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.     <TextView
  7.         android:id=“@+id/tvHeader”
  8.         android:layout_width=“wrap_content”
  9.         android:layout_height=“match_parent”
  10.         android:layout_gravity=“start|left”
  11.         android:padding=“5dp”
  12.         android:textColor=“@android:color/white”
  13.         android:textSize=“17sp”
  14.         android:textStyle=“bold” />
  15. </RelativeLayout>

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

1.2)task_item.xml

  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.     <ImageView
  8.         android:padding=“5dp”
  9.         android:layout_centerVertical=“true”
  10.         android:id=“@+id/ivComplete”
  11.         android:layout_width=“wrap_content”
  12.         android:layout_height=“match_parent”
  13.         android:layout_alignParentLeft=“true”
  14.         android:layout_alignParentTop=“true”
  15.         android:contentDescription=“@string/imageview_contentdesc”
  16.         android:src=“@drawable/handdraw_tick”
  17.         android:visibility=“gone” />
  18.     <TextView
  19.         android:id=“@+id/tvTitle”
  20.         android:layout_width=“wrap_content”
  21.         android:layout_height=“match_parent”
  22.         android:layout_toRightOf=“@+id/ivComplete”
  23.         android:gravity=“left|center_vertical”
  24.         android:padding=“5dp”
  25.         android:textSize=“20sp” />
  26. </RelativeLayout>

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

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

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

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


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

  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的定义:

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


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

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

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

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

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

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

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

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

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

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

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

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

在xml中定义:

  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” />

在MainActivity中使用:

  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中去实现,不过这些就有待大家自己去慢慢学习了。

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


结束。

转自:http://blog.csdn.net/linmiansheng/article/details/20747775

天边的星星