Android RecyclerView 源碼淺析

OverView

RecyclerView是Android5.0推出的新組件皂岔,可以認(rèn)為是更加靈活強(qiáng)大的ListView考抄,在日常開發(fā)中基本上已經(jīng)取代了ListView成為長(zhǎng)列表控件的首選。
其繼承結(jié)構(gòu)如下:

image.png

從繼承結(jié)構(gòu)可以看到沐绒,RecyclerView是ViewGroup的直接子類俩莽,這里就是RecyclerView與ListView第一點(diǎn)大不同,ListView和ViewGroup之間隔了兩層乔遮,其實(shí)個(gè)人認(rèn)為在繼承結(jié)構(gòu)上揭露了RecyclerView相比ListView的優(yōu)點(diǎn):由于RecyclerView這么簡(jiǎn)單的繼承結(jié)構(gòu)扮超,說明了其主要功能都是通過組合來實(shí)現(xiàn)的,而ListView是通過繼承實(shí)現(xiàn)的蹋肮。

用法

通常來說出刷,在業(yè)務(wù)代碼中使用RecyclerView,主要有以下幾個(gè)步驟:

  • 創(chuàng)建RecyclerView實(shí)例
  • 為RecyclerView設(shè)置LayoutManager坯辩,LayoutManager是一個(gè)抽象類馁龟,常用的實(shí)現(xiàn)類有LinearLayoutManager
  • 如有必要,為RecyclerView設(shè)置ItemAnimator
  • 如有必要漆魔,為RecyclerView設(shè)置ItemDecoration
  • 繼承RecyclerView.Adapter<VH extends ViewHolder>坷檩,實(shí)現(xiàn)getItemCount()/onCreateViewHolder(ViewGroup parent, int viewType)/onBindViewHolder(VH holder, int position)
  • 在RecyclerView.Adapter中使用到的VH是ViewHolder的實(shí)現(xiàn)類,但ViewHolder雖然是一個(gè)抽象類改抡,其中卻沒有需要我們實(shí)現(xiàn)的抽象方法矢炼,ViewHolder中關(guān)鍵的成員是itemView,即其承載的視圖阿纤,可見性為public句灌,可以直接獲取

源碼淺析

這部分跟隨RecyclerView使用到的各個(gè)類的方法,看下這些關(guān)鍵方法是在何時(shí)被調(diào)用的

Adapter

  • public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    在源碼中查找onCreateViewHolder(ViewGroup parent, int viewType)的調(diào)用阵赠,能得到下圖


    image.png

    內(nèi)一層是外一層的調(diào)用涯塔,最終可以看到主要的調(diào)用在LinearLayoutManager的onLayoutChildren()肌稻;直接的調(diào)用方法是在RecyclerView的tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs)中有這么1段:

                if (holder == null) {
                    long start = getNanoTime();
                    if (deadlineNs != FOREVER_NS
                            && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                        // abort - we have a deadline we can't meet
                        return null;
                    }
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    if (ALLOW_THREAD_GAP_WORK) {
                        // only bother finding nested RV if prefetching
                        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                        if (innerView != null) {
                            holder.mNestedRecyclerView = new WeakReference<>(innerView);
                        }
                    }

                    long end = getNanoTime();
                    mRecyclerPool.factorInCreateTime(type, end - start);
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
                    }
                }

簡(jiǎn)單來說,就是在layout()過程匕荸,如果holder為null爹谭,就調(diào)用這個(gè)方法來創(chuàng)建一個(gè)ViewHolder

  • public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
    首先在RecyclerView.Adapter的bindViewHolder(VH holder, int position)中有直接調(diào)用:
        public final void bindViewHolder(VH holder, int position) {
            holder.mPosition = position;
            if (hasStableIds()) {
                holder.mItemId = getItemId(position);
            }
            holder.setFlags(ViewHolder.FLAG_BOUND,
                    ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
                            | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
            TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
            onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
            holder.clearPayload();
            final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
            if (layoutParams instanceof RecyclerView.LayoutParams) {
                ((LayoutParams) layoutParams).mInsetsDirty = true;
            }
            TraceCompat.endSection();
        }

調(diào)用完之后從其itemView中取出LayoutParams來進(jìn)行處理
然后再源碼中查找最終的調(diào)用鏈,結(jié)果如下:


image.png

調(diào)用鏈中關(guān)鍵的一環(huán)是在RecyclerView的View getViewForPosition(int position, boolean dryRun)中

        View getViewForPosition(int position, boolean dryRun) {
            return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
        }

里面先調(diào)用了tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS)方法榛搔,在前面分析onCreateViewHolder()的時(shí)候已經(jīng)看到了诺凡,這個(gè)方法里嘗試獲取holder,取不到就創(chuàng)建holder践惑,創(chuàng)建后就進(jìn)行onBindViewHolder()的操作腹泌,bind完之后在這里可以看到,返回的是holder.itemView尔觉,因此在布局過程中凉袱,就是從這里獲得了正確的itemView

  • int getItemCount()
    這個(gè)方法的調(diào)用,在源碼里看都是直接調(diào)用


    image.png
  • int getItemViewType(int position)
    這個(gè)方法在源碼中的被調(diào)用位置主要是RecyclerView的tryGetViewHolderForPositionByDeadline()函數(shù)侦铜,這個(gè)函數(shù)在上面也已經(jīng)出現(xiàn)過了

                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }

