谈谈RecyclerView的LayoutManager-LinearLayoutManager源码分析

泡在网上的日子 / 文 发表于2016-09-22 15:15 次阅读

今天我们来好好谈谈LayoutManager的问题。

前言

LayoutManager是RecyclerView用来管理子view布局的一个组件(另一个组件应该是Recycler,负责回收视图),它主要负责三个事情:

  1. 布局子视图

  2. 在滚动过程中根据子视图在布局中所处的位置,决定何时添加子视图和回收视图。

  3. 滚动子视图

其中,只有滚动子视图,才会需要对子视图回收或者添加,而添加子视图则必然伴随着对所添加对象的布局处理。在滚动过程中,添加一次子视图只会影响到被添加对象,原有子视图的相对位置不会变化。 

LayoutManager是RecyclerView的一个抽象内部类,一般我们使用它都是使用它的子类,常用的有LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager,它们都是sdk自带的,实现了几种常用的布局。这里就不介绍它们的用法了。

你也可以自定义一个LayoutManager,但是在你自定义之前,你必须分析现有的LayoutManager。这篇文章就是从分析LinearLayoutManager入手,来深入的理解LayoutManager这个东西。相信在看了LinearLayoutManager的源码之后,你对布局管理器会有深入的认识。

准备工作

我首先把SDK中LinearLayoutManager的源码copy了一份出来,重新命名为TestLayoutManager,然后解决了里面的错误(因为这个时候已经不在原来的包里了,一些类会找不到),这样我就能随意的在里面打log,修改代码看效果。我喜欢在读代码的同时去改代码:假如这里去掉,或者增加一些代码会发生什么情况。如果你需要,可以直接在这里下载我独立出来的这个TestLayoutManager http://jcodecraeer.oss-cn-shanghai.aliyuncs.com/cod/TestLayoutManager.java  。

一些基本的知识

不管是LinearLayoutManager,还是其它自定义的LayoutManager,这些方法基本都是逃不掉的:

onLayoutChildren()

onLayoutChildren()是 LayoutManager 的主入口。 它会在初始化布局时调用, 当适配器的数据改变时(或者整个适配器被换掉时)会再次调用。它的作用就是在初始化的时候放置item,直到填满布局为止。

canScrollHorizontally() & canScrollVertically()

这些方法很简单,在你想要滚动方向对应的方法里返回 true , 不想要滚动方向对应的方法里返回 false。

scrollHorizontallyBy() & scrollVerticallyBy()

在这里实现滚动的逻辑。RecyclerView 已经处理了触摸事件的那些事情,当你上下左右滑动的时候scrollHorizontallyBy() & scrollVerticallyBy()会传入此时的位移偏移量dy(或者dx), 根据这个dy你需要完成下面这三个任务:

  1. 将所有的子视图移动适当的位置 (对的,你得自己做这个)。

  2. 决定移动视图后 添加/移除 视图。

  3. 返回滚动的实际距离。框架会根据它判断你是否触碰到边界。

开始

LinearLayoutManager一共有2000多行代码,并不多,而且LinearLayoutManager需要处理纵向,横向,动画等问题,但是我们只关心它是如何做到管理布局的,其实关键的代码并不多,不会超过1000行。

似乎我们该从onLayoutChildren方法开始对吧,因为它是入口嘛。本来期望里面是类似于添加view,为view设置位置的代码,应该很简单。但是看了onLayoutChildren方法的代码之后,一下子就受到10000点伤害,居然有近200行代码,而且完全不知所云。

在碰壁之后,我觉得从RecyclerView的滚动过程开始分析。LinearLayoutManager支持横向和纵向滚动因此,它的canScrollHorizontally() 和 canScrollVertically()方法是这样实现的:

@Override
public boolean canScrollHorizontally() {
    return mOrientation == HORIZONTAL;
}

/**
 * @return true if {@link #getOrientation()} is {@link #VERTICAL}
 */
@Override
public boolean canScrollVertically() {
    return mOrientation == VERTICAL;
}

再来看看scrollHorizontallyBy() 和 scrollVerticallyBy()

/**
 * {@inheritDoc}
 */
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
                                RecyclerView.State state) {
    if (mOrientation == VERTICAL) {
        return 0;
    }
    return scrollBy(dx, recycler, state);
}

