滑动及加载数据举例源码分析。

1,005 阅读8分钟
原文链接: www.jianshu.com

承接上篇listview 源码解析

以下用第一次向下滑动举例。

由于ListView和GridView都继承自AbsListView当中的了。那么监听触控事件是在onTouchEvent()方法当中进行的,我们就来看一下AbsListView中的这个方法:

   case MotionEvent.ACTION_MOVE: {
            onTouchMove(ev, vtev);
            break;
        }

跟进onTouchMove

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
          ...省略非重点代码

    if (mDataChanged) {
        // Re-sync everything if data has been changed
        // since the scroll operation can query the adapter.
        layoutChildren();
    }

    final int y = (int) ev.getY(pointerIndex);

    switch (mTouchMode) {

  ...省略非重点代码
        case TOUCH_MODE_OVERSCROLL:
            scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
            break;
    }
}

在此我们发现 if (mDataChanged)就layoutChildren();可以说数据发生改变时就layoutChildren。跟进scrollIfNeeded方法;

 private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
  ...省略非重点代码。。。。。。省略的有点太多了哈哈。

           else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
        if (y != mLastY) {
            final int oldScroll = mScrollY;
            final int newScroll = oldScroll - incrementalDeltaY;
            int newDirection = y > mLastY ? 1 : -1;

            if (mDirection == 0) {
                mDirection = newDirection;
            }

            int overScrollDistance = -incrementalDeltaY;
            if ((newScroll < 0 && oldScroll >= 0) || (newScroll > 0 && oldScroll <= 0)) {
                overScrollDistance = -oldScroll;
                incrementalDeltaY += overScrollDistance;
            } else {
                incrementalDeltaY = 0;
            }

            if (overScrollDistance != 0) {
                overScrollBy(0, overScrollDistance, 0, mScrollY, 0, 0,
                        0, mOverscrollDistance, true);
                final int overscrollMode = getOverScrollMode();
                if (overscrollMode == OVER_SCROLL_ALWAYS ||
                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&
                                !contentFits())) {
                    if (rawDeltaY > 0) {
                        mEdgeGlowTop.onPull((float) overScrollDistance / getHeight(),
                                (float) x / getWidth());
                        if (!mEdgeGlowBottom.isFinished()) {
                            mEdgeGlowBottom.onRelease();
                        }
                        invalidateTopGlow();
                    } else if (rawDeltaY < 0) {
                        mEdgeGlowBottom.onPull((float) overScrollDistance / getHeight(),
                                1.f - (float) x / getWidth());
                        if (!mEdgeGlowTop.isFinished()) {
                            mEdgeGlowTop.onRelease();
                        }
                        invalidateBottomGlow();
                    }
                }
            }

            if (incrementalDeltaY != 0) {
                // Coming back to 'real' list scrolling
                if (mScrollY != 0) {
                    mScrollY = 0;
                    invalidateParentIfNeeded();
                }

                trackMotionScroll(incrementalDeltaY, incrementalDeltaY);

                mTouchMode = TOUCH_MODE_SCROLL;

                // We did not scroll the full amount. Treat this essentially like the
                // start of a new touch scroll
                final int motionPosition = findClosestMotionRow(y);

                mMotionCorrection = 0;
                View motionView = getChildAt(motionPosition - mFirstPosition);
                mMotionViewOriginalTop = motionView != null ? motionView.getTop() : 0;
                mMotionY =  y + scrollOffsetCorrection;
                mMotionPosition = motionPosition;
            }
            mLastY = y + lastYCorrection + scrollOffsetCorrection;
            mDirection = newDirection;
        }
    }
        }