通過這個(gè)方法拿到的type专甩,就是在onCreateViewHolder()/onBindViewHolder()函數(shù)中的入?yún)ype

LayoutManager

  • public abstract LayoutParams generateDefaultLayoutParams();
    查找源碼庐橙,這個(gè)方法被調(diào)用的位置為RecyclerView的同名函數(shù)中:
    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        if (mLayout == null) {
            throw new IllegalStateException("RecyclerView has no LayoutManager");
        }
        return mLayout.generateDefaultLayoutParams();
    }

  • public void onLayoutChildren(Recycler recycler, State state)
    這個(gè)方法本身并不是abstract函數(shù)乎赴,但在LayoutManager中必須重寫:
public void onLayoutChildren(Recycler recycler, State state) {
            Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}

在源碼中查找其調(diào)用努溃,可以看到被調(diào)用的地方主要有兩處:
首先是RecyclerView的dispatchLayoutStep1()函數(shù)

    private void dispatchLayoutStep1() {
        mState.assertLayoutStep(State.STEP_START);
        mState.mIsMeasuring = false;
        eatRequestLayout();
        mViewInfoStore.clear();
        onEnterLayoutOrScroll();
        processAdapterUpdatesAndSetAnimationFlags();
        saveFocusInfo();
        mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
        mItemsAddedOrRemoved = mItemsChanged = false;
        mState.mInPreLayout = mState.mRunPredictiveAnimations;
        mState.mItemCount = mAdapter.getItemCount();
        findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);

        if (mState.mRunSimpleAnimations) {
            // Step 0: Find out where all non-removed items are, pre-layout
            int count = mChildHelper.getChildCount();
            for (int i = 0; i < count; ++i) {
                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
                    continue;
                }
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
                mViewInfoStore.addToPreLayout(holder, animationInfo);
                if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                        && !holder.shouldIgnore() && !holder.isInvalid()) {
                    long key = getChangedHolderKey(holder);
                    // This is NOT the only place where a ViewHolder is added to old change holders
                    // list. There is another case where:
                    //    * A VH is currently hidden but not deleted
                    //    * The hidden item is changed in the adapter
                    //    * Layout manager decides to layout the item in the pre-Layout pass (step1)
                    // When this case is detected, RV will un-hide that view and add to the old
                    // change holders list.
                    mViewInfoStore.addToOldChangeHolders(key, holder);
                }
            }
        }
        if (mState.mRunPredictiveAnimations) {
            // Step 1: run prelayout: This will use the old positions of items. The layout manager
            // is expected to layout everything, even removed items (though not to add removed
            // items back to the container). This gives the pre-layout position of APPEARING views
            // which come into existence as part of the real layout.

            // Save old positions so that LayoutManager can run its mapping logic.
            saveOldPositions();
            final boolean didStructureChange = mState.mStructureChanged;
            mState.mStructureChanged = false;
            // temporarily disable flag because we are asking for previous layout
            mLayout.onLayoutChildren(mRecycler, mState);
            mState.mStructureChanged = didStructureChange;

            for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
                final View child = mChildHelper.getChildAt(i);
                final ViewHolder viewHolder = getChildViewHolderInt(child);
                if (viewHolder.shouldIgnore()) {
                    continue;
                }
                if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                    int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
                    boolean wasHidden = viewHolder
                            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    if (!wasHidden) {
                        flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                    }
                    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                            mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                    if (wasHidden) {
                        recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                    } else {
                        mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                    }
                }
            }
            // we don't process disappearing list because they may re-appear in post layout pass.
            clearOldPositions();
        } else {
            clearOldPositions();
        }
        onExitLayoutOrScroll();
        resumeRequestLayout(false);
        mState.mLayoutStep = State.STEP_LAYOUT;
    }

