资讯专栏INFORMATION COLUMN

嗨!这是一篇值得深入学习的控件-RecyclerView(源码解析篇)

myeveryheart / 2279人阅读

摘要:其实通过父类的这个方法之后会调用它的方法,这个名字熟悉自定义的童鞋都知道了。

为什么要写这篇源码解析呢?

我一直在说RecyclerView是一个值得深入学习,甚至可以说是一门具有艺术性的控件。那到底哪里值得我们花时间去深入学习呢。没错了,就是源码的设计。但是看源码其实是一件不简单的事情,就拿RecyclerView的源码来说,打开源码一看,往下拉啊拉啊,我擦,怎么还没到头,汗....居然有12k+行。看到这里恐怕会吓一跳,就这么一个看似简单的控件就这么多行源码,这让我从何看起,一股畏惧感油然而生。

其实不需要害怕,我们不需要一开始就想完全弄懂它每一步怎么实现的,这样反而会造成只见森林不见树木的感觉。我们就把源码就当成一片森林来说吧。首先我们只需要先抓住一条路径去看,也就是带着一个问题去看,这样就能够把这条路径上的树都看明白了。就不会有只见森林不见树,一脸茫然了。当然我们大多数情况肯定是不满足于此一条路径,想完全看明白它是怎么实现的,那就继续另开路径(再带着另外一个问题),继续看这条路上的树。当你把每条路都走差不多了,再回头来看,就会发现你既见到了森林又见到了一颗颗清晰树木,犹如醍醐灌顶、豁然开朗。

说着很简单,但是不得不说看源码的过程还是有点小痛苦的。不过,不用慌,看完之后你所获得那种充实感和满足感会远远大于过程中的痛苦感。毕竟这是一个充满艺术感的控件嘛,值得我们去欣赏和学习。

那么开始放正片了......
一、开辟一条路径

从使用RecyclerView的时候,它的一个功能就让我感觉很这个控件不简单,不知道你和我想的是不是一样。那是什么功能呢?我们只需改变一行代码就可以直接设置它的ItemView为水平布局、垂直布局、表格布局以及瀑布流布局。这是ListView所不能做到的。用起来简单,其背后肯定有故事啊。那我们就以这条路为核心来看这片森林了。

二、开始寻路

从哪里开始看呢?
1.我们先从setAdapter()看起,这个方法我们比较熟悉,在Activity中这是我们直接接触的方法。

</>复制代码

  1. /**
  2. *Replaces the current adapter with the new one and triggers listeners.
  3. */
  4. public void setAdapter(Adapter adapter){
  5. .....
  6. //用一个新的设配器和触发器来替代目前正在使的
  7. setAdapterInternal(adapter,false,true);
  8. //请求布局,直接调用View类的请求布局方法
  9. requestLayout();
  10. }

setAdapter里面主要做了两件事:

首先调用setAdapterInternal方法,目的是用一个新的设配器和触发器来替代目前正在使用的。
我们深入进去看看它做了什么?

对于熟悉了观察者设计模式的,可以从下面的代码看出来,其实里面有个操作是:

注销观察者(之前的设配器)和注册观察者(新的设配器)操作。简单的理解一下就是设配器观察者会监测一些对象的状态,当这些对象状态改变,它可以通过这种设计模式低耦合的做出相应的改变。最后调用markKnownViewsInvalid方法刷新一下视图。

如果你想深入了解观察者设计模式的可以看一下这篇文章

传送门:观察者设计模式

</>复制代码

  1. {
  2. Adapter mAdapter;
  3. ......
  4. private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
  5. boolean removeAndRecycleViews) {
  6. if (mAdapter != null) {
  7. mAdapter.unregisterAdapterDataObserver(mObserver); //注销观察者
  8. mAdapter.onDetachedFromRecyclerView(this); //Called by RecyclerView when it stops observing this Adapter.
  9. }
  10. ......
  11. mAdapterHelper.reset();
  12. final Adapter oldAdapter = mAdapter;
  13. mAdapter = adapter;
  14. if (adapter != null) {
  15. adapter.registerAdapterDataObserver(mObserver); //注册观察者
  16. adapter.onAttachedToRecyclerView(this);
  17. }
  18. ......
  19. //刷新视图
  20. markKnownViewsInvalid();
  21. }

之后调用了 requestLayout方法请求重新布局。这个方法很关键,和我们的这次选的路是相通的。

</>复制代码

  1. @Override
  2. public void requestLayout() {
  3. if (mEatRequestLayout == 0 && !mLayoutFrozen) {
  4. super.requestLayout();
  5. } else {
  6. mLayoutRequestEaten = true;
  7. }
  8. }

