之前看到一个开源组件PinnedHeaderListView(github 开源地址: https://github.com/JimiSmith/PinnedHeaderListView),它将所有的View分组,每一组都有一个Header,上下滑动到某一个组的时候,它的Header都会悬浮在顶部,有时候我们需要实现这样的效果。本能的想要看下它是怎么实现的,在网上搜了很久也没有找到一个详细的分析,干脆就不找了,自己把源代码下载下来分析一下。今天我们就来讲一下这个组件。
首先来看一下效果图
为了更加直观的理解它的实现代码,先来看下使用方法,由于这个组件是直接继承ListView实现的,用法和直接使用ListView类似,具体代码如下:
public class PinnedHeaderListViewActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_pinned_header_listview_main); PinnedHeaderListView listView = (PinnedHeaderListView) findViewById(R.id.pinnedListView); LayoutInflater inflator = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); LinearLayout header1 = (LinearLayout) inflator.inflate(R.layout.pinned_header_listview_list_item, null); ((TextView) header1.findViewById(R.id.textItem)).setText("HEADER 1"); LinearLayout header2 = (LinearLayout) inflator.inflate(R.layout.pinned_header_listview_list_item, null); ((TextView) header2.findViewById(R.id.textItem)).setText("HEADER 2"); LinearLayout footer = (LinearLayout) inflator.inflate(R.layout.pinned_header_listview_list_item, null); ((TextView) footer.findViewById(R.id.textItem)).setText("FOOTER"); listView.addHeaderView(header1); listView.addHeaderView(header2); listView.addFooterView(footer); TestSectionedAdapter sectionedAdapter = new TestSectionedAdapter(); listView.setAdapter(sectionedAdapter); } }布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <za.co.immedia.pinnedheaderlistview.PinnedHeaderListView android:id="@+id/pinnedListView" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> </za.co.immedia.pinnedheaderlistview.PinnedHeaderListView> </LinearLayout>
我们看到它的使用方法很简单,就是我们熟悉的ListView加Adapter的方式,只是为了演示效果,给ListView加了两个Header和一个Footer。布局文件中组件的使用也和普通的ListView没有什么两样。用法看起来很简单,下面就来分析一下这个组件的实现。
为了更容易理解,需要首先来说说明一下PinnedHeaderListView的大概思路。它是通过将ListView所有的子item分成不同的section,每个section的item的数目不一样,每一个section的第一个item称为header。我们姑且将这两种不同的类型称之为section header和section item。不过值得注意的是,只是从逻辑上做了这样的划分,实际上所有的item,无论是否是header都是ListView里普通的一项。
基于上面的说明,首先看一下SectionedBaseAdapter,它就是一个BaseAdapter,
private static int HEADER_VIEW_TYPE = 0; private static int ITEM_VIEW_TYPE = 0; /** * Holds the calculated values of @{link getPositionInSectionForPosition} */ private SparseArray<Integer> mSectionPositionCache; /** * Holds the calculated values of @{link getSectionForPosition} */ private SparseArray<Integer> mSectionCache; /** * Holds the calculated values of @{link getCountForSection} */ private SparseArray<Integer> mSectionCountCache; /** * Caches the item count */ private int mCount; /** * Caches the section count */ private int mSectionCount; public SectionedBaseAdapter() { super(); mSectionCache = new SparseArray<Integer>(); mSectionPositionCache = new SparseArray<Integer>(); mSectionCountCache = new SparseArray<Integer>(); mCount = -1; mSectionCount = -1; }
1-2行,定义了两种类型HEADER_VIEW_TYPE和ITEM_VIEW_TYPE,分别对应section header和section item。
7、11、15行定义了三个SparseArray,它是Android上对HashMap<Integer, Object>的性能更优的替代品。我们可以将他们当做HashMap<Integer, Integer>来理解。他们的作用是用来对section的信息做记录(缓存)。具体来讲,mSectionPositionCache表示第i个位置的item在对应的section中是第几个位置,mSectionCache表示第i个位置的item是属于第几个section,mSectionCountCache表示每个section有几个item。
20、24行,表示总的item数(包括每个section的header)和section数。
@Override public final int getCount() { if (mCount >= 0) { return mCount; } int count = 0; for (int i = 0; i < internalGetSectionCount(); i++) { count += internalGetCountForSection(i); count++; // for the header view } mCount = count; return count; } @Override public final Object getItem(int position) { return getItem(getSectionForPosition(position), getPositionInSectionForPosition(position)); } @Override public final long getItemId(int position) { return getItemId(getSectionForPosition(position), getPositionInSectionForPosition(position)); } @Override public final View getView(int position, View convertView, ViewGroup parent) { if (isSectionHeader(position)) { return getSectionHeaderView(getSectionForPosition(position), convertView, parent); } return getItemView(getSectionForPosition(position), getPositionInSectionForPosition(position), convertView, parent); } @Override public final int getItemViewType(int position) { if (isSectionHeader(position)) { return getItemViewTypeCount() + getSectionHeaderViewType(getSectionForPosition(position)); } return getItemViewType(getSectionForPosition(position), getPositionInSectionForPosition(position)); } @Override public final int getViewTypeCount() { return getItemViewTypeCount() + getSectionHeaderViewTypeCount(); }这段代码就是重写了BaseAdapter的几个方法,大家对此应该很熟悉,所不同的是在方法的内部实现,对section的item和header做了区分处理。
public abstract Object getItem(int section, int position); public abstract long getItemId(int section, int position); public abstract int getSectionCount(); public abstract int getCountForSection(int section); public abstract View getItemView(int section, int position, View convertView, ViewGroup parent); public abstract View getSectionHeaderView(int section, View convertView, ViewGroup parent);这几个方法是需要子类去实现的。
public final int getSectionForPosition(int position) { // first try to retrieve values from cache Integer cachedSection = mSectionCache.get(position); if (cachedSection != null) { return cachedSection; } int sectionStart = 0; for (int i = 0; i < internalGetSectionCount(); i++) { int sectionCount = internalGetCountForSection(i); int sectionEnd = sectionStart + sectionCount + 1; if (position >= sectionStart && position < sectionEnd) { mSectionCache.put(position, i); return i; } sectionStart = sectionEnd; } return 0; } private int internalGetCountForSection(int section) { Integer cachedSectionCount = mSectionCountCache.get(section); if (cachedSectionCount != null) { return cachedSectionCount; } int sectionCount = getCountForSection(section); mSectionCountCache.put(section, sectionCount); return sectionCount; } private int internalGetSectionCount() { if (mSectionCount >= 0) { return mSectionCount; } mSectionCount = getSectionCount(); return mSectionCount; }
这三个方法与开头的三个SparseArray对应,方法中先分别从这三个Cache中获取对应的值,如果获取不到,就根据条件进行计算,将计算后的结果放入Cache中。
getPositionInSectionForPosition(int position)用于获取指定位置的item在它所在的section是第几个位置。
internalGetCountForSection(int section)用于获取指定section中item的数目。
internalGetSectionCount()用户获得section总的数目。
之前提到有几个抽象方法需要实现,下面就看一下SectionedBaseAdapter的实现类TestSectionedAdapter。
public class TestSectionedAdapter extends SectionedBaseAdapter { @Override public Object getItem(int section, int position) { // TODO Auto-generated method stub return null; } @Override public long getItemId(int section, int position) { // TODO Auto-generated method stub return 0; } @Override public int getSectionCount() { return 7; } @Override public int getCountForSection(int section) { return 15; } @Override public View getItemView(int section, int position, View convertView, ViewGroup parent) { LinearLayout layout = null; if (convertView == null) { LayoutInflater inflator = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); layout = (LinearLayout) inflator.inflate(R.layout.pinned_header_listview_list_item, null); } else { layout = (LinearLayout) convertView; } ((TextView) layout.findViewById(R.id.textItem)).setText("Section " + section + " Item " + position); return layout; } @Override public View getSectionHeaderView(int section, View convertView, ViewGroup parent) { LinearLayout layout = null; if (convertView == null) { LayoutInflater inflator = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); layout = (LinearLayout) inflator.inflate(R.layout.pinned_header_listview_header_item, null); } else { layout = (LinearLayout) convertView; } ((TextView) layout.findViewById(R.id.textItem)).setText("Header for section " + section); return layout; } }
由于在这个例子中,getItem和getItemId两个方法没有实际的作用,所以直接返回0和null了。
15-23行可以看出,要实现的这个ListView有7个section,每个section有15个item。
25-49行可以获得section header和section item的View。
Adapter的代码我们就分析完了,大致就是将ListView分成section,然后其他的方法都是围绕着section的管理来做的。
下面来看一下PinnedHeaderListView这个类,它有一个内部接口,这个接口就是在上面提到的Adapter中实现的,在这里都会用到,相信通过上面的讲解,大家可以看出来每个接口的大概意思。
public static interface PinnedSectionedHeaderAdapter { public boolean isSectionHeader(int position); public int getSectionForPosition(int position); public View getSectionHeaderView(int section, View convertView, ViewGroup parent); public int getSectionHeaderViewType(int section); public int getCount(); }这个类里的变量定义和构造函数等内容我们不在这里啰嗦了,直接看最重要的一部分代码,这也是实现这个功能的关键。
@Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (mOnScrollListener != null) { mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin || (firstVisibleItem < getHeaderViewsCount())) { mCurrentHeader = null; mHeaderOffset = 0.0f; for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) { View header = getChildAt(i); if (header != null) { header.setVisibility(VISIBLE); } } return; } firstVisibleItem -= getHeaderViewsCount(); int section = mAdapter.getSectionForPosition(firstVisibleItem); int viewType = mAdapter.getSectionHeaderViewType(section); mCurrentHeader = getSectionHeaderView(section, mCurrentHeaderViewType != viewType ? null : mCurrentHeader); ensurePinnedHeaderLayout(mCurrentHeader); mCurrentHeaderViewType = viewType; mHeaderOffset = 0.0f; for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) { if (mAdapter.isSectionHeader(i)) { View header = getChildAt(i - firstVisibleItem); float headerTop = header.getTop(); float pinnedHeaderHeight = mCurrentHeader.getMeasuredHeight(); header.setVisibility(VISIBLE); if (pinnedHeaderHeight >= headerTop && headerTop > 0) { mHeaderOffset = headerTop - header.getHeight(); } else if (headerTop <= 0) { header.setVisibility(INVISIBLE); } } } invalidate(); }
ListView滚动的时候,会不断的回调这个方法,然后在这个方法里实现悬浮header显示逻辑的控制。
7-17行先对ListView添加的Header的情况进行处理,这里的Header不是我们说的section header,而是我们通过ListView的addHeaderView()添加的,文章开始的使用方法介绍中就是添加了两个Header。这种情况下,刚开始是不会有悬浮效果的,因为还没有进入section。
23行得到了mCurrentHeader,就是要悬浮显示的View。
24行代码保证mCurrentHeader可以悬浮在ListView顶部的固定位置。
29-41行代码就是用来控制header移动的。因为当下方section的header快要到达顶端时,会将之前悬浮的header顶出显示区域,然后直到之前header消失,新的header就会悬浮在ListView顶端。这里的关键就是通过View的位置来计算之前悬浮header的偏移量mHeaderOffset,然后通过invalidate触发dispatchDraw方法以重绘View。
@Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (mAdapter == null || !mShouldPin || mCurrentHeader == null) return; int saveCount = canvas.save(); canvas.translate(0, mHeaderOffset); canvas.clipRect(0, 0, getWidth(), mCurrentHeader.getMeasuredHeight()); // needed // for // < // HONEYCOMB mCurrentHeader.draw(canvas); canvas.restoreToCount(saveCount); }
从dispatchDraw的实现中我们可以看到,确实是用到了偏移量mHeaderOffset。其中,先将canvas在Y轴方向上移动了mHeaderOffset的距离,然后截取画布,在截取后的画布上绘制header。
通过上面一系列的处理,最终实现了我们在开头看到的ListView的悬浮效果。总结一下PinnedHeaderListView的基本思路:将ListView逻辑上分成若干个section,每个section有一个header,当header滑动到顶端时,会在ListView上绘制一个悬浮的View,View的内容就是这个header,当下面的header2达到顶部与header相交时,根据滑动距离将header向上移,直到header消失,header2会悬浮在顶端,这样就实现了我们看到的效果。