美文网首页Android iOS开发知识库
意想不到的技巧 StickyListHeaders

意想不到的技巧 StickyListHeaders

作者: 894f7731744b | 来源:发表于2016-02-26 14:05 被阅读6050次

    StickyListHeaders这个控件目前被很多app广泛应用,翻译过来的意思是“ 粘列表标题”。
    是什么意思就不细说了,看下面的示例图就能明白。

    看起来感觉非常的神奇,那究竟是怎么做到的呢?
    开始之前,我们先约定好几个概念:
    上图中,列表中的所有数据称为dataSet(数据集),列表中的每条数据,称为item(列表项,它的id称为itemID),
    item被根据首字母分成了若干不同的section(分区, 它的id称为sectionID),
    每个section显示大写红色字母的地方叫做head(头部,它的id称为headID),每个item的位置称为position

    下面我们来分析一下源码,这个项目的Github地址是:https://github.com/emilsjolander/StickyListHeaders
    打开StickyListHeaders的 LIB(v2.0),核心的类总共有11个,包结构是很扁平的。


    类之间的关系如下所示:

    开发者在使用时主要面向StickyListHeadersListViewExpandableStickyListHeadersListViewStickyListHeadersAdapter这3个类,
    他们就好像原生控件当中的 ListView 和 BaseAdapter,用法也是类似的。
    请看下面的Demo:
    在这段 Activity 的 onCreate方法 中,首先获取到 ExpandableStickyListHeadersListView对象,然后为其设置Adapter对象,之后设置头部的点击事件,这个和我们平时对ListView的操作可以说是没有什么区别。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.expandable_sample);
        mListView = (ExpandableStickyListHeadersListView) findViewById(R.id.list);
        mTestBaseAdapter = new TestBaseAdapter(this);
        mListView.setAdapter(mTestBaseAdapter);
        mListView.setOnHeaderClickListener(new StickyListHeadersListView.OnHeaderClickListener() {
            @Override
            public void onHeaderClick(StickyListHeadersListView l, View header, int itemPosition, long headerId, boolean currentlySticky) {
                if(mListView.isHeaderCollapsed(headerId)){
                    mListView.expand(headerId);
                }else {
                    mListView.collapse(headerId);
                }
            }
        });
    }
    

    然后 我们再看一下 TestBaseAdapter 的实现,它继承于原生的 BaseAdapter 并实现了 StickyListHeadersAdapter 和 SectionIndexe 两个接口(其中 SectionIndexe 是原生中android.widget的接口)。

    public class TestBaseAdapter extends BaseAdapter implements
            StickyListHeadersAdapter, SectionIndexer {
     
        @Override
        public int getCount() {
            return mCountries.length;
        }
     
        @Override
        public Object getItem(int position) {
            return mCountries[position];
        }
     
        @Override
        public long getItemId(int position) {
            return position;
        }
     
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // TODO 产生 列表行的View
            // ... ... 
            return convertView;
        }
     
        @Override
        public View getHeaderView(int position, View convertView, ViewGroup parent) {
            // TODO 产生 分区头部的View
            // ... ... 
            return convertView;
        }
     
        @Override
        public long getHeaderId(int position) {
            // 根据 位置 获取 对应的 分区头部ID
            return headID;
        }
     
        @Override
        public int getPositionForSection(int section) {
            // 通过 位置 返回 对应的分区ID
            // ... ...
            return positionForSection;
        }
     
        @Override
        public int getSectionForPosition(int position) {
            // 通过 分区ID 返回 对应的 位置
            // ... ...
            return sectionForPosition;
        }
     
        @Override
        public Object[] getSections() {
            // 获取 分区的对象
            return mSectionLetters;
        }
     
    }
    

    OK,到目前为止,我们可以推断出,StickyListHeadersListView (或者是ExpandableStickyListHeadersListView)的用法和 ListView 是一样的。
    即:首先获取到 StickyListHeadersListView 对象,然后编写 Adapter 对象与之关联,当 StickyListHeadersListView 被添加到屏幕上准备开始渲染时,底层会调用 onMeasure方法 和 onLayout方法,并执行 layoutChild方法,
    这个时候 StickyListHeadersListView 会根据 Adapter 中的 getView方法 返回的 View对象,将视图组合起来,并最后渲染到屏幕上。
    Fine,在上面的论证基础上,我们把跟踪代码的入口,设定在StickyListHeadersListView 的setAdapter方法,这里我们从demo中ExpandableStickyListHeadersListView的 setAdapter方法 进入:

    @Override
    public void setAdapter(StickyListHeadersAdapter adapter) {
        mExpandableStickyListHeadersAdapter = new ExpandableStickyListHeadersAdapter(adapter);
        super.setAdapter(mExpandableStickyListHeadersAdapter);
    }
    

    这里用 ExpandableStickyListHeadersListView 重新构造了一个 ExpandableStickyListHeadersAdapter 对象,并传递到父类(StickyListHeadersListView )的setAdapter中。
    我们来一下 ExpandableStickyListHeadersAdapter 这个类的实现 :

    class ExpandableStickyListHeadersAdapter extends BaseAdapter implements StickyListHeadersAdapter {
     
        private final StickyListHeadersAdapter mInnerAdapter;
        DualHashMap<View, Long> mViewToItemIdMap = new DualHashMap<View, Long>();
        DistinctMultiHashMap<Integer, View> mHeaderIdToViewMap = new DistinctMultiHashMap<Integer, View>();
        List mCollapseHeaderIds = new ArrayList();
     
        ExpandableStickyListHeadersAdapter(StickyListHeadersAdapter innerAdapter) {
            this.mInnerAdapter = innerAdapter;
        }
     
        // ... ... 
    }
    

    可以发现,ExpandableStickyListHeadersAdapter 也是继承了原生的 BaseAdapter 并实现了 StickyListHeadersAdapter,但是多了 DualHashMap 、DistinctMultiHashMap 这两个Map对象和一个存储 Long型数据的List对象。
    其中,DualHashMap 是实现了 “Key和Value双向关联关系” 的数据结构,也就是说,可以通过 Key 取到 Value,也可以通过 Value 取到 Key。
    而 DistinctMultiHashMap 是实现了 “一个 Key对应N个Value的关联关系” 的数据结构,可以通过一个Key取到所有(多个)和这个Key有关的Value对象集合。
    根据 变量名称 我们可以判断,DualHashMap 和 DistinctMultiHashMap 是用来实现 数据分区ID(headID) 和 分区内View 的双向关联管理。
    由代码中我们看到,ExpandableStickyListHeadersAdapter 的访问权限并不是public,而是default,所以我们是无法从外部直接实例化这种类型的对象进行使用的。
    这样的设计应该是为了简化开发者实现 Adapter 的代码量,把用来处理 数据分区ID(headID) 和 分区内View 的双向关联管理的逻辑封装了起来。
    接着,我们进入到 StickyListHeadersListView 的 setAdapter方法 中:

    public void setAdapter(StickyListHeadersAdapter adapter) {
        if (adapter == null) {
            if (mAdapter instanceof SectionIndexerAdapterWrapper) {
                ((SectionIndexerAdapterWrapper) mAdapter).mSectionIndexerDelegate = null;
            }
            if (mAdapter != null) {
                mAdapter.mDelegate = null;
            }
            mList.setAdapter(null);
            clearHeader();
            return;
        }
        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }
     
        if (adapter instanceof SectionIndexer) {
            mAdapter = new SectionIndexerAdapterWrapper(getContext(), adapter);
        } else {
            mAdapter = new AdapterWrapper(getContext(), adapter);
        }
        mDataSetObserver = new AdapterWrapperDataSetObserver();
        mAdapter.registerDataSetObserver(mDataSetObserver);
     
        if (mOnHeaderClickListener != null) {
            mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler());
        } else {
            mAdapter.setOnHeaderClickListener(null);
        }
     
        mAdapter.setDivider(mDivider, mDividerHeight);
     
        mList.setAdapter(mAdapter);
        clearHeader();
    }
    

    这里是我们要重点讲解的地方之一,在这个方法中,我们可以看到三个重要的对象:mList、mAdapter 和 mDataSetObserver,
    首先看这三个变量的定义:mList 是一个 WrapperViewList 类型的成员变量,他是一个原生的 ListView 的子类。
    mAdapter是一个 AdapterWrapper 类型的成员变量,他是一个原生 BaseAdapter 的子类; mDataSetObserver 则是 AdapterWrapperDataSetObserver 类型的成员变量,是 DataSetObserver 的子类,它负责在数据集发生改变时通知外部的监听(观察者模式),下面是AdapterWrapperDataSetObserver 的定义:

    private class AdapterWrapperDataSetObserver extends DataSetObserver {
     
        @Override
        public void onChanged() {
            clearHeader();
        }
     
        @Override
        public void onInvalidated() {
            clearHeader();
        }
     
    }
    

    通过上面对 StickyListHeadersListView 的 setAdapter方法 的阅读,我们发现此处的逻辑,是根据用户传入的 StickyListHeadersAdapter对象,构造出 AdapterWrapper(或者SectionIndexerAdapterWrapper)对象,并 set 到其内部成员变量 mList 中。由于 StickyListHeadersListView 并不是 ListView 的子类,而是继承于FrameLayout,所以 StickyListHeadersListView 中的列表控件,实际上就是 mList 这个成员变量。
    根据ListView的布局原理,StickyListHeadersListView 要进行渲染时,会调用 AdapterWrapper 的getView方法,构建出内部的子视图,完成控件的布局。
    所以我们下一步便要看一看 AdapterWrapper 对象的 getView方法:

    @Override
    public WrapperView getView(int position, View convertView, ViewGroup parent) {
       WrapperView wv = (convertView == null) ? new WrapperView(mContext) : (WrapperView) convertView;
       View item = mDelegate.getView(position, wv.mItem, parent);
       View header = null;
       if (previousPositionHasSameHeader(position)) {
          recycleHeaderIfExists(wv);
       } else {
          header = configureHeader(wv, position);
       }
       if((item instanceof Checkable) && !(wv instanceof CheckableWrapperView)) {
          // Need to create Checkable subclass of WrapperView for ListView to work correctly
          wv = new CheckableWrapperView(mContext);
       } else if(!(item instanceof Checkable) && (wv instanceof CheckableWrapperView)) {
          wv = new WrapperView(mContext);
       }
       wv.update(item, header, mDivider, mDividerHeight);
       return wv;
    }
    

    非常的清晰明了,这边负责创建每个 item 的视图。首先根据 mDelegate 的 getView 方法,获取每个position所要创建的 view 的类型,mDelegate 实际上就是我们创建的 StickyListHeadersAdapter 对象实例。同时 数据集分区头部的视图 也是在此创建,就是 header 成员变量,通过 previousPositionHasSameHeader方法,判断上一个位置是否有头部视图,来决定是否复用旧的头部视图 或者是 重新创建一个。最后根据 view 的类型 就创建出对应的 WrapperView 对象,WrapperView 就是最后渲染到屏幕上 每条item的实际视图对象。
    此处的 upadte方法 负责对 WrapperView 对象进行设置,达到最终的绘制效果。upadte方法 的入参为item, header, mDivider, mDividerHeight四个参数,
    接着我们来看一下 upadte方法 都做了什么:

    void update(View item, View header, Drawable divider, int dividerHeight) {
        
       //every wrapperview must have a list item
       if (item == null) {
          throw new NullPointerException("List view item must not be null.");
       }
     
       //only remove the current item if it is not the same as the new item. this can happen if wrapping a recycled view
       if (this.mItem != item) {
          removeView(this.mItem);
          this.mItem = item;
          final ViewParent parent = item.getParent();
          if(parent != null && parent != this) {
             if(parent instanceof ViewGroup) {
                ((ViewGroup) parent).removeView(item);
             }
          }
          addView(item);
       }
     
       //same logik as above but for the header
       if (this.mHeader != header) {
          if (this.mHeader != null) {
             removeView(this.mHeader);
          }
          this.mHeader = header;
          if (header != null) {
             addView(header);
          }
       }
     
       if (this.mDivider != divider) {
          this.mDivider = divider;
          this.mDividerHeight = dividerHeight;
          invalidate();
       }
    }
    

    由于WrapperView是ViewGroup的子类,所以必须要重写onLayout方法,我们再看一下WrapperView的onLayout方法:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
     
       l = 0;
       t = 0;
       r = getWidth();
       b = getHeight();
     
       if (mHeader != null) {
          int headerHeight = mHeader.getMeasuredHeight();
          mHeader.layout(l, t, r, headerHeight);
          mItemTop = headerHeight;
          mItem.layout(l, headerHeight, r, b);
       } else if (mDivider != null) {
          mDivider.setBounds(l, t, r, mDividerHeight);
          mItemTop = mDividerHeight;
          mItem.layout(l, mDividerHeight, r, b);
       } else {
          mItemTop = t;
          mItem.layout(l, t, r, b);
       }
    }
    

    可以看到,这里对 mHeader、mDivider 和 mItem 这三个对象进行 layout操作,而这三个对象就是update方法入参中的 head、divider 和 item。
    通过分析,我们发现 StickyListHeadersListView 中的每个item视图 “是否有头部、是否有分割线、高度是多少”,实际上都是根据 update方法 传入的参数,在 WrapperView 中进行设置的。

    StickyListHeadersListView 的实现原理,实际上是通过对 ListView 和 BaseAdapter 进行了二次封装实现。
    通过用户构造的Adapter对象,在内部重新构造一个封装过的新Adapter对象,再绑定到内部的ListView中。
    所以 ListView 的所有特性在 StickyListHeadersListView 都得到的保存,包括重要的RecycleBin视图复用结构。实际上用户还可以通过StickyListHeadersListView的getWrappedList() 方法直接获取到内部ListView进行操作,扩展出自己的API。
    通过本文开始处的效果图,我们看到在滚到列表的时候,每个分区的头部视图(headView)可以不断被顶掉替换,这样的操作是如何实现的呢?实际上这也是在 StickyListHeadersListView 内完成的,StickyListHeadersListView定义了一个内部类 WrapperListScrollListener,它实现了 AbsListView 的 OnScrollListener 接口,它的作用是监听成员变量mList的滚到情况。当用户滚到列表的时候,WrapperListScrollListener中的onScroll方法便会被调用。其中的 updateOrClearHeader方法 负责设置顶部 headView 的显示和偏移,具体的实现是在
    ensureHeaderHasCorrectLayoutParams方法 中更改头部视图的LayoutParams。

    private class WrapperListScrollListener implements OnScrollListener {
     
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
                             int visibleItemCount, int totalItemCount) {
            if (mOnScrollListenerDelegate != null) {
                mOnScrollListenerDelegate.onScroll(view, firstVisibleItem,
                        visibleItemCount, totalItemCount);
            }
            updateOrClearHeader(mList.getFixedFirstVisibleItem());
        }
     
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            if (mOnScrollListenerDelegate != null) {
                mOnScrollListenerDelegate.onScrollStateChanged(view,
                        scrollState);
            }
        }
     
    }
    
    private void ensureHeaderHasCorrectLayoutParams(View header) {
            ViewGroup.LayoutParams lp = header.getLayoutParams();
            if (lp == null) {
                lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
                header.setLayoutParams(lp);
            } else if (lp.height == LayoutParams.MATCH_PARENT || lp.width == LayoutParams.WRAP_CONTENT) {
                lp.height = LayoutParams.WRAP_CONTENT;
                lp.width = LayoutParams.MATCH_PARENT;
                header.setLayoutParams(lp);
            }
        }
    

    相关文章

      网友评论

      • ebe22d2852f0:你好楼主,这个StickyListHeaders字布局里面嵌套如果是一个GridView的话数据过多会出现卡顿怎么办,还有数据过多会出现滚动条怎么消除啊?
        Demon_gu:您好,请问您解决这个问题了嘛?我也出现了这个问题

      本文标题:意想不到的技巧 StickyListHeaders

      本文链接:https://www.haomeiwen.com/subject/uukwkttx.html