/**
 * {@inheritDoc}
 */
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
                              RecyclerView.State state) {
    if (mOrientation == HORIZONTAL) {
        return 0;
    }
    return scrollBy(dy, recycler, state);
}

可以看到,这两个方法都把滚动的处理交给了scrollBy方法,这个方法很短

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() == 0 || dy == 0) {
        return 0;
    }
    mLayoutState.mRecycle = true;
    ensureLayoutState();
    final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDy = Math.abs(dy);
    updateLayoutState(layoutDirection, absDy, true, state);
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    if (consumed < 0) {
        if (DEBUG) {
            Log.d(TAG, "Don't have any more elements to scroll");
        }
        return 0;
    }
    final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
    mOrientationHelper.offsetChildren(-scrolled);
    if (DEBUG) {
        Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
    }
    mLayoutState.mLastScrollDelta = scrolled;
    return scrolled;
}

当dy=0或者没有子元素的时候,什么也不做直接返回0。这个很好理解吧。

然后根据dy判断滚动方向。如果是垂直布局的LinearLayoutManager的话,LayoutState.LAYOUT_END表示向下翻滚(手指向上划),反之LayoutState.LAYOUT_START表示向上翻滚。

然后取dy的绝对值,并保存在absDy变量中。LinearLayoutManager在处理滚动的时候,都是用正整数来计算的,而不是用带有正负号的向量来计算。对于数学不好的人来说使用带正负号的数字太抽象了。

在滚动的时候保存状态

接下来调用了updatelayoutState方法,这个方法主要是完成一些状态的更新,在后面添加和回收视图的时候会把这些状态作为判断的条件。updatelayoutState方法代码不多,如下:

private void updateLayoutState(int layoutDirection, int requiredSpace,
                               boolean canUseExistingSpace, RecyclerView.State state) {
    mLayoutState.mInfinite = mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED;
    mLayoutState.mExtra = getExtraLayoutSpace(state);
    mLayoutState.mLayoutDirection = layoutDirection;
    int scrollingOffset;
    if (layoutDirection == LayoutState.LAYOUT_END) {
        mLayoutState.mExtra += mOrientationHelper.getEndPadding();
        // get the first child in the direction we are going
        final View child = getChildClosestToEnd();
        // the direction in which we are traversing children
        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                : LayoutState.ITEM_DIRECTION_TAIL;
        mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
        mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
        // calculate how much we can scroll without adding new children (independent of layout)
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                - mOrientationHelper.getEndAfterPadding();

    } else {
        final View child = getChildClosestToStart();
        mLayoutState.mExtra += mOrientationHelper.getStartAfterPadding();
        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                : LayoutState.ITEM_DIRECTION_HEAD;
        mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
        mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
        scrollingOffset = -mOrientationHelper.getDecoratedStart(child)
                + mOrientationHelper.getStartAfterPadding();
    }
    mLayoutState.mAvailable = requiredSpace;
    if (canUseExistingSpace) {
        mLayoutState.mAvailable -= scrollingOffset;
    }

    mLayoutState.mScrollingOffset = scrollingOffset;
}

这些状态保存在mLayoutState变量中,下面是mLayoutState的各项数据代表的意思:

  • mLayoutState.mLayoutDirection 滑动方向

  • mLayoutState.mCurrentPosition 当前应该从adapter中获取item的position,用于在添加视图的时候,根据这个position从recycler中获取相应的View。

  • mLayoutState.mOffset 用于在添加布局的时候,根据它来确定被添加子View的布局位置。

  • mLayoutState.mAvailable 此次滚动发生后,

  • mLayoutState.mScrollingOffset 在添加一个新view之前,还可以滑动多少空间。

其中mLayoutState.mScrollingOffset有点诡异。在下面的fill方法中又对它重新赋值:layoutState.mScrollingOffset += layoutState.mAvailable;

导致实际上它等于dy。


保存完状态之后,就进入fill方法。

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
             RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
                //Log.d(TAG, "layoutState.mScrollingOffset =" + layoutState.mScrollingOffset);
            }
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            /**
             * Consume the available space if:
             * * layoutChunk did not request to be ignored
             * * OR we are laying out scrap children
             * * OR we are not doing pre-layout
             */
            if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        if (DEBUG) {
            validateChildOrder();
        }
        return start - layoutState.mAvailable;
    }