这么关键的方法代码却这么少?而且好像只做了一个操作?没错,表面上只调用了父类View的requestLayout方法。其实通过父类的这个方法之后会调用它的onLayout方法,这个名字熟悉自定义View的童鞋都知道了。但我们看父类View的onLayout方法其实是个空方法。也就是说最终需要由它的子类来重写,也即RecyclerVie调用自身的onLayout方法。

</>复制代码

  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3. TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
  4. dispatchLayout();
  5. TraceCompat.endSection();
  6. mFirstLayoutComplete = true;
  7. }

onLayout又调用了dispatchLayout方法,来分发layout

</>复制代码

  1. void dispatchLayout() {
  2. ......
  3. if (mState.mLayoutStep == State.STEP_START) {
  4. //分发第一步
  5. dispatchLayoutStep1();
  6. mLayout.setExactMeasureSpecsFrom(this);
  7. //分发第二步
  8. dispatchLayoutStep2();
  9. }
  10. ......
  11. //分发第三步
  12. dispatchLayoutStep3();
  13. ......
  14. }

它把这个分发的过程分为了三步走
step1:做一下准备工作:决定哪一个动画被执行,保存一些目前view的相关信息

</>复制代码

  1. private void dispatchLayoutStep1() {
  2. ......
  3. if (mState.mRunSimpleAnimations) {
  4. // Step 0: Find out where all non-removed items are, pre-layout
  5. int count = mChildHelper.getChildCount();
  6. for (int i = 0; i < count; ++i) {
  7. final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
  8. final ItemHolderInfo animationInfo = mItemAnimator
  9. .recordPreLayoutInformation(mState, holder,
  10. ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
  11. holder.getUnmodifiedPayloads());
  12. mViewInfoStore.addToPreLayout(holder, animationInfo);
  13. }
  14. ......
  15. }

step2:找到实际的view和最终的状态后运行layout。

</>复制代码

  1. private void dispatchLayoutStep2() {
  2. eatRequestLayout();
  3. onEnterLayoutOrScroll();
  4. ......
  5. mState.mInPreLayout = false;
  6. // Step 2: 运行layout
  7. mLayout.onLayoutChildren(mRecycler, mState);
  8. mState.mStructureChanged = false;
  9. ....
  10. resumeRequestLayout(false);
  11. }

这里面有个方法很关键了,就是下面这个onLayoutChildren,这个为什么关键呢,先提一下这个,待会要详细说的。

</>复制代码

  1. mLayout.onLayoutChildren(mRecycler, mState);

step3:做一些分发的收尾工作了,保存动画和一些其他的信息。和我们不同路,就不看它了。

看了这么多先喝一杯92年的肥宅快乐水压压惊吧~~,顺便看张图小结一下上面的过程

三、寻得果树

之前说过RecyclerView和ListView最大的不同就是在它们的布局实现上。在ListView中布局是通过自身的layoutChildren方法实现的,但对于RecyclerView来说就不是了,那是谁来实现了呢?

这就要从刚才结束的onLayoutChildren方法说起了,它不是RecyclerView的类直接方法,它是RecyclerView的内部类LayoutManager的方法,顾名思义,就是布局管理者了。我们的RecyclerView布局就通过这个布局管理者来做了,把这样一个很重要的职责就交给它了。从而实现某种程度上的低耦合。

那我们继续走,它是怎么执行这一职责的。
但是点进去看onLayoutChildren方法,发现只有一行代码,而且还是打印的日志:必须重写这个方法。

</>复制代码

  1. public void onLayoutChildren(Recycler recycler, State state) {
  2. Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
  3. }

那么既然要重写必须要寻找一个子类,所以这里我就找了一个子类LinearLayoutManager类,也是我们最常用的一种线性布局来看。

</>复制代码

  1. public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
  2. ......
  3. int startOffset;
  4. int endOffset;
  5. final int firstLayoutDirection;
  6. onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
  7. ......
  8. if (mAnchorInfo.mLayoutFromEnd) {
  9. // 底部向顶部的填充
  10. updateLayoutStateToFillStart(mAnchorInfo);
  11. mLayoutState.mExtra = extraForStart;
  12. //填充
  13. fill(recycler, mLayoutState, state, false);
  14. startOffset = mLayoutState.mOffset;
  15. final int firstElement = mLayoutState.mCurrentPosition;
  16. if (mLayoutState.mAvailable > 0) {
  17. extraForEnd += mLayoutState.mAvailable;
  18. }
  19. // 顶部向底部的填充
  20. updateLayoutStateToFillEnd(mAnchorInfo);
  21. mLayoutState.mExtra = extraForEnd;
  22. mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
  23. //填充
  24. fill(recycler, mLayoutState, state, false);
  25. endOffset = mLayoutState.mOffset;
  26. ......
  27. }
  28. } else {
  29. ......
  30. }
  31. ......
  32. }

这个方法主要就是通过一个布局算法,实现itemView从顶部到底部或者底部到顶部的填充,并创建一个布局的状态。接下来看一下fill方法是怎么进行填充的。

</>复制代码

  1. int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
  2. RecyclerView.State state, boolean stopOnFocusable) {
  3. ......
  4. //1.计算RecyclerView可用的布局宽或高
  5. int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
  6. LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
  7. //2.迭代布局item View
  8. while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
  9. layoutChunkResult.resetInternal();
  10. //3.布局item view
  11. layoutChunk(recycler, state, layoutState, layoutChunkResult);
  12. //4.计算布局偏移量
  13. layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
  14. if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
  15. || !state.isPreLayout()) {
  16. layoutState.mAvailable -= layoutChunkResult.mConsumed;
  17. // we keep a separate remaining space because mAvailable is important for recycling
  18. //5.计算剩余的可用空间
  19. remainingSpace -= layoutChunkResult.mConsumed;
  20. }
  21. ......
  22. }
  23. return start - layoutState.mAvailable;
  24. }

fill方法总的来说用了5步实现了itemVIew的填充:

(1)计算RecyclerView可用的布局宽或高

(2)迭代布局item View

(3)布局itemview

(4)计算布局偏移量

(5)计算剩余的可用空间

fill方法又会循环的调用layoutChunk来进行itemView的布局,下面先看看layoutChunk的实现

</>复制代码

  1. void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
  2. LayoutState layoutState, LayoutChunkResult result) {
  3. //1.获取itemview
  4. View view = layoutState.next(recycler);
  5. ......
  6. //2.获取itemview的布局参数
  7. LayoutParams params = (LayoutParams) view.getLayoutParams();
  8. //3.测量Item View
  9. measureChildWithMargins(view, 0, 0);
  10. //4.计算该itemview消耗的宽和高
  11. result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
  12. int left, top, right, bottom;
  13. //5.按照水平或竖直方向布局来计算itemview的上下左右坐标
  14. if (mOrientation == VERTICAL) {
  15. if (isLayoutRTL()) {
  16. right = getWidth() - getPaddingRight();
  17. left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
  18. } else {
  19. left = getPaddingLeft();
  20. right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
  21. }
  22. if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
  23. bottom = layoutState.mOffset;
  24. top = layoutState.mOffset - result.mConsumed;
  25. } else {
  26. top = layoutState.mOffset;
  27. bottom = layoutState.mOffset + result.mConsumed;
  28. }
  29. } else {
  30. ......
  31. }
  32. 6.计算itemview的边界比如下划线和margin,从而确定itemview准确的位实现最终的布局
  33. layoutDecoratedWithMargins(view, left, top, right, bottom);
  34. }
  35. result.mFocusable = view.hasFocusable();
  36. }

在layoutChunk中首先从layoutState获取此时的itemview,然后根据获得的这个itemview获取它的布局参数和尺寸信息,并且判断布局方式(横向或者纵向),以此计算出itemview的上下左右坐标。最后调用layoutDecoratedWithMargins方法完成布局。

这样一看就对整个过程有了个清晰的认识了吧,有没有感觉设计的很优雅。

四、贯穿布局的一条线

到这里已经算走完我们之前准备走的一条路了。但从开始到这里始终忽略了一个东西没有说,那就在布局过程的大多方法中的参数都有一个Recycler对象。这个Recycler是什么呢?

在使用RecyclerView的过程中,我们都知道Adapter被缓存的单位不再是普通的itemview了,而是一个ViewHolder。这是和listview的一个很大的不同。

</>复制代码

  1. public final class Recycler {
  2. final ArrayList mAttachedScrap = new ArrayList<>();
  3. ArrayList mChangedScrap = null;
  4. final ArrayList mCachedViews = new ArrayList();
  5. private final List
  6. mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
  7. public View getViewForPosition(int position) {
  8. return getViewForPosition(position, false);
  9. }
  10. View getViewForPosition(int position, boolean dryRun) {
  11. return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
  12. }
  13. ViewHolder tryGetViewHolderForPositionByDeadline(int position,
  14. boolean dryRun, long deadlineNs) {
  15. ...
  16. }
  17. ......

在Recycler类的开始就看到mAttachedScrap、mChangedScrap、mCachedViews、 mUnmodifiableAttachedScrap这几个ViewHolder的列表对象,它们就是用来缓存ViewHolder的。

具体是怎么实现的这里就不做详细的解释了。因为这里一说又会牵涉到其他的点,子子孙孙无穷尽也,毕竟这是一个有艺术感的控件,不能指望一篇文章把它说透哈。

到这里我们就结束了我们对RecyclerView的的源码分析了。相信你看完会有所收获。

作者:錦小白
https://www.jianshu.com/p/102...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76915.html

相关文章

发表评论

0条评论

myeveryheart

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<