意想不到的技巧 StickyListHeaders

StickyListHeaders这个控件目前被很多app广泛应用,翻译过来的意思是“ 粘列表标题”。
是什么意思就不细说了,看下面的示例图就能明白。

意想不到的技巧 StickyListHeaders_第1张图片

看起来感觉非常的神奇,那究竟是怎么做到的呢?
开始之前,我们先约定好几个概念:
上图中,列表中的所有数据称为dataSet(数据集),列表中的每条数据,称为item(列表项,它的id称为itemID),
item被根据首字母分成了若干不同的section(分区, 它的id称为sectionID),
每个section显示大写红色字母的地方叫做head(头部,它的id称为headID),每个item的位置称为position

下面我们来分析一下源码,这个项目的Github地址是:https://github.com/emilsjolander/StickyListHeaders
打开StickyListHeaders的 LIB(v2.0),核心的类总共有11个,包结构是很扁平的。

意想不到的技巧 StickyListHeaders_第2张图片

类之间的关系如下所示:

意想不到的技巧 StickyListHeaders_第3张图片

开发者在使用时主要面向StickyListHeadersListViewExpandableStickyListHeadersListViewStickyListHeadersAdapter这3个类,
他们就好像原生控件当中的 ListView 和 BaseAdapter,用法也是类似的。
请看下面的Demo:
在这段 Activity 的 onCreate方法 中,首先获取到 ExpandableStickyListHeadersListView对象,然后为其设置Adapter对象,之后设置头部的点击事件,这个和我们平时对ListView的操作可以说是没有什么区别。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.expandable_sample);
    mListView = (ExpandableStickyListHeadersListView) findViewById(R.id.list);
    mTestBaseAdapter = new TestBaseAdapter(this);
    mListView.setAdapter(mTestBaseAdapter);
    mListView.setOnHeaderClickListener(new StickyListHeadersListView.OnHeaderClickListener() {
        @Override
        public void onHeaderClick(StickyListHeadersListView l, View header, int itemPosition, long headerId, boolean currentlySticky) {
            if(mListView.isHeaderCollapsed(headerId)){
                mListView.expand(headerId);
            }else {
                mListView.collapse(headerId);
            }
        }
    });
}

然后 我们再看一下 TestBaseAdapter 的实现,它继承于原生的 BaseAdapter 并实现了 StickyListHeadersAdapter 和 SectionIndexe 两个接口(其中 SectionIndexe 是原生中android.widget的接口)。

public class TestBaseAdapter extends BaseAdapter implements
        StickyListHeadersAdapter, SectionIndexer {
 
    @Override
    public int getCount() {
        return mCountries.length;
    }
 
    @Override
    public Object getItem(int position) {
        return mCountries[position];
    }
 
    @Override
    public long getItemId(int position) {
        return position;
    }
 
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // TODO 产生 列表行的View
        // ... ... 
        return convertView;
    }
 
    @Override
    public View getHeaderView(int position, View convertView, ViewGroup parent) {
        // TODO 产生 分区头部的View
        // ... ... 
        return convertView;
    }
 
    @Override
    public long getHeaderId(int position) {
        // 根据 位置 获取 对应的 分区头部ID
        return headID;
    }
 
    @Override
    public int getPositionForSection(int section) {
        // 通过 位置 返回 对应的分区ID
        // ... ...
        return positionForSection;
    }
 
    @Override
    public int getSectionForPosition(int position) {
        // 通过 分区ID 返回 对应的 位置
        // ... ...
        return sectionForPosition;
    }
 
    @Override
    public Object[] getSections() {
        // 获取 分区的对象
        return mSectionLetters;
    }
 
}

