PinnedHeaderListView开源组件源码分析

来源:互联网 发布:如何清理mac其他内存 编辑:程序博客网 时间:2024/06/03 19:23

      之前看到一个开源组件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();}
       这个类里的变量定义和构造函数等内容我们不在这里啰嗦了,直接看最重要的一部分代码,这也是实现这个功能的关键。
@Overridepublic 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。

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

0 0
原创粉丝点击