其次是dispatchLayoutStep2()中:

    private void dispatchLayoutStep2() {
        eatRequestLayout();
        onEnterLayoutOrScroll();
        mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mItemCount = mAdapter.getItemCount();
        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        // onLayoutChildren may have caused client code to disable item animations; re-check
        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        resumeRequestLayout(false);
    }

ItemDecoration

  • onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)
    這個(gè)函數(shù)作用在于繪制分割線桐玻,在源碼中被調(diào)用的地方在RecyclerView中:
    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

其實(shí)就是RecyclerView的onDraw()函數(shù)中進(jìn)行了分割線的繪制

  • public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
    這個(gè)函數(shù)是删窒,在每一項(xiàng)itemView繪制的時(shí)候宏多,通過控制outRect的left/top/right/bottom值萧求,來確定itemView四周留出來的范圍腕巡,效果類似于padding或margin
    在源碼中被調(diào)用的地方有:


    image.png

可以看到俊卤,主要是和measure有關(guān)的函數(shù)嫩挤,很容易理解,因?yàn)榫褪窃趍easure過程確定一個(gè)itemView占據(jù)多少空間

ItemAnimator

ItemAnimator主要用于對(duì)item的插入瘾蛋、修改俐镐、刪除動(dòng)畫處理主要有animateApprearance/animateDisappearance/animateChange/animatePersistence這幾個(gè)在對(duì)應(yīng)動(dòng)作的函數(shù)

  • public abstract boolean animateAppearance(@NonNull ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
    在源碼中查找animateAppearance的調(diào)用鏈為:


    image.png

    最終又是在熟悉的dispatchLayoutStep3()中被調(diào)用

            // Step 4: Process view info lists and trigger animations
            mViewInfoStore.process(mViewInfoProcessCallback);

但這一行并不是很能看出怎么就調(diào)用了animateAppearance,看下再上一步的調(diào)用:


image.png

可以看到在調(diào)用process()函數(shù)的時(shí)候哺哼,是有根據(jù)record.flags來判斷應(yīng)該執(zhí)行的是哪個(gè)處理

  • public abstract boolean animateDisappearance(@NonNull ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo)
    在源碼中查找其調(diào)用位置:


    image.png

    可以看到跟animateAppearance一樣佩抹,最終都是被dispatchLayoutStep3()調(diào)用

  • public abstract boolean animateChange(@NonNull ViewHolder oldHolder,
    @NonNull ViewHolder newHolder,
    @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo)
    這個(gè)函數(shù)的調(diào)用鏈和上述一致,就不重復(fù)貼圖了

  • public abstract boolean animatePersistence(@NonNull ViewHolder viewHolder,
    @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo);
    這個(gè)函數(shù)的調(diào)用鏈依然和上述一致

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末取董,一起剝皮案震驚了整個(gè)濱河市棍苹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌茵汰,老刑警劉巖枢里,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡栏豺,警方通過查閱死者的電腦和手機(jī)彬碱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奥洼,“玉大人巷疼,你說我怎么就攤上這事×榻保” “怎么了嚼沿?”我有些...
    開封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)瓷患。 經(jīng)常有香客問我骡尽,道長(zhǎng),這世上最難降的妖魔是什么擅编? 我笑而不...
    開封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任攀细,我火速辦了婚禮,結(jié)果婚禮上沙咏,老公的妹妹穿的比我還像新娘辨图。我一直安慰自己,他們只是感情好肢藐,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吱韭,像睡著了一般吆豹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上理盆,一...
    開封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天痘煤,我揣著相機(jī)與錄音,去河邊找鬼猿规。 笑死衷快,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的姨俩。 我是一名探鬼主播蘸拔,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼环葵!你這毒婦竟也來了调窍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤张遭,失蹤者是張志新(化名)和其女友劉穎邓萨,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缔恳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年宝剖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歉甚。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡万细,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铃芦,到底是詐尸還是另有隱情雅镊,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布刃滓,位于F島的核電站仁烹,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏咧虎。R本人自食惡果不足惜卓缰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砰诵。 院中可真熱鬧征唬,春花似錦、人聲如沸茁彭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)理肺。三九已至摄闸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妹萨,已是汗流浹背年枕。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留乎完,地道東北人熏兄。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像树姨,于是被迫代替她去往敵國(guó)和親摩桶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350