PinnedHeaderListView开源组件源码分析

      之前看到一个开源组件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会悬浮在顶端,这样就实现了我们看到的效果。

       欢迎关注我的公众号一起交流学习

     

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值