OK,到目前为止,我们可以推断出,StickyListHeadersListView (或者是ExpandableStickyListHeadersListView)的用法和 ListView 是一样的。
即:首先获取到 StickyListHeadersListView 对象,然后编写 Adapter 对象与之关联,当 StickyListHeadersListView 被添加到屏幕上准备开始渲染时,底层会调用 onMeasure方法 和 onLayout方法,并执行 layoutChild方法,
这个时候 StickyListHeadersListView 会根据 Adapter 中的 getView方法 返回的 View对象,将视图组合起来,并最后渲染到屏幕上。
Fine,在上面的论证基础上,我们把跟踪代码的入口,设定在StickyListHeadersListView 的setAdapter方法,这里我们从demo中ExpandableStickyListHeadersListView的 setAdapter方法 进入:

@Override
public void setAdapter(StickyListHeadersAdapter adapter) {
    mExpandableStickyListHeadersAdapter = new ExpandableStickyListHeadersAdapter(adapter);
    super.setAdapter(mExpandableStickyListHeadersAdapter);
}

这里用 ExpandableStickyListHeadersListView 重新构造了一个 ExpandableStickyListHeadersAdapter 对象,并传递到父类(StickyListHeadersListView )的setAdapter中。
我们来一下 ExpandableStickyListHeadersAdapter 这个类的实现 :

class ExpandableStickyListHeadersAdapter extends BaseAdapter implements StickyListHeadersAdapter {
 
    private final StickyListHeadersAdapter mInnerAdapter;
    DualHashMap mViewToItemIdMap = new DualHashMap();
    DistinctMultiHashMap mHeaderIdToViewMap = new DistinctMultiHashMap();
    List mCollapseHeaderIds = new ArrayList();
 
    ExpandableStickyListHeadersAdapter(StickyListHeadersAdapter innerAdapter) {
        this.mInnerAdapter = innerAdapter;
    }
 
    // ... ... 
}

可以发现,ExpandableStickyListHeadersAdapter 也是继承了原生的 BaseAdapter 并实现了 StickyListHeadersAdapter,但是多了 DualHashMap 、DistinctMultiHashMap 这两个Map对象和一个存储 Long型数据的List对象。
其中,DualHashMap 是实现了 “Key和Value双向关联关系” 的数据结构,也就是说,可以通过 Key 取到 Value,也可以通过 Value 取到 Key。
而 DistinctMultiHashMap 是实现了 “一个 Key对应N个Value的关联关系” 的数据结构,可以通过一个Key取到所有(多个)和这个Key有关的Value对象集合。
根据 变量名称 我们可以判断,DualHashMap 和 DistinctMultiHashMap 是用来实现 数据分区ID(headID) 和 分区内View 的双向关联管理。
由代码中我们看到,ExpandableStickyListHeadersAdapter 的访问权限并不是public,而是default,所以我们是无法从外部直接实例化这种类型的对象进行使用的。
这样的设计应该是为了简化开发者实现 Adapter 的代码量,把用来处理 数据分区ID(headID) 和 分区内View 的双向关联管理的逻辑封装了起来。
接着,我们进入到 StickyListHeadersListView 的 setAdapter方法 中:

public void setAdapter(StickyListHeadersAdapter adapter) {
    if (adapter == null) {
        if (mAdapter instanceof SectionIndexerAdapterWrapper) {
            ((SectionIndexerAdapterWrapper) mAdapter).mSectionIndexerDelegate = null;
        }
        if (mAdapter != null) {
            mAdapter.mDelegate = null;
        }
        mList.setAdapter(null);
        clearHeader();
        return;
    }
    if (mAdapter != null) {
        mAdapter.unregisterDataSetObserver(mDataSetObserver);
    }
 
    if (adapter instanceof SectionIndexer) {
        mAdapter = new SectionIndexerAdapterWrapper(getContext(), adapter);
    } else {
        mAdapter = new AdapterWrapper(getContext(), adapter);
    }
    mDataSetObserver = new AdapterWrapperDataSetObserver();
    mAdapter.registerDataSetObserver(mDataSetObserver);
 
    if (mOnHeaderClickListener != null) {
        mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler());
    } else {
        mAdapter.setOnHeaderClickListener(null);
    }
 
    mAdapter.setDivider(mDivider, mDividerHeight);
 
    mList.setAdapter(mAdapter);
    clearHeader();
}

