PinnedHeaderListView

组件介绍

所有的View分组,每一组都有一个Header,上下滑动到某一个组的时候,它的Header都会悬浮在顶部

  1. 当组的头部从屏幕顶部消失,而且组还有成员在屏幕内的时候,组的头部悬浮在屏幕顶部
  2. 当下一个组的头部滑到屏幕顶部与悬浮头部挨着的时候,把悬浮头部
    顶走,最终悬浮的头部被替代
pinnedHeaderView.gif

为了更容易理解,需要首先来说说明一下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 mSectionPositionCache;  
/**  
 * Holds the calculated values of @{link getSectionForPosition}  
 */  
private SparseArray mSectionCache;  
/**  
 * Holds the calculated values of @{link getCountForSection}  
 */  
private SparseArray mSectionCountCache;  
  
/**  
 * Caches the item count  
 */  
private int mCount;  
/**  
 * Caches the section count  
 */  
private int mSectionCount;  
  
public SectionedBaseAdapter() {  
    super();  
    mSectionCache = new SparseArray();  
    mSectionPositionCache = new SparseArray();  
    mSectionCountCache = new SparseArray();  
    mCount = -1;  
    mSectionCount = -1;  
}  

1-2行,定义了两种类型HEADER_VIEW_TYPE和ITEM_VIEW_TYPE,分别对应section header和section item。
7、11、15行定义了三个SparseArray,它是Android上对HashMap的性能更优的替代品。我们可以将他们当做HashMap来理解。他们的作用是用来对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会悬浮在顶端,这样就实现了我们看到的效果。

版本控制

版本号 更新内容 修改人 修改时间
1.0 初次发布 lucky_tiger 2017/7/13

项目地址

所在文件夹 demo位置
widget.PinnedHeaderListView com.qr.demo.widget.PinnedHeaderListViewActivity

你可能感兴趣的:(PinnedHeaderListView)