很多情况下, 我们想要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
- <?xml version=“1.0” encoding=“utf-8”?>
- <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
- android:layout_width=“match_parent”
- android:layout_height=“match_parent”
- android:background=“@drawable/header_selector” >
- <TextView
- android:id=“@+id/tvHeader”
- android:layout_width=“wrap_content”
- android:layout_height=“match_parent”
- android:layout_gravity=“start|left”
- android:padding=“5dp”
- android:textColor=“@android:color/white”
- android:textSize=“17sp”
- android:textStyle=“bold” />
- </RelativeLayout>
因为我们在Header上面只是展现一个日期,所以我们只需要一个TextView即可。
1.2)task_item.xml
- <?xml version=“1.0” encoding=“utf-8”?>
- <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
- android:layout_width=“match_parent”
- android:layout_height=“32dp”
- android:descendantFocusability=“blocksDescendants”
- android:padding=“5dip”>
- <ImageView
- android:padding=“5dp”
- android:layout_centerVertical=“true”
- android:id=“@+id/ivComplete”
- android:layout_width=“wrap_content”
- android:layout_height=“match_parent”
- android:layout_alignParentLeft=“true”
- android:layout_alignParentTop=“true”
- android:contentDescription=“@string/imageview_contentdesc”
- android:src=“@drawable/handdraw_tick”
- android:visibility=“gone” />
- <TextView
- android:id=“@+id/tvTitle”
- android:layout_width=“wrap_content”
- android:layout_height=“match_parent”
- android:layout_toRightOf=“@+id/ivComplete”
- android:gravity=“left|center_vertical”
- android:padding=“5dp”
- android:textSize=“20sp” />
- </RelativeLayout>
在这里面,我们定义了每一个item要展现的布局,跟平常我们经常用的layout其实是一样的,大家接下来自定义的Adapter也就理解了。
2)第二步,跟平常绑定ListView一样,我们也需要自定义一个Adapter,称之为StickyListTaskAdapter。
我们来看一下 StickListTaskAdapter 完整的代码,如下:
- public class StickListTaskAdapter extends BaseAdapter
- implements SectionIndexer, StickyListHeadersAdapter{
- private LayoutInflater layoutInflater;
- private List<TodoTask> tasks;
- private int[] sectionIndices;
- private String[] sectionHeaders;
- public StickListTaskAdapter(Context context, List<TodoTask> tasks) {
- layoutInflater = LayoutInflater.from(context);
- this.tasks = tasks;
- sectionIndices = getSectionIndices();
- sectionHeaders = getSectionHeaders();
- }
- public void refresh(List<TodoTask> tasks){
- this.tasks = tasks;
- sectionIndices = getSectionIndices();
- sectionHeaders = getSectionHeaders();
- notifyDataSetChanged();
- }
- private int[] getSectionIndices() {
- List<Integer> sectionIndices = new ArrayList<Integer>();
- String lastCreateDate = Helper.getFormatDate(tasks.get(0).getCreateTime());
- sectionIndices.add(0);
- for (int i = 1; i < tasks.size(); i++) {
- String createDate = Helper.getFormatDate(tasks.get(i).getCreateTime());
- if (!createDate.equals(lastCreateDate)) {
- lastCreateDate = createDate;
- sectionIndices.add(i);
- }
- }
- int[] sections = new int[sectionIndices.size()];
- for (int i = 0; i < sectionIndices.size(); i++) {
- sections[i] = sectionIndices.get(i);
- }
- return sections;
- }
- private String[] getSectionHeaders() {
- String[] sectionHeaders = new String[sectionIndices.length];
- for (int i = 0; i < sectionIndices.length; i++) {
- sectionHeaders[i] = Helper.getFormatDate(tasks.get(sectionIndices[i]).getCreateTime());
- }
- return sectionHeaders;
- }
- @Override
- public int getCount() {
- return tasks.size();
- }
- @Override
- public Object getItem(int position) {
- return tasks.get(position);
- }
- @Override
- public long getItemId(int position) {
- return tasks.get(position).getId();
- }
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- ViewHolder viewHolder;
- if (convertView == null) {
- viewHolder = new ViewHolder();
- convertView = layoutInflater.inflate(R.layout.task_item, null);
- 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);
- } else {
- viewHolder = (ViewHolder) convertView.getTag();
- }
- if(“Y”.equals(tasks.get(position).getFlagCompleted())){
- viewHolder.ivComplete.setVisibility(View.VISIBLE);
- viewHolder.tvCreateTime.setText(Helper.getFormatDate(tasks.get(position).getCompleteTime()));
- }else{
- viewHolder.ivComplete.setVisibility(View.GONE);
- viewHolder.tvCreateTime.setText(Helper.getFormatDate(tasks.get(position).getCreateTime()));
- }
- viewHolder.tvTitle.setText(tasks.get(position).getTitle());
- return convertView;
- }
- @Override
- public View getHeaderView(int position, View convertView, ViewGroup parent) {
- HeaderViewHolder hvh;
- if(convertView == null){
- hvh = new HeaderViewHolder();
- convertView = layoutInflater.inflate(R.layout.task_header, null);
- hvh.tvHeader = (TextView) convertView.findViewById(R.id.tvHeader);
- convertView.setTag(hvh);
- }else{
- hvh = (HeaderViewHolder)convertView.getTag();
- }
- hvh.tvHeader.setText(Helper.getFormatDate(tasks.get(position).getCreateTime()));
- return convertView;
- }
- @Override
- public long getHeaderId(int position) {
- return Helper.changeStringDateToLong(Helper.getFormatDate(tasks.get(position).getCreateTime()));
- }
- @Override
- public Object[] getSections() {
- // TODO Auto-generated method stub
- return sectionHeaders;
- }
- @Override
- public int getPositionForSection(int sectionIndex) {
- if (sectionIndex >= sectionIndices.length) {
- sectionIndex = sectionIndices.length – 1;
- } else if (sectionIndex < 0) {
- sectionIndex = 0;
- }
- return sectionIndices[sectionIndex];
- }
- @Override
- public int getSectionForPosition(int position) {
- for (int i = 0; i < sectionIndices.length; i++) {
- if (position < sectionIndices[i]) {
- return i – 1;
- }
- }
- return sectionIndices.length – 1;
- }
- class ViewHolder {
- ImageView ivComplete;
- TextView tvTitle;
- TextView tvCreateTime;
- }
- class HeaderViewHolder{
- TextView tvHeader;
- }
- }
首先我们定义了下面两个数组,并且需要在构造的时候初始化它们:
- private int[] sectionIndices;
- 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的定义:
- public interface StickyListHeadersAdapter extends ListAdapter {
- View getHeaderView(int position, View convertView, ViewGroup parent);
- long getHeaderId(int position);
- }
这是开源库提供的接口,因为我们需要添加Header,所以我们必须在Adapter中也返回一个Header的View,这其实跟实现getView是一样的道理的,都挺好理解的。
所以在getHeaderView里面就会用到我们一开始新定义的那个task_header.xml了,同样的,为了实现优化,也会利用一个HeaderViewHolder。
另外一个接口就是SectionIndexer了,它有三个方法要实现,如下:
- public interface SectionIndexer {
- Object[] getSections();
- int getPositionForSection(int sectionIndex);
- int getSectionForPosition(int position);
- }
看代码的实现,可以发现:
getSections:返回的其实就是Header上面要展示的数据,在这里其实就是sectionHeaders了,存放的是create_time的数据。
getPositionForSection:返回的是这个section数据在List<TodoTask>这个基础数据源中的位置,因为section中的数据其实也是从List<TodoTask>中获取到的。
getSectionForPosition:则是通过在基础数据源List<TodoTask>中的位置找出对应的Section中的数据,原因同上。
那么上面这两个函数的作用在哪?
大家有没有发现,当同一个分组的数据在滚动的时候,最上面的分组并不会变化,只有当滑到其它分组的时候,这个分组才会被新的分组给替换掉。这个效果实现的原理就在这里了,虽然我没有看过源代码,但是我认为,在每一个item滚动的时候,都会找出其对应的分组,然后显示在最上方,如果都是属于同一个分组的话,那么最上面的显示的当然一直都是这个分组对应的Header了。
综上所述,为了实现Sticky和分组的效果,我们就要在原来继承BaseAdapter的基础上再实现多两个接口,并实现对应的逻辑。
那么如何在Activity中使用呢?请看下面的代码:
在xml中定义:
- <se.emilsjolander.stickylistheaders.StickyListHeadersListView
- android:id=“@+id/lvTasks”
- android:layout_width=“match_parent”
- android:layout_height=“match_parent”
- android:background=“@drawable/todo_bg”
- android:clipToPadding=“false”
- android:divider=“#44FFFFFF”
- android:dividerHeight=“1dp”
- android:drawSelectorOnTop=“true”
- android:fastScrollEnabled=“true”
- android:overScrollMode=“never”
- android:padding=“16dp”
- android:scrollbarStyle=“outsideOverlay” />
在MainActivity中使用:
- lvTasks = (StickyListHeadersListView) findViewById(R.id.lvTasks);
- taskAdapter = new StickListTaskAdapter(this, tasks);
- lvTasks.setAdapter(taskAdapter);
- lvTasks.setDrawingListUnderStickyHeader(true);
- lvTasks.setAreHeadersSticky(true);
- lvTasks.setOnItemLongClickListener(onItemLongClickListener);
- lvTasks.setOnItemClickListener(onItemClickListener);
而开源库中StickyListHeadersListView还提供了几个接口,可以让我们在Activity中去实现,不过这些就有待大家自己去慢慢学习了。
- public class StickyListHeadersListView extends FrameLayout {
- public interface OnHeaderClickListener {
- public void onHeaderClick(StickyListHeadersListView l, View header,
- int itemPosition, long headerId, boolean currentlySticky);
- }
- /**
- * Notifies the listener when the sticky headers top offset has changed.
- */
- public interface OnStickyHeaderOffsetChangedListener {
- /**
- * @param l The view parent
- * @param header The currently sticky header being offset.
- * This header is not guaranteed to have it’s measurements set.
- * It is however guaranteed that this view has been measured,
- * therefor you should user getMeasured* methods instead of
- * get* methods for determining the view’s size.
- * @param offset The amount the sticky header is offset by towards to top of the screen.
- */
- public void onStickyHeaderOffsetChanged(StickyListHeadersListView l, View header, int offset);
- }
- /**
- * Notifies the listener when the sticky header has been updated
- */
- public interface OnStickyHeaderChangedListener {
- /**
- * @param l The view parent
- * @param header The new sticky header view.
- * @param itemPosition The position of the item within the adapter’s data set of
- * the item whose header is now sticky.
- * @param headerId The id of the new sticky header.
- */
- public void onStickyHeaderChanged(StickyListHeadersListView l, View header,
- int itemPosition, long headerId);
- }
结束。
转自:http://blog.csdn.net/linmiansheng/article/details/20747775