PinnedSectionListView的实现原理

最近使用新浪体育客户端看NBA新闻,发现其比赛界面也采用了磁铁效果,即上下滑动ListView时,当前时间条会吸附在界面的顶部,具体效果如下图:


其实Android手机自带的联系人界面(至少Nexus 4是)也实现了这种效果,滑动联系人列表时,会把当前联系人分组首字母固定在顶端。这种效果的确很酷,如果你问这么酷的效果是否很难实现呢?请记住在我们尚未成为最前沿拓荒者的时候,总有人会在我们前面,我们要做的就是好好学习,努力追赶他们的脚步,当然这种效果也早有人实现,并且有开源代码,先附上地址:https://github.com/beworker/pinned-section-listview。有了源码我们很容易将这个效果添加到自己的应用中,但是我们需要也应该了解它的实现原理,下面就根据源代码简单分析一下其实现原理。

 

在分析实现代码之前,我想先把实现原理大致解释一下,这样在分析代码时就更有针对性,也会减少很多迷惑。第一次看到运行案例时我也在想究竟是如何实现这个吸附效果的,我们理解中的ListView的Item随着滑动是不会停止不动的,Demo中默认的颜色很难看出来是怎样产生吸附条的,因此请把要吸附的部分改为透明色,再观察滑动吸附就很容易看明白了,原来ListView仍然是我们认识的ListView,它的滑动效果和原理没有改变,那个吸附条是在ListView上面画上去的,不过是一个障眼法而已。当吸附条向上滑动,刚刚碰到顶部时,这时并没有绘制吸附条,当再往上滑动,则绘制一个当前吸附条的完整副本,固定在页面顶部,而ListView中的真正的吸附部分其实是已经继续上滑出去了,留下的只是一个绘制的副本,当再滑动到一个新的吸附条时,原理也是如此,同样向下滑动也是一样的道理。改为透明效果后的吸附条效果如下,可以很容易看明白:


大概了解了实现原理之后,来通过代码印证一下,核心源码很少,少到只有一个java文件,就是PinnedSectionListView.java,该类继承自ListView,很显然是自定义了一个带有磁铁吸附效果的ListView,而自定义View最简单的方法就是继承已有的View。

 

再来看一些重要的代码片段:

<span style="font-size:14px;">	/** List adapter to be implemented for being used with PinnedSectionListView adapter. */
	public static interface PinnedSectionListAdapter extends ListAdapter {
		/** This method shall return 'true' if views of given type has to be pinned. */
		boolean isItemViewTypePinned(int viewType);
	}

	/** Wrapper class for pinned section view and its position in the list. */
	static class PinnedSection {
		public View view;
		public int position;
		public long id;
	}</span>

第一段代码重新定义了一个接口,定义了一个布尔类型的方法:boolean isItemViewTypePinned(int viewType); 注释已经写的很清楚,该方法根据某一行viewTpye来判断是否将该行View吸附到顶部。第二段代码则定义了一个内部类,用来封装吸附部分的各个属性,包括view、position以及id。


对于ListView,最重要的就是滑动响应,因此我们来看OnScrollListener是如何实现的:

<span style="font-size:14px;">	/** Scroll listener which does the magic */
	private final OnScrollListener mOnScrollListener = new OnScrollListener() {

		@Override public void onScrollStateChanged(AbsListView view, int scrollState) {
			if (mDelegateOnScrollListener != null) { // delegate
				mDelegateOnScrollListener.onScrollStateChanged(view, scrollState);
			}
		}

		@Override
        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();
                }
            }
		};

	};</span>

来看关键部分,final boolean isFirstVisibleItemSection =isItemViewTypePinned(adapter, adapter.getItemViewType(firstVisibleItem)),这句代码判断当前ListView中的第一个可见元素是否为要吸附到顶端的磁条。后边的if-else语句则针对是和否做了两种处理。如果第一个元素是吸附条,且刚好在最顶端,这时是不需要绘制吸附条副本的,因此调用destroyPinnedShadow();如果没在最顶端,则根据当前吸附条的position创建一个吸附条副本,那么就会调用ensureShadowForPosition(firstVisibleItem, firstVisibleItem, visibleItemCount),在ensureShadowForPosition()方法里,会检查当然吸附条是否为null,是的话则调用 createPinnedShadow(sectionPosition)创建一个。如果第一个元素不是吸附条,则通过findCurrentSectionPosition(firstVisibleItem)函数计算当前吸附条的position,然后依然调用ensureShadowForPosition()绘制吸附条副本。


findCurrentSectionPosition(int fromPosition)函数中核心部分是一个循环遍历,根据行view的viewType来判断是否为吸附条。在使用该自定义的PinnedSectionListView时,必须使用初始定义的PinnedSectionListAdapter,目的是为了使用接口中定义的方法isItemViewTypePinned(int viewType) 来确定某一行是否为吸附条。


既然要绘制的吸附条副本都准备好了,那么再来看是怎么样绘制到ListView上的,具体代码如下:

<span style="font-size:14px;">	@Override
	protected void dispatchDraw(Canvas canvas) {
		super.dispatchDraw(canvas);

		if (mPinnedSection != null) {

			// prepare variables
			int pLeft = getListPaddingLeft();
			int pTop = getListPaddingTop();
			View view = mPinnedSection.view;

			// draw child
			canvas.save();

			int clipHeight = view.getHeight() +
			        (mShadowDrawable == null ? 0 : Math.min(mShadowHeight, mSectionsDistanceY));
			canvas.clipRect(pLeft, pTop, pLeft + view.getWidth(), pTop + clipHeight);

			canvas.translate(pLeft, pTop + mTranslateY);
			drawChild(canvas, mPinnedSection.view, getDrawingTime());

			if (mShadowDrawable != null && mSectionsDistanceY > 0) {
			    mShadowDrawable.setBounds(mPinnedSection.view.getLeft(),
			            mPinnedSection.view.getBottom(),
			            mPinnedSection.view.getRight(),
			            mPinnedSection.view.getBottom() + mShadowHeight);
			    mShadowDrawable.draw(canvas);
			}

			canvas.restore();
		}
	}</span>

上述代码里除了绘制吸附条,还绘制了吸附条下的阴影效果,这只是锦上添花了,无碍整个效果的实现原理。

 

以上简要分析了磁铁吸附效果的实现原理,详细代码及该自定义控件的使用请参考源代码。希望我们在使用开源代码的时候,还要知其所以然,一定要弄明白原理,再应用到自己的程序中,否则你就不能灵活地驾驭它。




评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值