mTouchMode在上一个方法中就已经是TOUCH_MODE_OVERSCROLL所以查看其代码块。
其中一旦上下卫衣 if (incrementalDeltaY != 0) 成立
跟进trackMotionScroll方法。此方法即为判断轨迹运动的具体代码。

  boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
   ...省略非重点代码 就是给一个向上滑还是向下滑的判断
    if (down) {
        int top = -incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            top += listPadding.top;
        }
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getBottom() >= top) {
                break;
            } else {
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    } else {
        int bottom = getHeight() - incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            bottom -= listPadding.bottom;
        }
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= bottom) {
                break;
            } else {
                start = i;
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    }

    mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

    mBlockLayoutRequests = true;

    if (count > 0) {
        detachViewsFromParent(start, count);
        mRecycler.removeSkippedScrap();
    }

    // invalidate before moving the children to avoid unnecessary invalidate
    // calls to bubble up from the children all the way to the top
    if (!awakenScrollBars()) {
       invalidate();
    }

    offsetChildrenTopAndBottom(incrementalDeltaY);

    if (down) {
        mFirstPosition += count;
    }

    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }

    mRecycler.fullyDetachScrapViews();
    if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
        final int childIndex = mSelectedPosition - mFirstPosition;
        if (childIndex >= 0 && childIndex < getChildCount()) {
            positionSelector(mSelectedPosition, getChildAt(childIndex));
        }
    } else if (mSelectorPosition != INVALID_POSITION) {
        final int childIndex = mSelectorPosition - mFirstPosition;
        if (childIndex >= 0 && childIndex < getChildCount()) {
            positionSelector(INVALID_POSITION, getChildAt(childIndex));
        }
    } else {
        mSelectorRect.setEmpty();
    }

    mBlockLayoutRequests = false;

    invokeOnItemScrollListener();

    return false;
}

重点来了,在判断有一个item移除后if(down)的判断中mRecycler.addScrapView(child, position);
此时的scrapView即为被回收的item也就是移除屏幕的item。但偏移量不够的时候不addScrapView
之后if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}

再之后 offsetChildrenTopAndBottom(incrementalDeltaY);
该方法存在于ViewGroup,跟进。

 public void offsetChildrenTopAndBottom(int offset) {
    final int count = mChildrenCount;
    final View[] children = mChildren;
    boolean invalidate = false;

    for (int i = 0; i < count; i++) {
        final View v = children[i];
        v.mTop += offset;
        v.mBottom += offset;
        if (v.mRenderNode != null) {
            invalidate = true;
            v.mRenderNode.offsetTopAndBottom(offset);
        }
    }

    if (invalidate) {
        invalidateViewProperty(false, false);
    }
    notifySubtreeAccessibilityStateChangedIfNeeded();
}

没别的意思,整体偏移所有的item。

之后回到trackMotionScroll我们发现if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}的判断满足之时就是下一个item添加之日。跟进。

  @Override
void fillGap(boolean down) {
    final int count = getChildCount();
    if (down) {
        int paddingTop = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            paddingTop = getListPaddingTop();
        }
        final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
                paddingTop;
        fillDown(mFirstPosition + count, startOffset);
        correctTooHigh(getChildCount());
    } else {
        int paddingBottom = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            paddingBottom = getListPaddingBottom();
        }
        final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
                getHeight() - paddingBottom;
        fillUp(mFirstPosition - 1, startOffset);
        correctTooLow(getChildCount());
    }
}

熟悉的老朋友fillDown以及fillUp又回来了代码不再重复。重点再次回到makeAndAddView中

 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View child;


    if (!mDataChanged) {
        // Try to use an existing view for this position
        child = mRecycler.getActiveView(position);
        if (child != null) {
            // Found it -- we're using an existing child
            // This just needs to be positioned
            setupChild(child, position, y, flow, childrenLeft, selected, true);

            return child;
        }
    }

    // Make a new view for this position, or convert an unused view if possible
    child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
}

