StickyListHeaders这个控件目前被很多app广泛应用,翻译过来的意思是“ 粘列表标题”。
是什么意思就不细说了,看下面的示例图就能明白。
看起来感觉非常的神奇,那究竟是怎么做到的呢?
开始之前,我们先约定好几个概念:
上图中,列表中的所有数据称为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个,包结构是很扁平的。
类之间的关系如下所示:
开发者在使用时主要面向StickyListHeadersListView、ExpandableStickyListHeadersListView和StickyListHeadersAdapter这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);
}
}