PinnedSectionListView源码分析



本文传送门:http://blog.csdn.net/u011429058/article/details/48128889
本文对开源项目PinnedSectionListViewhttps://github.com/beworker/pinned-section-listview的源码进行了分析。

PinnedSectionListView这个类的特色就是在ListView主体上面钉住一个浮窗,用来显示当前处在列表中第一个位置的Item所属的Section。这个浮窗是单独在dispatchDraw函数中画出来的,不是ListView的子View。

PinnedSectionLivtView钉在最顶端的浮窗会随着列表的滚动而切换,这个事件必然和onScroll函数有直接关系,所以从onScroll入手。
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

            if (mDelegateOnScrollListener != null) { // delegate
                mDelegateOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
            }

            // get expected adapter or fail fast
            ListAdapter adapter = getAdapter();
            if (adapter == null || visibleItemCount == 0) return; // nothing to do

            final boolean isFirstVisibleItemSection =
                    isItemViewTypePinned(adapter, adapter.getItemViewType(firstVisibleItem));

            if (isFirstVisibleItemSection) {
                View sectionView = getChildAt(0);
                if (sectionView.getTop() == getPaddingTop()) { // view sticks to the top, no need for pinned shadow
                    destroyPinnedShadow();
                } else { // section doesn't stick to the top, make sure we have a pinned shadow
                    ensureShadowForPosition(firstVisibleItem, firstVisibleItem, visibleItemCount);
                }

            } else { // section is not at the first visible position
                int sectionPosition = findCurrentSectionPosition(firstVisibleItem);
                if (sectionPosition > -1) { // we have section position
                    ensureShadowForPosition(sectionPosition, firstVisibleItem, visibleItemCount);
                } else { // there is no section for the first visible item, destroy shadow
                    destroyPinnedShadow();
                }
            }

这里提一下,ListView所拥有的数据和Adapter中所拥有的数据是有区别的。Adapter中保存的数据就是我们需要显示的数据,比如有100条。而ListView所拥有的数据则指屏幕上显示的这些数据,最多也就10来条。只显示了一部分的Item也算是1个。

onScroll中先用isItemViewTypePinned判断了当前显示的第一个Item类型是不是Section,并分支出两种不同操作。
onScroll分支①:ListView第一个是Section。
根据该Section的位置来决定要不要显示浮窗。Section的位置有两种情况,如下图:

destroyPinnedShadow()不是直接销毁浮窗的,而是对清空了用来保存浮窗信息的mPinnedSection
	void destroyPinnedShadow() {
	    if (mPinnedSection != null) {
	        // keep shadow for being recycled later
	        mRecycleSection = mPinnedSection;
	        mPinnedSection = null;
	    }
	}

真正的绘制浮窗是在dispatchDraw(Canvas canvas) 函数中,如果mPinnedSection不为空则根据它保存的view信息来绘制浮窗。而dispatchDraw函数是在onScroll被调用之后再被调用的,所以onScroll中对浮窗状态信息的修改能马上在dispatchDraw中体现出来。

为了更直观一点,我在PinnedSectionListView的onScroll、onScrollStateChanged、onDraw、dispatchDraw和mOnScrollListener的onScroll onScrollStateChanged 函数中加入了Log信息,然后触发一次划屏动作打印出了如下Log。onScrollStateChanged函数是在scroll三种状态发生切换时被调用,而onScroll、onDraw和dispatchDraw则不断地重复调用。前面分析的onScroll函数是定义在mOnScrollListener中的,对应日志中的 “in listener onScroll~~~“,它发生在”dispatchDraw~~~“之前。



