您的位置:首页 > 其它

PinnedHeaderListView开源组件源码分析

2015-11-19 00:49 363 查看
之前看到一个开源组件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会悬浮在顶端,这样就实现了我们看到的效果。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: