ListView是Android中常用的控件之一,也是信息流中主体展示的重要控件,它在复用性、流畅性及易用性上均有着较好的表现。Google在V7支持包中推出了新控件RecyclerView,它封装了处理convertView缓存的部分,开发者只需要实现View的创建和更新两个部分就可实现列表,另外它还可以用LayoutManager实现不同的布局,比如List和Grid。最近Facebook的首页信息流也使用了这一控件,但从拉View树的结果看,貌似是和ListView切换使用的。
下面就在源码层面,从初始化、缓存机制和绘制机制上分三部分对比下两种View,欢迎大家指正。
(ListView的源码版本采用的是Android 4.4,为目前最主流的版本;RecyclerView则是用的最新v7包里的)
(一)初始化篇
1. AbsListView(源码版本4.4)
ListView的父类是AbsListView,ListView中的View缓存和Adapter等主要机制均继承自AbsListView。
1.1 初始化
这一部分主要在构造函数中实现,主要分三部分:
(1) AbsListView的初始化initAbsListView(),主要是对View的Clickable、Focusable、Cache等熟悉进行初始化,同时拿到ViewConfiguration初始化TouchSlop(触摸溢出)、Velocity(滑动速度)等变量。
(2) ScrollBar的初始化,在Context中拿到View的Style,调initializeScrollbars(a)进行初始化,这是View里的函数。
(3) AbsListView风格设置,从Context拿到AbsListView的TypedArray,对其专属的风格进行设置,包括scrollingCache、transcriptMode、fastScroll、choiceMode等。
1.2 Adapter
ListView的适配器,基类Adapter是个接口,需要开发者继承实现自己的Adapter,主要有以下几个函数需要实现:
(1) getView(int position, View convertView, ViewGroup parent):这是处理缓存View和更新View的主要函数,position是当前需要处理的item项的位置,convertView是外面传进来的缓存View,parent是父View
(2) hasStableIds():可自己实现,如果item id永远是与对象的一一对应的,就返回true,这个函数在寻找缓存View时会用到,如果是true,就可以在Adapter里找缓存View
(3) getItem、getCount、getItemId等get函数
(4) getItemViewType(int position):获取相应position上的ViewType,这个ViewType会在getView的时候确定,两个可以互相转换的View归作一类,这种就可以重用
(5) registerDataSetObserver/unregister,设置一个观察者,当Adapter里的Data发生变化,就执行相应操作。
2 ListView(源码版本4.4)
ListView的初始化在父类的基础上增加了Divider(分割线)、OverScrollHeader和OverScrollFooter的设置。
3 RecyclerView(源码版本5.1.1)
LayoutParams
RecyclerView自定义的LayoutParams,里面可以存储mViewHolder,表示该View所属的ViewHolder,通过getChildViewHolder(view),可以拿到view的ViewHolder,很常用
3.1初始化
RecyclerView会做以下几个初始化动作:
(1)mChildHelper的初始化,首先介绍下这个ChildHelper类,它会协助获取RecyclerView中的childVIew,并提供忽略隐藏Children的功能,也就是说,调它的getChildAt只会在当前显示的Children中去查找,如果想查HiddenChildren,需要调getUnfilteredChildAt。
(2)mItemAnimator的初始化,处理各Item的动画
(3)mAdapterHelper的初始化,其中封装了处理Adapter更新的操作。
3.2几个重要的类
3.2.1.ViewHolder
ViewHolder是RecyclerView进行缓存和处理的基本单位,每一种类型的item需要对应一种ViewHolder。它与ListView中的ViewHolder又有所不同,其中只存储着唯一一个itemView及其相关参数,这个itemView类似于ListView的convertView。有趣的是为了能查到itemView所属的ViewHolder,这里自定义了一个LayoutParams,其所属的ViewHolder作为一个参数存储在里面,然后set到itemView里。
(1)变量:
itemView:ViewHolder中存储的View,为其提供缓存和更新服务
mPosition:itemView当前在RecyclerView中的位置
mOldPosition:itemView在RecyclerView中的旧位置
mItemId:itemView对应的Id,当Adapter的StableID为true时,id与position相等
mItemViewType:itemView对应的ViewType
mScrapContainer:ViewHolder自带的Recycler,存储可重用的ViewHolder,其实存储的就是其所属ViewHolder(this)。
函数方法方面大多是get方法,就不细说了
3.2.2.Adapter<VH>
RecyclerView自定义的Adapter,VH表示其对应的ViewHolder类型,可以写个BaseViewHolder作为泛型入参,把多个ViewHolder集合到一个Adapter中。
(1)Adapter的变量只有两个:
AdapterDataObservable mObservable:用于监控Adapter的数据变化,有变化就进行相应操作。
Boolean mHasStableIds:沿用了原版Adapter的StableId机制,也就是如果item id永远是与对象的一一对应的,就设为true,此时id等同于position。
(2)Adapter是一个虚拟类,需要其子类实现几个核心函数:
public abstract VH onCreateViewHolder(ViewGroupparent, int viewType)
这个方法用于创建ViewHolder,根据viewType创建View,作为入参创建相应的ViewHolder,这里的viewType是通过内部调用getItemViewType(int position)获得的,它默认是返回0,所以在处理多种类型的显示时,需要重写此函数返回相应类型。
public abstract void onBindViewHolder(VH holder, intposition)
当RecyclerView需要展示数据时,就会调用这个函数,其主要功能是对holder中的itemView的数据进行更新。与ListView不同的是,当item的data发生变化时,不会再次调用这个函数,除非item进行invalidate或者无法确定新位置,所以只需要根据position拿到数据使用即可,不用在里面存一份。
public abstract int getItemCount()
返回Item的总数,这个函数的实现与ListView的类似。
3.2.3.LayoutManager
LayoutManager负责对RecyclerView中的itemView进行measure和定位,同时还决定什么时候回收用户不可见的itemView。目前官方已支持Linear和Grid的LayoutManger。
(1)变量:
ChildHelper mChildHelper:存储itemView的容器。
RecyclerView mRecyclerVIew:它所管理的RecyclerView,使用时需set进来。
SmoothScroller mSmoothScroller:用于追踪目标View的位置和触发计划好的滑动。(可为空)
Boolean mRequestedSimpleAnimations:是否有简单动画的标志位
(2)关键函数:
public void onLayoutChildren(Recyclerrecycler, State state)
这个和AbsListView的layoutChildren类似,必须由子类进行具体实现,但这个函数并没有被设成abstract,而是在里面写了个Log注明“You must override onLayoutChildren(Recycler recycler, State state)”。
private void addViewInt(View child, int index, boolean disappearing)
LayoutManager可以自行往RecyclerView里添加View,流程如下:
a. 首先根据child拿到其所属的ViewHolder。
b. 根据第三个入参diappearing判断,选择是否加入或移出RecyclerView的DisappearingList。
c. 检查ViewHolder的isScrap和ReturndFromScrap标志位,也就是检查ViewHolder自带的mScrapContainer,不为空就代表ViewHolder属于缓存的View。如果为true则清理其Scrap状态。然后调mChildHelper.attachViewtoParent将此View与父View(RecyclerView)关联。
d. 如果不是缓存View,就看(child.getParent()== mRecyclerView),如果是那说明也是合法的View,并且已存在mChildHelper中。然后分别获取其在mChildHelper中的index和mChildHelper的count,如果index!=count,就把这个child移到count位置,也就是队列最后。
e. 如果以上条件都不符合,说明是个全新的View,那就直接调mChildHelper.addView加入。
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
if (disappearing || holder.isRemoved()) {
//layout后这些View将会被隐藏
mRecyclerView.addToDisappearingList(child);
} else {
mRecyclerView.removeFromDisappearingList(child);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 这个Holder是Scrap中的缓存Holder
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
if (holder.isScrap()) {
holder.unScrap();
} else {
holder.clearReturnedFromScrapFlag();
}
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchFinishTemporaryDetach(child);
}
} else if (child.getParent() == mRecyclerView) {
// 可能已经存储在ChildHelper中,修正位置
int currentIndex = mChildHelper.indexOfChild(child);
if (index == -1) {
index = mChildHelper.getChildCount();
}
if (currentIndex == -1) {
throw new IllegalStateException("Added View has RecyclerView as parent but"
+ " view is not a real child. Unfiltered index:"
+ mRecyclerView.indexOfChild(child));
}
if (currentIndex != index) {
mRecyclerView.mLayout.moveView(currentIndex, index);
}
} else {
//全新的View,直接add
mChildHelper.addView(child, index, false);
lp.mInsetsDirty = true;
if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
mSmoothScroller.onChildAttachedToWindow(child);
}
}
if (lp.mPendingInvalidate) {
if (DEBUG) {
Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder);
}
holder.itemView.invalidate();
lp.mPendingInvalidate = false;
}
}
public voidmeasureChild(View child, int widthUsed, int heightUsed)
前面说过,LayoutManager也负责对childView进行measure,这里就看看它是怎么Measure的:
a. 首先拿到两个东西:child的LayoutParams--lp,调getItemDecorInsetsForChild(child)拿到itemDecoration的Rect—insets。
b. widthUsed和heightUsed分别加上insets的左右和上下间隔。
c. 用上面的计算结果作为入参,调getChildMeasureSpec拿到widthSpec和heightSpec,执行child.measure(widthSpec, heightSpec)。
public void measureChild(View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(),
getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
child.measure(widthSpec, heightSpec);
}