此时的ActiveView的返回值是null,因为activeview的值仅仅是第一次layout的显示出来的那几个值。判断中child= null故而再次进入obtainView方法。跟进。

 View obtainView(int position, boolean[] isScrap) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

    isScrap[0] = false;

    // Check whether we have a transient state view. Attempt to re-bind the
    // data and discard the view if we fail.
    final View transientView = mRecycler.getTransientStateView(position);
    if (transientView != null) {
        final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

        // If the view type hasn't changed, attempt to re-bind the data.
        if (params.viewType == mAdapter.getItemViewType(position)) {
            final View updatedView = mAdapter.getView(position, transientView, this);

            // If we failed to re-bind the data, scrap the obtained view.
            if (updatedView != transientView) {
                setItemViewLayoutParams(updatedView, position);
                mRecycler.addScrapView(updatedView, position);
            }
        }

        isScrap[0] = true;

        // Finish the temporary detach started in addScrapView().
        transientView.dispatchFinishTemporaryDetach();
        return transientView;
    }

    final View scrapView = mRecycler.getScrapView(position);
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
            // Failed to re-bind the data, return scrap to the heap.
            mRecycler.addScrapView(scrapView, position);
        } else {
            if (child.isTemporarilyDetached()) {
                isScrap[0] = true;

                // Finish the temporary detach started in addScrapView().
                child.dispatchFinishTemporaryDetach();
            } else {
                // we set isScrap to "true" only if the view is temporarily detached.
                // if the view is fully detached, it is as good as a view created by the
                // adapter
                isScrap[0] = false;
            }

        }
    }

    if (mCacheColorHint != 0) {
        child.setDrawingCacheBackgroundColor(mCacheColorHint);
    }

    if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
        child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    }

    setItemViewLayoutParams(child, position);

    if (AccessibilityManager.getInstance(mContext).isEnabled()) {
        if (mAccessibilityDelegate == null) {
            mAccessibilityDelegate = new ListItemAccessibilityDelegate();
        }
        if (child.getAccessibilityDelegate() == null) {
            child.setAccessibilityDelegate(mAccessibilityDelegate);
        }
    }

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);

    return child;
}

神奇的东西来了scrapView!=null所以 此时view的来源来自于mRecycler.addScrapView(scrapView, position);从ScrapVie中复用一个view。

刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。

我们可以暂时回顾小结一下,实际上RecycleBin的主要结构就是三个,一个是activeView堆,结构是一个View数组,另一个是scrapView堆,结构是一个
ArrayList数组。还有一个是transientViews,结构是SparseArray,主要通过Id或者position存取。
transientViews的获取会首先通过id获取之后再通过position,这个是撸代码的常识了吧,不解释。

几个结构可以理解为层级不同,activeView比scrapView高一点,当触发了某种瞬态或其他条件机制后,child的view就会从activieView中移到transientViews或者scrapView中进行缓存。

当ListView需要obtainView时,会先从有瞬时态的sparseArray中获取view,当失败时就会去scrapViews中获取view。
当然,这些过程又是和一个boolean变量mDataChanged进行配合的,具体的过程在上面的源码分析中已经解释过了,诸位可以回过去看看。
基本思路是在给子view布局时,如果数据没有发生改变,就使用当前已经存在ActiveViews的view。

至此结束

添加几个被老大提问的疑难杂症看法

1.如果mDataChanged=true怎么办。

mDataChanged=true 肯定是调用了adapter的notifyDataSetChanged,跟进。

 public void notifyDataSetChanged() {
    mDataSetObservable.notifyChanged();
}

这里我们可以看到这里使用了Observable的观察者模式,继续跟入。

public void notifyChanged() {
    synchronized(mObservers) {
        // since onChanged() is implemented by the app, it could do anything, including
        // removing itself from {@link mObservers} - and that could cause problems if
        // an iterator is used on the ArrayList {@link mObservers}.
        // to avoid such problems, just march thru the list in the reverse order.
        for (int i = mObservers.size() - 1; i >= 0; i--) {
            mObservers.get(i).onChanged();
        }
    }
}