fill做了两件事情:先回收移除不再显示的子View,然后添加即将进入可见区域的子View。

回收过程

在fill方法中首先重新设置了layoutState.mScrollingOffset的值,然后根据上面updatelayoutState方法所得到的状态对item进行回收,回收是通过recycleByLayoutState()方法实现的。recycleByLayoutState()方法的大致流程很简单,当一个item滚出了布局边界立即回收。其实这里不仅仅是做了回收,还把滚出页面的视图从RecyclerView移除。

下面让我们来一步步分析recycleByLayoutState()的过程。

    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
        } else {
            recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
        }
    }

第一个if语句暂时不管它。

在第二个if语句中,根据当前的滚动方向调用了不同的方法。如果是上拉(即LayoutState.LAYOUT_START)则调用recycleViewsFromEnd方法,从名字可以看出回收是从末尾开始的,这个很好理解,上拉的时候是查看前面的内容,底部的item 不断滚出界面,当然是回收末尾的view了;而如果是下拉(LayoutState.LAYOUT_END)则调用recycleViewsFromStart方法。

顺藤摸瓜,进入recycleViewsFromStart(),一会儿再来看recycleViewsFromEnd(),其实原理都一样:

private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
    if (dt < 0) {
        if (DEBUG) {
            Log.d(TAG, "Called recycle from start with a negative value. This might happen"
                    + " during layout changes but may be sign of a bug");
        }
        return;
    }
    // ignore padding, ViewGroup may not clip children.
    final int limit = dt;
    final int childCount = getChildCount();
    if (mShouldReverseLayout) {
        for (int i = childCount - 1; i >= 0; i--) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here
                recycleChildren(recycler, childCount - 1, i);
                return;
            }
        }
    } else {
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    }
}

这里的第二个参数dt即某时刻滚动距离的绝对值。如果你仔细看了前面的代码就知道它来自于layoutState.mScrollingOffset。虽然layoutState.mScrollingOffset本身代表的不是这个意思,但是代码里确实让它在此时等效于dy了。这也是我说layoutState.mScrollingOffset比较诡异的原因。

然后把这个dt赋值给了limit,在这个方法里也许是多此一举,不过它主要是为了和recycleViewsFromEnd方法相统一。

接着判断是否为mShouldReverseLayout,一般情况下都不是了,正常情况下是进入第二个条件。

在第二个条件里是个for循环。这个for循环比较难懂。

粗略的看就是遍历当前布局(RecyclerView)的子View,符合一定条件的子View就回收。

执行回收的具体方法是recycleChildren()。

但是符合什么条件才回收呢,还有就是 recycleChildren(recycler, 0, i)这个方法为什么有三个参数呢?

回收一个view只需i这个参数就行了吧?

mOrientationHelper.getDecoratedEnd(child)获得的是一个子view的底部边界的位置,我是根据log和方法名推测出来的,没有去深究它的实现。而> limit意思就是如果一旦一个子view的底部位置大于即将发生的位移(dt

),说明这个view在位移发生后,它仍然是可见的,那么就开始回收它之前的View,循环也到此结束(return)。for循环的作用就是跳过不可见的View。recycleChildren(recycler, 0, i)不是回收一个view,而是一堆view。但是实际上recycleChildren一次也只能回收了一个view。如果你从i=0开始分析(或者在recycleChildren中打log分析)就知道,不会存在累积很多个view才一起回收的情况,一有机会就回收了。

我通过在recycleChildren中打log得出,在下拉时候,被回收的始终是child 0(第一个子View)。因为一旦回收了一个view,它随即也被从布局中移除,第二个立即变成了第一个。

说完了recycleViewsFromStart()方法,还得简要的说下recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt)方法啰,它和recycleViewsFromStart()相反是处理上拉的时候的回收,即LayoutState.LAYOUT_START。


上拉的时候,我们是要看上面的item,底部的item逐渐消失,顶部item不断出现,这时该回收的是底部的item。这个方法的代码跟recycleViewsFromStart是完全一样的步骤,只是limit的计算变了,这里的limit = mOrientationHelper.getEnd() - dt;遍历的顺序也变了不是从0开始,而是从最后一个child开始,寻找第一个可见的child,一旦发现某个child的上边界在位移发生后还能小于limit,那么它就是可见的,它就是倒数第一个可见item,它之后的都是不可见的,调用recycleChildren(recycler, childCount - 1, i)把它们删除回收。

所以你知道为什么recycleChildren方法需要三个参数了吧,因为它是删除一个区间,当然需要起始和末尾的索引啦,在加上参数recycler(用于回收)就是三个参数了。

跟recycleViewsFromStart()方法一样,虽然这里回收一个区间,但是不会存在累积很多个view才一起回收的情况,被回收的始终是 childCount - 1。一旦有一个item就立马被回收了,然后被回收的item被它前面(或者后面一个)item替代。

为了验证我的想法(不会存在累积很多个view才一起回收的情况),我在里面打了两个log,看看是否一次其实只回收了一个。

没有判断if (DEBUG) 的那两个log才是我打的哈:


下拉,一直都在回收第一个:


上拉,一直都在回收第七个(即最后一个,具体最后一个是多少跟手机屏幕,布局大小有关):


回收过程就这样结束了,接下来是视图的添加,注意回收和添加并没有什么因果关系,它们发生在两头,以下拉查看后面的内容为例,回收发生在顶部,而添加则发生在尾部。

添加视图的过程

让我们再回到fill方法:


添加View的条件

当一个子view完全显示出来,意味着下一个子view就要进来了,这个时候你就需要向布局中添加view。接下来的while循环就是添加View的过程。在添加之前,需要判断什么时候可以添加View。

它有三个条件(有&&的也有||的),但是起决定因素的是remainingSpace > 0,因为layoutState.hasMore(state)和layoutState.mInfinite两个条件可以快速判断出来在多数情况下是一定的(看它们的源码就知道了)。所以我们这里就要去弄明白remainingSpace > 0到底是什么意思。

remainingSpace = layoutState.mAvailable + layoutState.mExtra;

其中 layoutState.mAvailable来自于上面提到的updatelayoutState方法, layoutState.mExtra可以暂时不用管,前面提到了 layoutState.mAvailable表示dy与最后一个View完全可见的所剩空间的差。但是并没有说明它怎么来的。我们还是再一次看看updateLayoutState方法吧:


我们找到和layoutState.mAvailable相关的代码:

mLayoutState.mAvailable = requiredSpace;
if (canUseExistingSpace) {
    mLayoutState.mAvailable -= scrollingOffset;
}

其中requiredSpace就是某一刻滚动的距离即scrollby中的dy。

canUseExistingSpace在滚动的时候始终为真的,所以

mLayoutState.mAvailable -= scrollingOffset。那么现在的问题就是要搞清楚scrollingOffset是什么东西了。

(1)当layoutDirection == LayoutState.LAYOUT_END
scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
-mOrientationHelper.getEndAfterPadding();

其中mOrientationHelper.getDecoratedEnd(child)表示获得child的下边界,而child=getChildClosestToEnd(),即最后一个可见元素。

那么mOrientationHelper.getDecoratedEnd(child)的意思就是获得最后一个可见子view的下边界。

mOrientationHelper.getEndAfterPadding()表示获得布局去除padding过后的底部边界。

所以scrollingOffset就等于:最后一个可见视图的底部边界 - 布局去除padding过后的底部边界

它代表什么意思呢?

它代表最后一个可见View在当前滚动方向上还能滚动多远就完全可见了。

而mLayoutState.mAvailable -= scrollingOffset就是用dy和它比较,当dy大于它,说明滚动发生后最后一个可见元素已经完全可见且离开了,该添加新的布局了。

因为remainingSpace = layoutState.mAvailable + layoutState.mExtra;所以remainingSpace和mLayoutState.mAvailable的意思是一样的。因此remainingSpace > 0可以作为判断是否添加View的条件。

(2)当layoutDirection == LayoutState.LAYOUT_START的时候

这个时候我们则是计算布局顶部边界与第一个可见View的顶部边界,进而计算第一个可见子view什么时候完全可见。

上面就是scrollingOffset的意思了:总结起来就是,在添加一个新的View之前,还能滚动多少距离,它包括了两个方向的情况。

而mLayoutState.mAvailable -= scrollingOffset则是用dy和scrollingOffset比较,如果dy大于scrollingOffset的话,那么说明滚动发生后临近边界的view已经完全消耗完了,需要添加新的view了。

现在回到remainingSpace,remainingSpace = layoutState.mAvailable + layoutState.mExtra;

layoutState.mExtra可以忽略它。那么remainingSpace就相当layoutState.mAvailable,remainingSpace>0就表示移动发生后,需要添加一个新的View了。

问题来了,为什么要用while循环而不是if语句呢?

这是因为fill方法不只在滚动的时候被调用,初次布局的时候也被调用了,用while循环是为了初始化的时候不断的填满布局。

添加View

添加一个View是调用layoutChunk方法来完成的,让我们来看看这个layoutChunk方法:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                 LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    if (view == null) {
        if (DEBUG && layoutState.mScrapList == null) {
            throw new RuntimeException("received null view when unexpected");
        }
        // if we are laying out views in scrap, this may return null which means there is
        // no more items to layout.
        result.mFinished = true;
        return;
    }
    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addView(view);
        } else {
            addView(view, 0);
        }
    } else {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addDisappearingView(view);
        } else {
            addDisappearingView(view, 0);
        }
    }
    measureChildWithMargins(view, 0, 0);
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    int left, top, right, bottom;
    if (mOrientation == VERTICAL) {
        if (isLayoutRTL()) {
            right = getWidth() - getPaddingRight();
            left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
        } else {
            left = getPaddingLeft();
            right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            bottom = layoutState.mOffset;
            top = layoutState.mOffset - result.mConsumed;
        } else {
            top = layoutState.mOffset;
            bottom = layoutState.mOffset + result.mConsumed;
        }
    } else {
        top = getPaddingTop();
        bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            right = layoutState.mOffset;
            left = layoutState.mOffset - result.mConsumed;
        } else {
            left = layoutState.mOffset;
            right = layoutState.mOffset + result.mConsumed;
        }
    }
    // We calculate everything with View's bounding box (which includes decor and margins)
    // To calculate correct layout position, we subtract margins.
    layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
            right - params.rightMargin, bottom - params.bottomMargin);
    if (DEBUG) {
        Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
    }
    // Consume the available space if the view is not removed OR changed
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }
    result.mFocusable = view.isFocusable();
}

layoutChunk方法做了三件事:

  • 一是根据当前的position获得一个View。

  • 二是调用addView方法,addView顾名思义就是添加view了,不过它是LayoutManager的方法,最终还是要调用ViewGroup的addView方法;

  • 三是对刚刚添加的View进行布局。把它放置在恰当的位置。因为RecyclerView的item还包含了itemdecoration,LayoutManager提供了layoutDecorated方法来简化布局的过程。

根据当前的position获取View的代码是

View view = layoutState.next(recycler);

它调用了layoutState的next方法来获取当前position的View:

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

if里面语句的意思是如果mScrapList不为空,则直接从mScrapList中获取。暂时没有搞明白这个到底在什么情况下有用,因为我在这个if中写log从来没有被调用过。

所以一般View还是从recycler的getViewForPosition(mCurrentPosition)中获取的。mCurrentPosition在前面已经解释了,它是在updatelayoutState方法中得到的。

接下来用 mCurrentPosition += mItemDirection;更新mCurrentPosition的值,不过在滚动的时候这个貌似没有用呢。应该是用于初始化布局的时候吧。

待续。。。

收藏 赞 (12) 踩 (1)
上一篇:自定义一些常见的Dialog效果,居中显示、顶部显示、仿IOS版淘宝、回弹效果、宽度和高度占屏比等,先看效果。
自定义一些常见的Dialog效果,居中显示、顶部显示、仿IOS版淘宝、回弹效果、宽度和高度占屏比等,先看效果。 博客地址 http://www.jianshu.com/p/d893ba8608ae 源码 https://github.com/Alex-Cin/Dialog demo https://github.com/Alex-Cin/DialogApp
下一篇:详解7.0带来的新工具类:DiffUtil
一 概述 DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集-》新数据集的最小变化量。 说到数据集,相信大家知道它是和谁相关的了,就是我的最爱,RecyclerView。 就我使用的这几天来看,它最大的用处就是在RecyclerView刷新