RecyclerView源码分析(一)--常用方法

前言

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()方法去重新绘制布局。