Android ListView 原理解析
现在这种复用型的列表View,已经逐步用RecyclerView代替,有更好的性能和更便捷的操作,具体原理可以参照 这篇文章。
一.模块分析
1.列表控件
(1)继承关系
(2)层次说明
AdapterView是一种ViewGroup,负责展示与一组数据相关的view,这些数据由一种adapter进行管理,该ViewGroup通过adapter来监听数据的改变以及相应的处理
AbsListView继承自AdapterView,泛型参数为ListAdapter,一个列表adapter,该ViewGroup有一套view的回收复用机制供子类使用,专门负责对多数据多view的控件的展示优化
ListView与GridView都继承自AbsListView,使用了其父类的回收机制,并重写相应的布局方法来实现各自的布局
ExpandableListView继承了ListView,只是对数据的展示划分的更细一层,分为section和child
2.Adapter适配器
列表控件是为了展示一组关联数据的view的,他的工作模式就是一个列表控件加上一个数据源即可,但是列表控件并不需要关系具体是什么类型的数据源,因为如果要列表控件直接和数据源打交道,那么控件所要做的工作就很繁琐而且没有任何扩展性
所以只需要为这个数据源定义一个通用的实现接口,使控件通过指定的方法去完成对数据的监控和使用,而不需要关系数据是什么类型
Adapter就是一个接口,定义了一组数据的统一方法,可以实现该接口完成各种各样的子类,比如ArrayAdapter专门存放一个简单数组的adapter、SimpleCursorAdapter存放游标数据的adapter等等,而列表控件只需知道这是一种数据源、并可以通过同样的方法来访问数据即可
3.RecycleBin回收机制
这货就是回收view的主要类,他是AbsListView的一个内部类(刚刚提到,回收是由列表控件基类AbsListView实现的),下面是一些主要的代码和简单的注解,先有个概念,下面分析流程时会重点讲到他的运作流程
class RecycleBin{
//主要field
private View[] mActiveViews = new View[0];//这是存储正在显示中的view的集合
private int mViewTypeCount;//itemTypeCount,有多少种item的type,也就是我们常写的adapter里的getViewTypeCount的值
private ArrayList<View>[] mScrapViews;//这是已被回收,等待重用的集合数组,为什么是集合的数组呢,因为要根据不同的itemType来存储相应type的已回收的view
private ArrayList<View> mCurrentScrap;//这是已被回收的view的集合,与上面的集合数组不同,这是itemTypeCount为1的时候存放回收view的集合,上面那个是按type存的所以要多个集合
//主要方法
//1.初始化设置(每次setAdapter时候会更新)
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {//viewTypeCount必须得大于0
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
//noinspection unchecked
ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];//这里要注意,viewType范围得是0~viewTypeCount-1
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList<View>();
}
mViewTypeCount = viewTypeCount;//初始化设置一些参数
mCurrentScrap = scrapViews[0];
mScrapViews = scrapViews;
}
//2.添加正在显示的view到集合
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;
//noinspection MismatchedReadAndWriteOfArray
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
// Don't put header or footer views into the scrap heap
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
// However, we will NOT place them into scrap views.
activeViews[i] = child;
// Remember the position so that setupChild() doesn't reset state.
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
//3.获取activeView
View getActiveView(int position) {
int index = position - mFirstActivePosition;//真实的child的index
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;//取出返回,不能再次使用
return match;
}
return null;
}
//4.添加view到回收池
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
...
lp.scrappedFromPosition = position;//记录lp的position,后续先根据position取view
// Remove but don't scrap header or footer views, or views that
// should otherwise not be recycled.
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {//不应该被回收
...
return;
}
...
if (mViewTypeCount == 1) {//就一种viewType放入到mCurrentScrap中
mCurrentScrap.add(scrap);
} else {//放入指定viewType的scrapViews集合里
mScrapViews[viewType].add(scrap);
}
...
}
}
//5.获取一个scrapView
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
...
从指定的scrap集合取view
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
// 如果仍然有以该position或者getItemId所指定的id相关联的view则取出
for (int i = 0; i < size; i++) {
final View view = scrapViews.get(i);
final AbsListView.LayoutParams params =
(AbsListView.LayoutParams) view.getLayoutParams();
if (mAdapterHasStableIds) {
final long id = mAdapter.getItemId(position);//itemId
if (id == params.itemId) {
return scrapViews.remove(i);
}
} else if (params.scrappedFromPosition == position) {//addScrapView时记录的position
final View scrap = scrapViews.remove(i);
clearAccessibilityFromScrap(scrap);
return scrap;
}
}
final View scrap = scrapViews.remove(size - 1);//否则拿出并移除scrapView集合的最后一个来使用
clearAccessibilityFromScrap(scrap);
return scrap;
} else {
return null;
}
}
}
RecycleBin主要管理着回收的view的集合以及正在显示的view的集合,还有对这些的集合的增删查功能,列表控件会在自己的不同处理时机调用这些方法来完成对view的复用
二.View布局流程分析
View的执行流程无非就分为三步,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并没有什么特殊的地方,因为它终归是一个View,占用的空间最多并且通常也就是整个屏幕。onDraw()在ListView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的,AbsListView里的onLayout没有进行过多的处理,真正的布局子view的方法是layoutChildren(),由子类重写实现
1.ListView的layoutChildren()方法
protected void layoutChildren() {
...
if (mAdapter == null) {//adapter为null
resetList();//清空已有views和各种集合、状态
invokeOnItemScrollListener();
return;
}
...
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();//处理一些逻辑,mLayoutMode的改变
}
...
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {//数据改变,现有view全部添加到scrapView集合,准备重新设置item时复用
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {//没有改变数据,只是重新layout,那么将所有现有view加入到activeViews准备复用
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent();//将所有view从parent上detach掉,后续这些复用的view只需attach上即可,不会重新inflate
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {//根据mLayoutMode决定加载view的方向和方式
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
case LAYOUT_SPECIFIC:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
default://NORMAL情况(一般情况)
if (childCount == 0) {//当前无view显示
if (!mStackFromBottom) {//从顶到底布局
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);//从顶到底布局
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {//已有显示的view
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {//通常情况
//从mFirstPosition位置开始设置view(从top到mFirstPosition-1,从mFirstPosition+1到bottom)
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
...
}
layoutChildren主要是根据数据是否改变来更新activeViews或者scrapViews的状态,然后根据数据状态来进行不同方向不同position的itemView的添加
有一点需要注意,在更新完active和scrapView后要将全部view从parent上detach掉,因为当view因为一些原因多次调用layoutChildren时,会执行相同的逻辑,如果不detach掉会有相同的view会在后面attach
2.fillFromTop()方法-→fillDown()方法ViewGroup顶部到底部设置view
fillFromTop方法主要调用fillDown方法,从ViewGroup的顶部到底部进行view的添加
其余的fillUp、fillSpecified等方法就是从不同的位置不同的方向开始添加itemView,方法和fillDown一样,这里只拿fillDown来举例
private View fillDown(int pos, int nextTop) {
...
//从第pos个item开始,拿到其view,根据view的大小增加top的值,循环添加itemView,直到下一个view的top已经大于ViewGroup的bottom或者全部item添加完成
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
...
pos++;
}
...
}
由代码可见,从top开始逐个添加itemView,并计算下一个item的top,直到下一个top大于底部就停止添加,所以一次只加载一屏的数据
makeAndAddView()方法就是获取和添加每个itemView的方法,也是复用view的地方
3.makeAndAddView()
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {//没有数据改变时
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);//从activeViews里拿到view(上面说到在没有改变时将已有view全部放入到了activeViews里)
if (child != null) {//没有拿到还需要进行obtainView获取
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);//添加view到层级上
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);//复用或新建view
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);//添加view到层级上
return child;
}
由代码可知,没有数据改变时就获取activeViews里的view继续显示即可,否则需要去obtainView进行复用或者新建view来添加
obtainView就是从scrapViews里复用或者新建的方法;当拿到view时,setupChild方法就是将view添加到层级中的方法
4.obtainView()
View obtainView(int position, boolean[] isScrap) {
...
final View scrapView = mRecycler.getScrapView(position);//拿到view
final View child = mAdapter.getView(position, scrapView, this);//调用adapter的getView并传入该view(就是我们常用的getView方法)
if (scrapView != null) {//为null说明child是新建的view
if (child != scrapView) {//当child没有复用而是新建的view时,要将scrapView再添加回scrapViews里(这就是为什么我们每次新建的话都会创建新的view到scrapViews里,没有复用效果)
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else {
isScrap[0] = true;//将该标志位置为true说明view是复用的,后续不用重新measure
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
...
}
上面说到getScrapView会先根据position拿view,如果还存有对应position(addScrapView时会设置对应的position)的scrapView就拿出来,否则就从scrapViews里拿出最后一个来复用
5.setupChild()
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
...
// Respect layout params that are already in the view. Otherwise make some up...
// noinspection unchecked
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();//没有则设置默认的lp
}
p.viewType = mAdapter.getItemViewType(position);//将viewType设置到lp里(add和get时根据type来处理)
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {//如果是复用的view(还记的上面说的将isScrap[0]标志置为true么)
attachViewToParent(child, flowDown ? -1 : 0, p);//将view重新attach到parent上即可
} else {//是新建的view
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);//第一次需要add到parent里
}
...
if (needToMeasure) {//是否需要测量和是否是回收的view以及AbsListView的onLayout时设置child是否需要requestLayout有关
final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
final int lpHeight = p.height;
final int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childTop = flowDown ? y : y - h;
if (needToMeasure) {//measure完计算ltrb进行child的layout
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
...
}
主要是根据是否是使用复用的view来判断应该将该view重新attach到parent上还是首次add到parent上
然后根据需要来measure和layout该child完成添加
三.滚动时view的展示与复用
1.拖动滑动
在AbsListView的onTouchEvent方法中,当捕获到MOVE事件时交由onTouchMove方法处理,该方法通过判断scroll的类型是普通的SCROLL时调用scrollIfNeeded方法,计算从开始触摸到此刻的deltaY距离以及增量incrementY,调用trackMotionScroll方法进行处理view
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
...
final boolean down = incrementalDeltaY < 0;
...
if (down) {//ListView向上滚动
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
for (int i = 0; i < childCount; i++) {//从顶到底遍历view,看是否产生不可见view(bottom<top)
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);//回收
}
}
}
} else {//ListView向下滚动
int bottom = getHeight() - incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
bottom -= listPadding.bottom;
}
for (int i = childCount - 1; i >= 0; i--) {//从底到顶遍历view,看是否产生不可见view(top>bottom)
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);//回收
}
}
}
}
...
if (count > 0) {//将回收的view从parent上detach掉
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
offsetChildrenTopAndBottom(incrementalDeltaY);//通过改变每个view的top来相当于移动view
...
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);//有新的view可见,调用此方法进行添加(此方法由子类重写实现自己的填充新view)
}
...
}
由代码可知,通过判断incrementY的正负来确定是向上滑动还是向下滑动,并判断相应的view是否不可见,如果处于不可见则要addScrapView,并将这些不可见的view从parent中detach掉;然后要将所有的view通过改变top来移动位置;最后,如果有新的view从顶部或底部可见,调用fillGap来填充该view,fillGap核心还是调用的fillDown或者fillUp,不再叙述
2.Fling滑动
在fling状态下,onTouchUp处理up事件,然后通过VelocityTracker计算滑动速度,以此速度开启一个scroller对象的fling状态,然后post执行一个runnable对象,在run方法里不断计算scroller当前的滑动量,调用trackMotionScroll方法进行当前滚动位置后的回收、移动和fill
//1.onTouchUp
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final int initialVelocity = (int)
(velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale);
// Fling if we have enough velocity and we aren't at a boundary.
// Since we can potentially overfling more than we can overscroll, don't
// allow the weird behavior where you can scroll to a boundary then
// fling further.
boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
if (flingVelocity &&
!((mFirstPosition == 0 &&
firstChildTop == contentTop - mOverscrollDistance) ||
(mFirstPosition + childCount == mItemCount &&
lastChildBottom == contentBottom + mOverscrollDistance))) {
if (!dispatchNestedPreFling(0, -initialVelocity)) {
if (mFlingRunnable == null) {
mFlingRunnable = new FlingRunnable();
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
mFlingRunnable.start(-initialVelocity);//开始scroller的fling操作
dispatchNestedFling(0, -initialVelocity, true);
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
}...
//2.FlingRunnable的start方法
void start(int initialVelocity) {
...
mScroller.fling(0, initialY, 0, initialVelocity,
0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);//fling
mTouchMode = TOUCH_MODE_FLING;
postOnAnimation(this);//post runnable
...
}
//3.FlingRunnable的run方法
final OverScroller scroller = mScroller;
boolean more = scroller.computeScrollOffset();
final int y = scroller.getCurrY();
int delta = mLastFlingY - y;
...
final boolean atEdge = trackMotionScroll(delta, delta);//执行滚动操作
...
四.数据刷新-AdapterDataSetObserver观察者模式
adapter用于管理一个数据集合及其种种操作,并且还应该可以在数据发生改变的时候得到相应的通知,所以,Adapter使用了观察者模式,允许向其注册多个DataSetObserver对象,当adapter的数据发生改变时,通知这些观察者,使其完成自己的操作
//1.Adapter可以注册/取消观察者
void registerDataSetObserver(DataSetObserver observer);
void unregisterDataSetObserver(DataSetObserver observer);
//2.AdapterView自己有一个观察者内部类,子类注册到adapter上,以便在数据发生改变时做相应处理
class AdapterDataSetObserver extends DataSetObserver {
...
@Override
public void onChanged() {
mDataChanged = true;//数据已更新
mOldItemCount = mItemCount;//更新oldItemCount
mItemCount = getAdapter().getCount();//更新itemCount
...
requestLayout();//重新请求布局
}
...
}
//3.AbsListView在创建时会主动创建一个继承自上述观察者类的观察者并在adapter更新时注册到adapter上
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
由代码可知,ListView自己创建一个观察者,并在adapter更新的地方保证给其注册一个该观察者,这样,当我们数据更新时经常调用的notifyDataSetChanged方法被调用后,会触发相应观察者的方法,也就是onChanged方法,此时会更新状态及数据信息,然后请求重新布局绘制,最终会回到我们上述讲的layoutChildren方法开始布局
初次之外,我们还可以创建自己的观察者注册到adapter上,不用担心被覆盖,因为BaseAdapter实现Observable来注册观察者,该类维护的是一个观察者数组,需要注意的是,notify的时候会从后往前一次调用观察者的onChanged方法
五.ListView的Adapter装饰器模式
我们注意到ListView是可以添加多个Header和Footer的,ListView也会将其作为adapter的一部分(占用与普通数据一样的position),但是我们自己写的adapter并没有去处理这些特殊item,那ListView如何知道并管理的呢?
1.HeaderFooter的Adapter
ListView对其管理的adapter使用了装饰器模式,构建adapter时,当我们有header或footer时,ListView会将我们的adapter作为被装饰者,连同Header和Footer信息一起创建一个自己维护的带Header和Footer的adapter对象,作为真正管理的adapter,在管理过程中调用的adapter的任何方法都是在这个adapter上调用的,只不过该adapter的方法又都调用我们自己的adapter(被装饰者)的相应方法,所以我们可以完全通过使用自己的adapter的方法达到任何效果
public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {
private final ListAdapter mAdapter;//被装饰者
//Header和Footer信息
ArrayList<ListView.FixedViewInfo> mHeaderViewInfos;
ArrayList<ListView.FixedViewInfo> mFooterViewInfos;
...
@Override
public boolean isEmpty() {
return mAdapter == null || mAdapter.isEmpty();
}
@Override
public int getCount() {
if (mAdapter != null) {
return getFootersCount() + getHeadersCount() + mAdapter.getCount();
} else {
return getFootersCount() + getHeadersCount();
}
}
@Override
public Object getItem(int position) {
// Header (negative positions will throw an IndexOutOfBoundsException)
int numHeaders = getHeadersCount();
if (position < numHeaders) {
return mHeaderViewInfos.get(position).data;
}
// Adapter
final int adjPosition = position - numHeaders;
int adapterCount = 0;
if (mAdapter != null) {
adapterCount = mAdapter.getCount();
if (adjPosition < adapterCount) {
return mAdapter.getItem(adjPosition);
}
}
// Footer (off-limits positions will throw an IndexOutOfBoundsException)
return mFooterViewInfos.get(adjPosition - adapterCount).data;
}
@Override
public long getItemId(int position) {
int numHeaders = getHeadersCount();
if (mAdapter != null && position >= numHeaders) {
int adjPosition = position - numHeaders;
int adapterCount = mAdapter.getCount();
if (adjPosition < adapterCount) {
return mAdapter.getItemId(adjPosition);
}
}
return -1;
}
@Override
public boolean hasStableIds() {
if (mAdapter != null) {
return mAdapter.hasStableIds();
}
return false;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Header (negative positions will throw an IndexOutOfBoundsException)
int numHeaders = getHeadersCount();
if (position < numHeaders) {
return mHeaderViewInfos.get(position).view;
}
// Adapter
final int adjPosition = position - numHeaders;
int adapterCount = 0;
if (mAdapter != null) {
adapterCount = mAdapter.getCount();
if (adjPosition < adapterCount) {
return mAdapter.getView(adjPosition, convertView, parent);
}
}
// Footer (off-limits positions will throw an IndexOutOfBoundsException)
return mFooterViewInfos.get(adjPosition - adapterCount).view;
}
@Override
public int getItemViewType(int position) {
int numHeaders = getHeadersCount();
if (mAdapter != null && position >= numHeaders) {
int adjPosition = position - numHeaders;
int adapterCount = mAdapter.getCount();
if (adjPosition < adapterCount) {
return mAdapter.getItemViewType(adjPosition);
}
}
return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
}
@Override
public int getViewTypeCount() {
if (mAdapter != null) {
return mAdapter.getViewTypeCount();
}
return 1;
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
if (mAdapter != null) {
mAdapter.registerDataSetObserver(observer);
}
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(observer);
}
}
...
}
由源码可知,该adapter维护了我们自己的adapter,在重写的方法里,自己处理了关于header和footer的各种情况,并把普通item的情况交由我们的adapter来处理,所以我们的adapter只需关注自己的数据集合即可
2.Listview创建Adapter
在4.4版本之前,通过ListView的addHeaderView和addFooterView添加header和footer,然后在setAdapter的时候,会根据有没有header或footer信息决定要不使用装饰器;如果在setAdapter之后再去调用这些方法将会出现问题
从4.4版本开始,ListView解决了这个问题,在setAdapter之后添加header或footer时,增加了对adapter的再创建,如果原来没有使用装饰器,则重新构建一个装饰器作为adapter,并且还会调用上面说到的自带的观察者的onChanged方法进行刷新
//1.设置(更改)adapter
public void setAdapter(ListAdapter adapter) {
if (mAdapter != null && mDataSetObserver != null) {//取消旧adapter的观察者
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
resetList();//重置view状态
mRecycler.clear();
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {//有header或footer信息则要使用装饰器adapter
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
...
// AbsListView#setAdapter will update choice mode states.
super.setAdapter(adapter);
if (mAdapter != null) {
mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();//更新信息
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
checkFocus();
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);//新adapter注册观察者
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());//更新RecycleBin信息
...
}...
requestLayout();//刷新布局
}
//2.增加header(footer类似header)
public void addHeaderView(View v, Object data, boolean isSelectable) {
...
mHeaderViewInfos.add(info);//添加header信息
mAreAllItemsSelectable &= isSelectable;
// Wrap the adapter if it wasn't already wrapped.
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {//如果还没有使用装饰器则使用装饰器构建adapter
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
// In the case of re-adding a header view, or adding one later on,
// we need to notify the observer.
if (mDataSetObserver != null) {//通知刷新布局
mDataSetObserver.onChanged();
}
}
}
六.ListView的Item点击事件
1.onItemClickListener
ListView的item点击事件可以通过setOnItemClickListener设置,该监听器是AdapterView维护的一个监听器,可以监听到每个item的点击事件,他并不是通过给每个itemView设置OnClickListener来监听点击事件,否则每次使用view都会创建新的监听器对象
ListView(实际上是AbsListView管理)是在onTouchEvent时判断当前触摸的item的position,然后postDelay一个PerformClick内部类对象,该对象的run方法,取到相应position的childView,然后调用onItemClickListener回调
//1.onTouchDown获取触摸item的position
int motionPosition = pointToPosition(x, y);
public int pointToPosition(int x, int y) {
...
final int count = getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
child.getHitRect(frame);
if (frame.contains(x, y)) {//遍历每个view,判断是否触摸点在该view内
return mFirstPosition + i;
}
}
}
return INVALID_POSITION;
//2.onTouchUp时执行通过handler发送一个点击事件处理的runnable
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
final AbsListView.PerformClick performClick = mPerformClick;
performClick.mClickMotionPosition = motionPosition;
...
performClick.run();
//3.执行run方法
private class PerformClick extends WindowRunnnable implements Runnable {
int mClickMotionPosition;
@Override
public void run() {
// The data has changed since we posted this action in the event queue,
// bail out before bad things happen
if (mDataChanged) return;
final ListAdapter adapter = mAdapter;
final int motionPosition = mClickMotionPosition;
if (adapter != null && mItemCount > 0 &&
motionPosition != INVALID_POSITION &&
motionPosition < adapter.getCount() && sameWindow()) {
final View view = getChildAt(motionPosition - mFirstPosition);//根据itemPosition找到child的position
// If there is no view, something bad happened (the view scrolled off the
// screen, etc.) and we should cancel the click
if (view != null) {
performItemClick(view, motionPosition, adapter.getItemId(motionPosition));
}
}
}
}
//4.AdapterView处理点击事件
public boolean performItemClick(View view, int position, long id) {
final boolean result;
if (mOnItemClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnItemClickListener.onItemClick(this, view, position, id);
result = true;
}...
return result;
}
}
2.onItemLongClickListener
onItemLongClickListener是监听item的长按事件对象,与onItemClickListener类似,他也不是通过给每个child设置监听器来实现
在onTouchDown时,postDelay一个点击runnable,在其run方法里,找到对应position的child,如果可以长按(isLongClickable),则postDelay一个长按事件runnable,delay为系统默认的长按事件的响应时间,在该run方法中执行onItemLongClickListener回调
除此之外,当点击或者长按事件发生后,另一个事件(runnable)在相应的方法中就会被removeCallback调不再执行;在onTouchMove时也会取消相应的runnable
//1.触发TapRunnable
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = ev.getX();
mPendingCheckForTap.y = ev.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());//执行轻触事件
//2.TapRunnable的run方法
if (longClickable) {
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress, longPressTimeout);//提交一个长按事件runnable,delay时间为系统默认长按响应时间
} else {
mTouchMode = TOUCH_MODE_DONE_WAITING;
}
//3.长按事件响应
public void run() {
final int motionPosition = mMotionPosition;
final View child = getChildAt(motionPosition - mFirstPosition);
if (child != null) {
final int longPressPosition = mMotionPosition;
final long longPressId = mAdapter.getItemId(mMotionPosition);
boolean handled = false;
if (sameWindow() && !mDataChanged) {
handled = performLongPress(child, longPressPosition, longPressId);//处理事件
}
if (handled) {
mTouchMode = TOUCH_MODE_REST;
setPressed(false);//处理view状态
child.setPressed(false);
} else {
mTouchMode = TOUCH_MODE_DONE_WAITING;
}
}
}
boolean performLongPress(final View child,
...
boolean handled = false;
if (mOnItemLongClickListener != null) {//调用回调
handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child,
longPressPosition, longPressId);
}
...
return handled;
}
3.position和itemId的区别
在我们设置onItemClickListener时,重写的方法里的参数有position和id,我们一般习惯性的使用getItem(position)取得相应的数据,但是这在有header和footer的时候是有问题的,我们先来看下代码
public long getItemId(int position) {
int numHeaders = getHeadersCount();
if (mAdapter != null && position >= numHeaders) {
int adjPosition = position - numHeaders;
int adapterCount = mAdapter.getCount();
if (adjPosition < adapterCount) {
return mAdapter.getItemId(adjPosition);//返回用户定义的id
}
}
return -1;
}
这是HeaderViewListAdapter的getItemId方法,还记得上面说过的,在AbsListView里的PerformClick里来处理onItemClick事件,传入的position时触摸点所在的view的position,他是包含了header和footer的位置的,所以在有header和footer的情况下通过getItem(position)拿数据时就会错位,而且会有越界异常。
而getItemId()方法,会将header和footer的id返回-1,否则会返回我们自定义adapter的getItemId()方法返回的id,此时传入的参数是数据正确的position,所以只要我们在自定义的adapter中,重写getItemId()方法,并直接返回这个position,就可以保证OnItemClickListener中拿到的id,不是-1(headers)就是正确数据的position,稍加判断即可。
要想使用这个itemId,需要重写hasStableIds()方法并返回true,否则无效。
参考文章: