前言
RecyclerView是我们最常用的一个控件之一了,相对于ListView及GridView,这个控件确实强大很多,例如,RecyclerView可以由我们自定义布局(LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager,即线性布局、网格布局和瀑布流布局)、自定义item之间的分割线等,这些都是ListView所不具备的。
在这里我不在提及RecyclerView的具体用法,因为大家已经对这个控件的基本用法很熟悉了,我们从常用方法的源码去了解这个控件。
RecyclerView用法
RecyclerView的最基本常用方法就是以下这些就足够了:
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
recyclerView.setLayoutManager(new LinearLayoutManaget(this));
recyclerView.setAdapter(new MyRecyclerViewAdapter(this, datas));
recyclerView.addItemDecoration(new MyItemDecoration(30));
以下我们就通过源码去看一下这些方法是如何去工作的。
源码分析
RecyclerView概述
首先打开源码,看一下RecyclerView的介绍(虽然已经老掉牙了,还是提及一下吧)
/**
* A flexible view for providing a limited window into a large data set.
*
*/
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
...
}
注释部分只截取了一部分,这些就够了,两个关键的词:
flexible
a large data
首先,灵活的,RecyclerView这个控件相对于ListView的介绍(A view that shows items in a vertically scrolling list.)更强调的是他的灵活,这也与上文中提到的一样,我们可以自定义很多内容。其次,大量的数据,这个就好理解了,在有限的窗口中去展示更多的数据。
RecyclerView也是继承子ViewGroup,并实现ScrollingView,NestedScrollingChild,所以他也具备自定义View的特征,这里暂时先不去分析他的绘制流程,先从用法上去通过源码分析。
构造方法
根据我个人的习惯,查看源码习惯性的先去看他的构造方法,RecyclerView提供了三个构造方法:
public RecyclerView(Context context) {
this(context, null);
}
public RecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
...
}
具体我们看第三个构造方法,贴出的源代码中我选取了一部分分析,其他的暂时先不用去管,有兴趣的可以自己更加深入的去了解。
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if (attrs != null) {
...
} else {
...
}
...
setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);
mItemAnimator.setListener(mItemAnimatorListener);
initAdapterManager();
initChildrenHelper();
}
其实这样一提取,也没多少东西了,先将就这样看吧,一切从简,满足用法即可,里面的代码是一些基本的设置,一些flag的赋值等。这里先看一下setWillNotDraw这个方法,这个方法是View类的一个方法,我们点进去看一下这个方法的源码:
/*
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on View,
* but could be set on some View subclasses such as ViewGroup.
*
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
首先,在RecyclerView的构造方法中,传入的参数getOverScrollMode() == View.OVER_SCROLL_NEVER,这里有必要提一下,getOverScrollMode()也是View的一个方法,他返回的是这个View的滑动模式,在View中就已经定义了View的滑动模型,这里有三个值:
public static final int OVER_SCROLL_ALWAYS = 0;
public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1;
public static final int OVER_SCROLL_NEVER = 2;
View.OVER_SCROLL_ALWAYS
只要该View是可以滚动的View,始终允许用户去滚动此View(View默认设置为此值)。
View.OVER_SCROLL_IF_CONTENT_SCROLLS
该View是可以滚动的View,但是只有在View中的内容足够多以至于能够形成有意义的滑动,才可以滚动此View,这里与默认的有个区别,对内容的大小有了判断,就算是能够滑动的视图,只要内容能够在View中显示完整,也不会滚动。
View.OVER_SCROLL_NEVER
始终不允许View滚动,这个比较好理解,View不具有滚动的属性。
setWillNotDraw(boolean willNotDraw)这个方法也就是标记一下这个View是否需要自己重新绘制,默认情况下不设置。
接下来是设置item动画监听等,这些先不介绍,看一下就好。
RecyclerView滑动状态
RecyclerView的滑动有三种状态,分别为一下三种,可以通过getScrollState()方法去获取当前滑动状态。
public static final int SCROLL_STATE_IDLE = 0;
public static final int SCROLL_STATE_DRAGGING = 1;
public static final int SCROLL_STATE_SETTLING = 2;
SCROLL_STATE_IDLE
RecyclerView当前不滑动
SCROLL_STATE_DRAGGING
在外部触摸事件的影响下滑动
SCROLL_STATE_SETTLING
RecyclerView滑动到最终位置,在没有外界操作的情况下,这个比较抽象一点,这样解释,我们手指不离开屏幕滑动,属于第二种情况,手指滑动一下,然后手指离开屏幕,RecyclerView还在继续滑动,就是这种情况。
RecyclerView常用方法源码
setLayoutManager()方法
RecyclerView可以由我们自己去定义布局,我们可以使用系统已经定义好的LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager,也可以基于这些布局去自己定义布局,下面我们贴出该方法的源码:
public void setLayoutManager(LayoutManager layout) {
if (layout == mLayout) {
return;
}
stopScroll();
// TODO We should do this switch a dispatchLayout pass and animate children. There is a good
// chance that LayoutManagers will re-use views.
if (mLayout != null) {
// end all running animations
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler);
mRecycler.clear();
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
mLayout.setRecyclerView(null);
mLayout = null;
} else {
mRecycler.clear();
}
// this is just a defensive measure for faulty item animators.
mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout +
" is already attached to a RecyclerView: " + layout.mRecyclerView);
}
mLayout.setRecyclerView(this);
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
requestLayout();
}
这里可以看到在为RecyclerView设置布局时,需要调用stopScroll()方法去将当前的滚动模式设置为不滚动,停止item动画等。然后去移除并回收可复用view,然后在经过一系列判断,最终去绘制RecyclerView布局,具体的绘制我们放到后面专门去通过源码去解析,现在只了解一下即可。
public void stopScroll() {
setScrollState(SCROLL_STATE_IDLE);
stopScrollersInternal();
}
void setScrollState(int state) {
if (state == mScrollState) {
return;
}
if (DEBUG) {
Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
new Exception());
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
stopScrollersInternal();
}
dispatchOnScrollStateChanged(state);
}
private void stopScrollersInternal() {
mViewFlinger.stop();
if (mLayout != null) {
mLayout.stopSmoothScroller();
}
}
上面的代码也很好理解,设置RecyclerView的当前滑动状态为暂停,为了绘制布局,对于Fling也是RecyclerView的一种滑动模式,我们后面再滑动中去详细解释。
setAdapter()方法
setAdapter()方法应该算是对于RecyclerView大家最熟悉的一个方法了,也是用的最多的一个方法,为RecyclerView设置适配器,将RecyclerView与数据进行绑定,并且在Adapter中为RecyclerView设置item布局,下面贴出该方法的源码:
public void setAdapter(Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false, true);
requestLayout();
}
对,你没有看错,有没有一些意外,确实就这么简单的几句,本以为会有长篇大论,逐步点开这些方法去追踪一下:
/**
* Enable or disable layout and scroll.
*
* @param frozen true to freeze layout and scroll, false to re-enable.
*/
public void setLayoutFrozen(boolean frozen) {
if (frozen != mLayoutFrozen) {
assertNotInLayoutOrScroll("Do not setLayoutFrozen in layout or scroll");
if (!frozen) {
mLayoutFrozen = false;
if (mLayoutRequestEaten && mLayout != null && mAdapter != null) {
requestLayout();
}
mLayoutRequestEaten = false;
} else {
final long now = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
onTouchEvent(cancelEvent);
mLayoutFrozen = true;
mIgnoreMotionEventTillDown = true;
stopScroll();
}
}
}
这个方法我们看一下注释,启用或禁用布局和滑动。参数为true时,冻结布局和滚动,为false时,重新启用。当setLayoutFrozen(true)被调用后,布局请求将被推迟,直到setLayoutFrozen(false)被调用。
当RecyclerView被冻结时,子View不会更新,smoothScrollBy(int, int)、scrollBy(int, int)、scrollToPosition(int)和smoothScrollToPosition(int)等方法将停止。触摸事件等也会停止。LayoutManager的onFocusSearchFailed(View, int, Recycler, State)也不会被调用。
setLayoutFrozen(true)并不能防止应用程序直接调用LayoutManager的scrollToPosition(int)和smoothScrollToPosition(RecyclerView, State, int)。
调用setAdapter(Adapter)和swapAdapter(Adapter, boolean)方法会自动解冻。
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
...
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
markKnownViewsInvalid();
}
这个方法用来将参数提供的adapter替换之前的旧adapter并触发监听事件。当我们首次调用setAdapter方法时,该RecyclerView中的adapter为null,所以源码中我省略了旧adapter不为空的部分,只看新设置的adapter。
这里可以看到,当设置的adapter不为空时,将该adapter设置为RecyclerView的adapter并为adapter注册数据变化监听事件,以及将adapter与RecyclerView绑定。最后将item的回收器也重新设置。
该方法的第二个参数,如果为true,新的adapter使用相同的布局和item类型(帮助我们避免缓存失效)。第三个参数,如果为true,移除并清除所有的视图,如果为false,该参数被忽略。这部分涉及到视图回收机制,往后解释。
这里还值得提及以下这个方法:
public void swapAdapter(Adapter adapter, boolean removeAndRecycleExistingViews) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, true, removeAndRecycleExistingViews);
setDataSetChangedAfterLayout();
requestLayout();
}
该方法与setAdapter()方法相似,用户互换新的adapter与现有的adapter。新的adapter和现有的adapter使用相同的ViewHolder并且没有清除RecycledViewPool。
addItemDecoration()方法
public void addItemDecoration(ItemDecoration decor) {
addItemDecoration(decor, -1);
}
public void addItemDecoration(ItemDecoration decor, int index) {
if (mLayout != null) {
mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or" + " layout");
}
if (mItemDecorations.isEmpty()) {
setWillNotDraw(false);
}
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
markItemDecorInsetsDirty();
requestLayout();
}
addItemDecoration(ItemDecoration decor)方法最终会调用addItemDecoration(ItemDecoration decor, int index)方法,并且第二个参数默认为 -1,我们看第二个方法。两个参数,第一个参数很明确,ItemDecoration对象,具体这个类我们放到后面再详细看源码,这个类我们可以去自定义实现,能够实现我们任何自定义的item间隔效果;第二个参数是在RecyclerView中插入这个装饰的位置,当参数为负值时,默认添加在最后。
方法内部将ItemDecoration添加到一个集合中,根据方法的介绍,该装饰内容有序,意为后面的会覆盖前面添加的效果。
void markItemDecorInsetsDirty() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final View child = mChildHelper.getUnfilteredChildAt(i);
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
mRecycler.markItemDecorInsetsDirty();
}
在该方法中,设置RecyclerView的Item的LayoutParams的mInsertDirty为true。这样,在measure时,才能够把所有的ItemDecoration的ItemOffset添加到Item布局。最终调用requestLayout()方法去重新绘制布局。