github地址:https://github.com/JimiSmith/PinnedHeaderListView
关于实现类似联系人列表,组的头部总是悬浮在listview最顶部的效果,github上面有两个比较好的实现,分别是pinnedSectionListview和pinnedHeaderListView,之所以选择后者进行源码解析,是因为后者的源码比较简单,便于我们理解实现的精髓所在。
如果你想直接实现Android仿联系人列表分组悬浮列表,
自定义PinnedHeaderListView,看这里 http://blog.csdn.net/u010335298/article/details/51150346
代码结构
翻开源码,我们一共可以找到四个有用的类,分别是:
1. PinnedHeaderListView: 实现组的头部总是悬浮在顶部的listview
2. SectionedBaseAdapter: 封装的adapter的抽象类
3. PinnedHeaderListViewMainActivity: 具体使用的activity
4. TestSectionedAdapter: 实现了抽象类SectionedBaseAdapter的adapter
SectionedBaseAdapter
首先,我们来看抽象类SectionedBaseAdapter的实现
接口PinnedHeaderListView.PinnedSectionedHeaderAdapter
public abstract class SectionedBaseAdapter extends BaseAdapter implements PinnedHeaderListView.PinnedSectionedHeaderAdapter
可以看到,SectionedBaseAdapter继承BaseAdapter,
同时实现了PinnedHeaderListView.PinnedSectionedHeaderAdapter这个接口
我们来看PinnedHeaderListView.PinnedSectionedHeaderAdapter的定义:
public static interface PinnedSectionedHeaderAdapter {
public boolean isSectionHeader(int position); //是否是组的头部
public int getSectionForPosition(int position); //根据位置判断对应的组号
public View getSectionHeaderView(int section, View convertView, ViewGroup parent); // 得到组的头部view
public int getSectionHeaderViewType(int section); //
public int getCount();
}
看一下SectionedBaseAdapter的实现:
/*********************************************************************************************************
*
*
* 以下 , 实现了PinnedSectionedHeaderAdapter接口
*
*
* *********************************************************************************************************/
/**
* 是否是组的头部
* @param position
* @return
*/
public final boolean isSectionHeader(int position) {
int sectionStart = 0;
for (int i = 0; i < internalGetSectionCount(); i++) {
if (position == sectionStart) {
return true;
} else if (position < sectionStart) {
return false;
}
sectionStart += internalGetCountForSection(i) + 1;
}
return false;
}
/**
* 根据位置得到对应的组号
* @param position
* @return
*/
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;
}
/**
*
* @param section
* @param convertView
* @param parent
* @return
*/
public abstract View getSectionHeaderView(int section, View convertView, ViewGroup parent);
/**
*
* @param section
* @return
*/
public int getSectionHeaderViewType(int section) {
return HEADER_VIEW_TYPE;
}
@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;
}
/*********************************************************************************************************
* 以上 , 实现了PinnedSectionedHeaderAdapter接口
*********************************************************************************************************/
可以看到,具体的getSectionHeaderView是要在我们自己的adapter中实现的。
继承的BaseAdapter部分的实现
getView方法
/**
* 根据position是不是sectionHeader,来判断是调用返回getSectionHeaderView,还是调用返回getItemView
* @param position
* @param convertView
* @param parent
* @return
*/
@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);
}
可以看到,getView跟据是否是组的头部,分别调用了getSectionHeaderView和getItemView,
getSectionHeaderView和getItemView都是抽象方法,都需要我们在自己定义的adapter中去实现。
getCount()
@Override
public final int getCount() {
if (mCount >= 0) {
return mCount;
}
int count = 0;
for (int i = 0; i < internalGetSectionCount(); i++) {
count += internalGetCountForSection(i);//添加组?元素的个数
count++; // 添加组头部
}
mCount = count;
return count;
}
可以看出,count包括了所有的组内元素的个数和所有的组头部个数
getPositionInSectionForPosition
/*********************************************************************************************************
* 以上 , 实现了PinnedSectionedHeaderAdapter接口
*********************************************************************************************************/
/**
* 得到在组中的位置
* @param position
* @return
*/
public int getPositionInSectionForPosition(int position) {
// first try to retrieve values from cache
Integer cachedPosition = mSectionPositionCache.get(position);
if (cachedPosition != null) {
return cachedPosition;
}
int sectionStart = 0;
for (int i = 0; i < internalGetSectionCount(); i++) {
int sectionCount = internalGetCountForSection(i);
int sectionEnd = sectionStart + sectionCount + 1;
if (position >= sectionStart && position < sectionEnd) {
int positionInSection = position - sectionStart - 1;
mSectionPositionCache.put(position, positionInSection);
return positionInSection;
}
sectionStart = sectionEnd;
}
return 0;
}
把从cache中得到的忽略,从for循环开始看
循环每个组内,可以看到,if (position >= sectionStart && position < sectionEnd),即position在组内的话,得到在组中的位置,返回在组中的位置
getSectionForPosition
/**
* 根据位置得到对应的组号
* @param position
* @return
*/
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;
}
从for循环开始看,if (position >= sectionStart && position < sectionEnd),即position在组内,返回组号。
isSectionHeader
/**
* 是否是组的头部
* @param position
* @return
*/
public final boolean isSectionHeader(int position) {
int sectionStart = 0;
for (int i = 0; i < internalGetSectionCount(); i++) {
if (position == sectionStart) {
return true;
} else if (position < sectionStart) {
return false;
}
sectionStart += internalGetCountForSection(i) + 1;
}
return false;
}
也是遍历所有的组,如果position == sectionStart,也就是是组的头部,返回true.
PinnedHeaderListView的源码
public class PinnedHeaderListView extends ListView implements OnScrollListener , AdapterView.OnItemClickListener{
PinnedHeaderListView继承自ListView,实现了OnScrollListener和OnItemClickListener,
在构造函数中setOnScrollListener(this)和setOnItemClickListener(this);
public PinnedHeaderListView(Context context) {
super(context);
super.setOnScrollListener(this);
super.setOnItemClickListener(this);
}
我们来看PinnedHeaderListView的代码结构:
红框标出的都是比较重要的方法,我们会进行一一讲解
首选,接口PinnedSectionHeaderAdapter我们已经讲过了
我们从onScroll方法开始看
OnScroll方法
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mOnScrollListener != null) {
mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
headerCount = getHeaderViewsCount();
if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin || (firstVisibleItem < headerCount)) {
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();//去掉header view的影响
int section = mAdapter.getSectionForPosition(firstVisibleItem); //得到组号
int viewType = mAdapter.getSectionHeaderViewType(section);
mCurrentHeader = getSectionHeaderView(section, mCurrentHeaderViewType != viewType ? null : mCurrentHeader);
//layout header,使它在最顶端
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) { // 下一个组的头部快滑动到顶部,距离顶部的距离小于现在在顶部悬浮的head的高度了
mHeaderOffset = headerTop - header.getHeight(); //MheaderOffset是小于0的
} else if (headerTop <= 0) { //下一个组的头部滑动到了顶部了
header.setVisibility(INVISIBLE);
}
}
}
invalidate();
}
我们一行一行的来看,
if (mOnScrollListener != null) {
mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
这里是对onScrollListener的set,因为我们在构造函数中setOnScrollListener(this),这句代码保证了用户也可以设置自己的onScrollListener
headerCount = getHeaderViewsCount();
得到header的个数
if (mAdapter == null || mAdapter.getCount() == 0 || !mShouldPin || (firstVisibleItem < headerCount)) {
mCurrentHeader = null;
mHeaderOffset = 0.0f;
for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
View header = getChildAt(i);
if (header != null) {
header.setVisibility(VISIBLE);
}
}
return;
}
如果adapter为空,或者adapter的count为0,或者我们设置了不顶部悬浮组头部等这些条件的话,就return,不再继续操作
firstVisibleItem -= getHeaderViewsCount();//去掉header view的影响
int section = mAdapter.getSectionForPosition(firstVisibleItem); //得到组号
int viewType = mAdapter.getSectionHeaderViewType(section);
mCurrentHeader = getSectionHeaderView(section, mCurrentHeaderViewType != viewType ? null : mCurrentHeader);
可以看出,通过getSectionForPosition方法得到了组号,然后根据getSectionHeaderView方法得到我们应该悬浮的组的header view
//layout header,使它在最顶端
ensurePinnedHeaderLayout(mCurrentHeader);
mCurrentHeaderViewType = viewType;
ensurePinnedHeaderLayout,顾名思义,确保pinned header 执行layout,而layout是为了保证pinned header的相对父布局的位置,我们看ensurePinnedHeaderLayout方法的实现
/**
* layout header,使它在最顶端
* @param header 组对应的头部view
*/
private void ensurePinnedHeaderLayout(View header) {
if (header.isLayoutRequested()) {
int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), mWidthMode);
int heightSpec;
ViewGroup.LayoutParams layoutParams = header.getLayoutParams();
if (layoutParams != null && layoutParams.height > 0) {
heightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
} else {
heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
header.measure(widthSpec, heightSpec);
header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
}
}
可以看出,对header执行了measure和layout,layout时left=0,top=0,也就是让header一直在顶部。
我们继续看scroll函数
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) {
// 下一个组的头部快滑动到顶部,距离顶部的距离小于现在在顶部悬浮的head的高度了
mHeaderOffset = headerTop - header.getHeight(); //MheaderOffset是小于0的
} else if (headerTop <= 0) { //下一个组的头部滑动到了顶部了
header.setVisibility(INVISIBLE);
}
}
}
invalidate();
使mHeaderOffset 置零
遍历所有可见的item,找到是sectionHeader的第i个item,得到header
看这一句话,if (pinnedHeaderHeight >= headerTop && headerTop > 0),意思是说,如果可见的元素中,第一个是SectionHeader的view距离顶部的距离小于现在悬浮在顶部的组的头部的高度,进行以下操作
mHeaderOffset = headerTop - header.getHeight(); //MheaderOffset是小于0的
给mHeaderOffset赋值。
我们来看mHeaderOffset在哪里用到的。是在disPatchDraw中用到了
dispatchDraw
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mAdapter == null || !mShouldPin || mCurrentHeader == null )
return;
int saveCount = canvas.save();
//沿y轴向下移动mHeaderOffset距离,把画布移动到(0,mHeaderOffset)
//注意,此处mHeaderOffset是<=0的,所以等于说是把画布往上移动了一段距离
canvas.translate(0, mHeaderOffset);
canvas.clipRect(0, 0, getWidth(), mCurrentHeader.getMeasuredHeight()); // needed
// for
// <
// HONEYCOMB
mCurrentHeader.draw(canvas);
canvas.restoreToCount(saveCount);
}
可以看出mHeaderOffset小于0的时候,正悬浮在顶部的view向上移动了mHeaderOffset距离。
到此为止,onScroll函数执行完毕了。
onItemClick方法
源码的onItemClick是有一些问题的,我在源码的基础上进行了修改。我们来看
先定义接口OnItemClickListener
public interface OnItemClickListener {
void onSectionItemClick(AdapterView<?> adapterView, View view, int section, int position, long id);
void onSectionClick(AdapterView<?> adapterView, View view, int section, long id);
void onHeaderClick(AdapterView<?> adapterView, View view, int position, long id);
void onFooterClick(AdapterView<?> adapterView, View view, int position, long id);
}
onSectionItemClick: 组的item被点击的点击回调
onSectionClick: 组的头部被点击的点击回调
onHeaderClick: list view的头部view被点击的点击回调
onFooterClick: list view的footer被点击的点击回调
onItemClick方法
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//header view
if(position < headerCount){
if(mOnItemClickListener !=null){
mOnItemClickListener.onHeaderClick(parent, view, position, id);
}
return;
}
//footer view
if(mAdapter!= null && position >= headerCount + mAdapter.getCount()){
if(mOnItemClickListener !=null){
mOnItemClickListener.onFooterClick(parent, view, position - headerCount - mAdapter.getCount(), id);
}
return;
}
//section header or section item
position = position - headerCount;
SectionedBaseAdapter adapter;
if (parent.getAdapter().getClass().equals(HeaderViewListAdapter.class)) {
HeaderViewListAdapter wrapperAdapter = (HeaderViewListAdapter) parent.getAdapter();
adapter = (SectionedBaseAdapter) wrapperAdapter.getWrappedAdapter();
} else {
adapter = (SectionedBaseAdapter) parent.getAdapter();
}
int section = adapter.getSectionForPosition(position);
int p = adapter.getPositionInSectionForPosition(position);
if (p == -1) {//click section header
if( mOnItemClickListener != null){
mOnItemClickListener.onSectionClick(parent, view, section, id);
}
} else {//click section item
if( mOnItemClickListener != null){
mOnItemClickListener.onSectionItemClick(parent, view, section, p, id);
}
}
}