这里是我们要重点讲解的地方之一,在这个方法中,我们可以看到三个重要的对象:mList、mAdapter 和 mDataSetObserver,
首先看这三个变量的定义:mList 是一个 WrapperViewList 类型的成员变量,他是一个原生的 ListView 的子类。
mAdapter是一个 AdapterWrapper 类型的成员变量,他是一个原生 BaseAdapter 的子类; mDataSetObserver 则是 AdapterWrapperDataSetObserver 类型的成员变量,是 DataSetObserver 的子类,它负责在数据集发生改变时通知外部的监听(观察者模式),下面是AdapterWrapperDataSetObserver 的定义:

private class AdapterWrapperDataSetObserver extends DataSetObserver {
 
    @Override
    public void onChanged() {
        clearHeader();
    }
 
    @Override
    public void onInvalidated() {
        clearHeader();
    }
 
}

通过上面对 StickyListHeadersListView 的 setAdapter方法 的阅读,我们发现此处的逻辑,是根据用户传入的 StickyListHeadersAdapter对象,构造出 AdapterWrapper(或者SectionIndexerAdapterWrapper)对象,并 set 到其内部成员变量 mList 中。由于 StickyListHeadersListView 并不是 ListView 的子类,而是继承于FrameLayout,所以 StickyListHeadersListView 中的列表控件,实际上就是 mList 这个成员变量。
根据ListView的布局原理,StickyListHeadersListView 要进行渲染时,会调用 AdapterWrapper 的getView方法,构建出内部的子视图,完成控件的布局。
所以我们下一步便要看一看 AdapterWrapper 对象的 getView方法:

@Override
public WrapperView getView(int position, View convertView, ViewGroup parent) {
   WrapperView wv = (convertView == null) ? new WrapperView(mContext) : (WrapperView) convertView;
   View item = mDelegate.getView(position, wv.mItem, parent);
   View header = null;
   if (previousPositionHasSameHeader(position)) {
      recycleHeaderIfExists(wv);
   } else {
      header = configureHeader(wv, position);
   }
   if((item instanceof Checkable) && !(wv instanceof CheckableWrapperView)) {
      // Need to create Checkable subclass of WrapperView for ListView to work correctly
      wv = new CheckableWrapperView(mContext);
   } else if(!(item instanceof Checkable) && (wv instanceof CheckableWrapperView)) {
      wv = new WrapperView(mContext);
   }
   wv.update(item, header, mDivider, mDividerHeight);
   return wv;
}

非常的清晰明了,这边负责创建每个 item 的视图。首先根据 mDelegate 的 getView 方法,获取每个position所要创建的 view 的类型,mDelegate 实际上就是我们创建的 StickyListHeadersAdapter 对象实例。同时 数据集分区头部的视图 也是在此创建,就是 header 成员变量,通过 previousPositionHasSameHeader方法,判断上一个位置是否有头部视图,来决定是否复用旧的头部视图 或者是 重新创建一个。最后根据 view 的类型 就创建出对应的 WrapperView 对象,WrapperView 就是最后渲染到屏幕上 每条item的实际视图对象。
此处的 upadte方法 负责对 WrapperView 对象进行设置,达到最终的绘制效果。upadte方法 的入参为item, header, mDivider, mDividerHeight四个参数,
接着我们来看一下 upadte方法 都做了什么:

void update(View item, View header, Drawable divider, int dividerHeight) {
    
   //every wrapperview must have a list item
   if (item == null) {
      throw new NullPointerException("List view item must not be null.");
   }
 
   //only remove the current item if it is not the same as the new item. this can happen if wrapping a recycled view
   if (this.mItem != item) {
      removeView(this.mItem);
      this.mItem = item;
      final ViewParent parent = item.getParent();
      if(parent != null && parent != this) {
         if(parent instanceof ViewGroup) {
            ((ViewGroup) parent).removeView(item);
         }
      }
      addView(item);
   }
 
   //same logik as above but for the header
   if (this.mHeader != header) {
      if (this.mHeader != null) {
         removeView(this.mHeader);
      }
      this.mHeader = header;
      if (header != null) {
         addView(header);
      }
   }
 
   if (this.mDivider != divider) {
      this.mDivider = divider;
      this.mDividerHeight = dividerHeight;
      invalidate();
   }
}

由于WrapperView是ViewGroup的子类,所以必须要重写onLayout方法,我们再看一下WrapperView的onLayout方法:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
 
   l = 0;
   t = 0;
   r = getWidth();
   b = getHeight();
 
   if (mHeader != null) {
      int headerHeight = mHeader.getMeasuredHeight();
      mHeader.layout(l, t, r, headerHeight);
      mItemTop = headerHeight;
      mItem.layout(l, headerHeight, r, b);
   } else if (mDivider != null) {
      mDivider.setBounds(l, t, r, mDividerHeight);
      mItemTop = mDividerHeight;
      mItem.layout(l, mDividerHeight, r, b);
   } else {
      mItemTop = t;
      mItem.layout(l, t, r, b);
   }
}

可以看到,这里对 mHeader、mDivider 和 mItem 这三个对象进行 layout操作,而这三个对象就是update方法入参中的 head、divider 和 item。
通过分析,我们发现 StickyListHeadersListView 中的每个item视图 “是否有头部、是否有分割线、高度是多少”,实际上都是根据 update方法 传入的参数,在 WrapperView 中进行设置的。

StickyListHeadersListView 的实现原理,实际上是通过对 ListView 和 BaseAdapter 进行了二次封装实现。
通过用户构造的Adapter对象,在内部重新构造一个封装过的新Adapter对象,再绑定到内部的ListView中。
所以 ListView 的所有特性在 StickyListHeadersListView 都得到的保存,包括重要的RecycleBin视图复用结构。实际上用户还可以通过StickyListHeadersListView的getWrappedList() 方法直接获取到内部ListView进行操作,扩展出自己的API。
通过本文开始处的效果图,我们看到在滚到列表的时候,每个分区的头部视图(headView)可以不断被顶掉替换,这样的操作是如何实现的呢?实际上这也是在 StickyListHeadersListView 内完成的,StickyListHeadersListView定义了一个内部类 WrapperListScrollListener,它实现了 AbsListView 的 OnScrollListener 接口,它的作用是监听成员变量mList的滚到情况。当用户滚到列表的时候,WrapperListScrollListener中的onScroll方法便会被调用。其中的 updateOrClearHeader方法 负责设置顶部 headView 的显示和偏移,具体的实现是在
ensureHeaderHasCorrectLayoutParams方法 中更改头部视图的LayoutParams。

private class WrapperListScrollListener implements OnScrollListener {
 
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
                         int visibleItemCount, int totalItemCount) {
        if (mOnScrollListenerDelegate != null) {
            mOnScrollListenerDelegate.onScroll(view, firstVisibleItem,
                    visibleItemCount, totalItemCount);
        }
        updateOrClearHeader(mList.getFixedFirstVisibleItem());
    }
 
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (mOnScrollListenerDelegate != null) {
            mOnScrollListenerDelegate.onScrollStateChanged(view,
                    scrollState);
        }
    }
 
}
private void ensureHeaderHasCorrectLayoutParams(View header) {
        ViewGroup.LayoutParams lp = header.getLayoutParams();
        if (lp == null) {
            lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
            header.setLayoutParams(lp);
        } else if (lp.height == LayoutParams.MATCH_PARENT || lp.width == LayoutParams.WRAP_CONTENT) {
            lp.height = LayoutParams.WRAP_CONTENT;
            lp.width = LayoutParams.MATCH_PARENT;
            header.setLayoutParams(lp);
        }
    }

你可能感兴趣的:(意想不到的技巧 StickyListHeaders)