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<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会悬浮在顶端,这样就实现了我们看到的效果。

版本控制

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

项目地址

所在文件夹 demo位置
widget.PinnedHeaderListView com.qr.demo.widget.PinnedHeaderListViewActivity
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,409评论 25 707
  • 好久没有变身成身经百战的社会人士了,突然的视频面试居然有点慌张,慌张到好多问题第一反应答不上来,呜……我老是这样的...
    JOY你是谁阅读 2,044评论 0 0
  • 婀娜梅枝迎飞雪,冰霜花蕊送清风。 摄影:刁刁
    郭绍武阅读 411评论 0 0
  • 三穷三富过到老,十年兴败谁知晓,谁人背后无人说,谁人背后不说人!什么是真,什么是假,喜我者,我惜之,嫌我者,我弃之...
    王路柯阅读 981评论 0 0
  • 留意到自己最近几天的情绪有些低落,身体有些不舒服。可能的原因是受外部比较的压力影响,也可能最近的事情比较多,给自己...
    sageness阅读 188评论 0 0