我们发现是其每一个注册过的观察者Observers调用相对应的onChanged方法。接下来让我们在找Observers在哪里。此时我们想起了listview的setAdapter方法
跟入去瞧瞧。

  @Override
public void setAdapter(ListAdapter adapter) {
    if (mAdapter != null && mDataSetObserver != null) {
        mAdapter.unregisterDataSetObserver(mDataSetObserver);
    }

    resetList();
    mRecycler.clear();

    if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
        mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
    } else {
        mAdapter = adapter;
    }

    mOldSelectedPosition = INVALID_POSITION;
    mOldSelectedRowId = INVALID_ROW_ID;

    // AbsListView#setAdapter will update choice mode states.
    super.setAdapter(adapter);

    if (mAdapter != null) {
        mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
        mOldItemCount = mItemCount;
        mItemCount = mAdapter.getCount();
        checkFocus();

        mDataSetObserver = new AdapterDataSetObserver();
        mAdapter.registerDataSetObserver(mDataSetObserver);

        mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

        int position;
        if (mStackFromBottom) {
            position = lookForSelectablePosition(mItemCount - 1, false);
        } else {
            position = lookForSelectablePosition(0, true);
        }
        setSelectedPositionInt(position);
        setNextSelectedPositionInt(position);

        if (mItemCount == 0) {
            // Nothing selected
            checkSelectionChanged();
        }
    } else {
        mAreAllItemsSelectable = true;
        checkFocus();
        // Nothing selected
        checkSelectionChanged();
    }

    requestLayout();
}

lucky 发现一个mDataSetObserver并且mAdapter.registerDataSetObserver(mDataSetObserver);这是将mDataSetObserver的这个观察者注册到了mAdapter中,而mAdapter来自于ListAdapter,而我们常见的BaseAdapter恰恰继承自ListAdapter。观察者模式至此串联成功。但我们继续查找mDataSetObserver的来源AdapterDataSetObserver,在AbsListView中我们发现了

    class AdapterDataSetObserver extends AdapterView.AdapterDataSetObserver {
    @Override
    public void onChanged() {
        super.onChanged();
        if (mFastScroll != null) {
            mFastScroll.onSectionsChanged();
        }
    }

    @Override
    public void onInvalidated() {
        super.onInvalidated();
        if (mFastScroll != null) {
            mFastScroll.onSectionsChanged();
        }
    }
}

然并卵。。查看其AdapterDataSetObserver的父类

class AdapterDataSetObserver extends DataSetObserver {

    private Parcelable mInstanceState = null;

    @Override
    public void onChanged() {
        mDataChanged = true;
        mOldItemCount = mItemCount;
        mItemCount = getAdapter().getCount();

        // Detect the case where a cursor that was previously invalidated has
        // been repopulated with new data.
        if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                && mOldItemCount == 0 && mItemCount > 0) {
            AdapterView.this.onRestoreInstanceState(mInstanceState);
            mInstanceState = null;
        } else {
            rememberSyncState();
        }
        checkFocus();
        requestLayout();
    }

    @Override
    public void onInvalidated() {
        mDataChanged = true;

        if (AdapterView.this.getAdapter().hasStableIds()) {
            // Remember the current state for the case where our hosting activity is being
            // stopped and later restarted
            mInstanceState = AdapterView.this.onSaveInstanceState();
        }

        // Data is invalid so we should reset our state
        mOldItemCount = mItemCount;
        mItemCount = 0;
        mSelectedPosition = INVALID_POSITION;
        mSelectedRowId = INVALID_ROW_ID;
        mNextSelectedPosition = INVALID_POSITION;
        mNextSelectedRowId = INVALID_ROW_ID;
        mNeedSync = false;

        checkFocus();
        requestLayout();
    }

    public void clearSavedState() {
        mInstanceState = null;
    }
}

