您的位置:首页 > 其它

PinnedSectionListView源码分析

2015-09-02 03:22 183 查看
本文传送门:/article/9521393.html

本文对开源项目PinnedSectionListView(https://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接口以提高列表滚动时的流畅度。

部分代码没有写在文章里,如有需要欢迎留言讨论。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: