PinnedSectionListView的实现原理

来源:互联网 发布:玩游戏的时候网络优先 编辑:程序博客网 时间:2024/06/14 01:42

最近使用新浪体育客户端看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) { // delegatemDelegateOnScrollListener.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;">@Overrideprotected void dispatchDraw(Canvas canvas) {super.dispatchDraw(canvas);if (mPinnedSection != null) {// prepare variablesint pLeft = getListPaddingLeft();int pTop = getListPaddingTop();View view = mPinnedSection.view;// draw childcanvas.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>

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

 

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




1 0