可以看到,Observable是adapter,其数据改变调用notifyDataSetChanged最终通知到了Observers的onChanged方法onChanged中调用了requestLayout
重新布局,自然也调用了ListView的layoutChildren
现在回到listvuew的layoutChildren中

if (dataChanged) {
    handleDataChanged();}

调用AbsListView的handleDataChanged

  @Override
protected void handleDataChanged() {
    int count = mItemCount;
    int lastHandledItemCount = mLastHandledItemCount;
    mLastHandledItemCount = mItemCount;

    if (mChoiceMode != CHOICE_MODE_NONE && mAdapter != null && mAdapter.hasStableIds()) {
        confirmCheckedPositionsById();
    }

    // TODO: In the future we can recycle these views based on stable ID instead.
    mRecycler.clearTransientStateViews();

    if (count > 0) {
        int newPos;
        int selectablePos;

        // Find the row we are supposed to sync to
        if (mNeedSync) {
            // Update this first, since setNextSelectedPositionInt inspects it
            mNeedSync = false;
            mPendingSync = null;

            if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL) {
                mLayoutMode = LAYOUT_FORCE_BOTTOM;
                return;
            } else if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) {
                if (mForceTranscriptScroll) {
                    mForceTranscriptScroll = false;
                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
                    return;
                }
                final int childCount = getChildCount();
                final int listBottom = getHeight() - getPaddingBottom();
                final View lastChild = getChildAt(childCount - 1);
                final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom;
                if (mFirstPosition + childCount >= lastHandledItemCount &&
                        lastBottom <= listBottom) {
                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
                    return;
                }
                // Something new came in and we didn't scroll; give the user a clue that
                // there's something new.
                awakenScrollBars();
            }

            switch (mSyncMode) {
            case SYNC_SELECTED_POSITION:
                if (isInTouchMode()) {
                    // We saved our state when not in touch mode. (We know this because
                    // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to
                    // restore in touch mode. Just leave mSyncPosition as it is (possibly
                    // adjusting if the available range changed) and return.
                    mLayoutMode = LAYOUT_SYNC;
                    mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);

                    return;
                } else {
                    // See if we can find a position in the new data with the same
                    // id as the old selection. This will change mSyncPosition.
                    newPos = findSyncPosition();
                    if (newPos >= 0) {
                        // Found it. Now verify that new selection is still selectable
                        selectablePos = lookForSelectablePosition(newPos, true);
                        if (selectablePos == newPos) {
                            // Same row id is selected
                            mSyncPosition = newPos;

                            if (mSyncHeight == getHeight()) {
                                // If we are at the same height as when we saved state, try
                                // to restore the scroll position too.
                                mLayoutMode = LAYOUT_SYNC;
                            } else {
                                // We are not the same height as when the selection was saved, so
                                // don't try to restore the exact position
                                mLayoutMode = LAYOUT_SET_SELECTION;
                            }

                            // Restore selection
                            setNextSelectedPositionInt(newPos);
                            return;
                        }
                    }
                }
                break;
            case SYNC_FIRST_POSITION:
                // Leave mSyncPosition as it is -- just pin to available range
                mLayoutMode = LAYOUT_SYNC;
                mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);

                return;
            }
        }

  ...省略非重要代码
}

此时mLayoutMode = LAYOUT_SYNC;再次回到listview的layoutChildren中

        // Clear out old views
        detachAllViewsFromParent();
        recycleBin.removeSkippedScrap();
         switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            if (newSel != null) {
                sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
            } else {
                sel = fillFromMiddle(childrenTop, childrenBottom);
            }
            break;
        case LAYOUT_SYNC:
            sel = fillSpecific(mSyncPosition, mSpecificTop);
            break;

即便是我的child被detach掉我也可以通过fillSpecific的方式继续在上一次显示的view中继续的显示上一次的布局,此处就是数据更改后的listView刷新重新布局。

希望大家多提点意见,下一篇准备分析下recyclerview的源码。喜欢请关注。