destroyPinnedShadow()相反的是ensureShadowForPosition()函数。
    void ensureShadowForPosition(int sectionPosition, int firstVisibleItem, int visibleItemCount) {
        if (visibleItemCount < 2) { // no need for creating shadow at all, we have a single visible item
            destroyPinnedShadow();
            return;
        }

        if (mPinnedSection != null
                && mPinnedSection.position != sectionPosition) { // invalidate shadow, if required
            destroyPinnedShadow();
        }

        if (mPinnedSection == null) { // create shadow, if empty
            createPinnedShadow(sectionPosition);
        }

        // align shadow according to next section position, if needed
        int nextPosition = sectionPosition + 1;
        if (nextPosition < getCount()) {
            int nextSectionPosition = findFirstVisibleSectionPosition(nextPosition,
                    visibleItemCount - (nextPosition - firstVisibleItem));
            if (nextSectionPosition > -1) {
                View nextSectionView = getChildAt(nextSectionPosition - firstVisibleItem);
                final int bottom = mPinnedSection.view.getBottom() + getPaddingTop();
                mSectionsDistanceY = nextSectionView.getTop() - bottom;
                if (mSectionsDistanceY < 0) {
                    // next section overlaps pinned shadow, move it up
                    mTranslateY = mSectionsDistanceY;
                } else {
                    // next section does not overlap with pinned, stick to top
                    mTranslateY = 0;
                }
            } else {
                // no other sections are visible, stick to top
                mTranslateY = 0;
                mSectionsDistanceY = Integer.MAX_VALUE;
            }
        }

    }
此函数的作用是,如果mPinnedSection不存在,则根据参数中提供的sectionPosition通过createPinnedShadow()重新创建它。创建mPinnedSection的关键代码就是下面这句,它调用了SimpleAdapter的getView函数去获取指定Section的View:
View pinnedView = getAdapter().getView(position, pinnedShadow.view, PinnedSectionListView.this);

如果mPinnedSection已经存在,则对其中保存的浮窗位置信息做了一些调整。
下图解释了代码中和位置信息相关的几个变量。(0,0)点是屏幕左上角,红线是ListView上下界,绿色的是两个section。


ensureShadowForPosition()中会调用到函数findFirstVisibleSectionPosition。这个函数参数1表示查找的起始点,参数2表示向下查找的个数,如果发现有Section则返回其位置。(参数改成startPosition、itemCount会更合适点)。
	int findFirstVisibleSectionPosition(int firstVisibleItem, int visibleItemCount) {
		ListAdapter adapter = getAdapter();

        int adapterDataCount = adapter.getCount();
        if (getLastVisiblePosition() >= adapterDataCount) return -1; // dataset has changed, no candidate

        if (firstVisibleItem+visibleItemCount >= adapterDataCount){//added to prevent index Outofbound (in case)
            visibleItemCount = adapterDataCount-firstVisibleItem;
        }

		for (int childIndex = 0; childIndex < visibleItemCount; childIndex++) {
			int position = firstVisibleItem + childIndex;
			int viewType = adapter.getItemViewType(position);
			if (isItemViewTypePinned(adapter, viewType)) return position;
		}
		return -1;
	}


onScroll分支②:ListView第一个不是Section。
显示第一个Item其所归属的Section浮窗。其中findCurrentSectionPosition(int fromPosition)的作用是用来查找fromPosition位置的Item所属的Section Item的位置。
	int findCurrentSectionPosition(int fromPosition) {
		ListAdapter adapter = getAdapter();

		if (fromPosition >= adapter.getCount()) return -1; // dataset has changed, no candidate
		
		if (adapter instanceof SectionIndexer) {
			// try fast way by asking section indexer
			SectionIndexer indexer = (SectionIndexer) adapter;
			int sectionPosition = indexer.getSectionForPosition(fromPosition);
			int itemPosition = indexer.getPositionForSection(sectionPosition);
			int typeView = adapter.getItemViewType(itemPosition);
			if (isItemViewTypePinned(adapter, typeView)) {
				return itemPosition;
			} // else, no luck
		}

		// try slow way by looking through to the next section item above
		for (int position=fromPosition; position>=0; position--) {
			int viewType = adapter.getItemViewType(position);
			if (isItemViewTypePinned(adapter, viewType)) return position;
		}
		return -1; // no candidate found
	}

里面分了两种方式。一种是adapter实现了SectionIndexer,这种方式能根据SectionIndexer的接口函数快速获得section位置;另一种是从当前位置向上挨个查找,直到找到一个type是SECTION的列表项。我们自定义的Adapter尽量实现SectionIndexer接口以提高列表滚动时的流畅度。

部分代码没有写在文章里,如有需要欢迎留言讨论。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值