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,否则无效。

参考文章:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值