源碼探索系列10---替代Listview的RecycleView

自從有了Recycleview,很多原本是我們的Listview業務都被替代了,關于兩者的簡單比較,可以看這篇文章。我們今天就去看看他背后故事,下次再寫Listview,這名征戰多年的老將。

一些不要搞懂的問題

  1. 為何谷歌推薦用這個,背后的效率是高在哪里?
  2. LayoutManager是怎么去弄不同布局的

起航

API:23 ,這RecyclerView有一萬多行,看起來真的亞歷山大啊。

我們常用的方式就是下面這樣:

mRecycleView.setAdapter(mAdapter);

扔給他一個適配器,所以這個就當作我們的起航的第一個突破口吧,看下他背后都做了些什么事。

public void setAdapter(Adapter adapter) {
    // bail out if layout is frozen
    setLayoutFrozen(false);
    setAdapterInternal(adapter, false, true);
    requestLayout();
}

他先去調用setLayoutFrozen()去停止移動,再更新適配器,最后調用requestLayout()去更新界面。這里補充說下,這個RecyclerView是直接繼承ViewGroup的。

public void setLayoutFrozen(boolean frozen) {
    if (frozen != mLayoutFrozen) { 
      ...
      final long now = SystemClock.uptimeMillis();
      MotionEvent cancelEvent = MotionEvent.obtain(now, now,
              MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
      onTouchEvent(cancelEvent);
      mLayoutFrozen = frozen;
      mIgnoreMotionEventTillDown = true;
      stopScroll();  
    }
}

我們看到他背后做的是發送一個cancelEvent同時調用了stopScroll()去停止滾動,背后是怎么停止滾動的呢?

public void stopScroll() {
    setScrollState(SCROLL_STATE_IDLE);
    stopScrollersInternal();
}

private void setScrollState(int state) {
    if (state == mScrollState) {
        return;
    } 
    ...
    mScrollState = state; 
    dispatchOnScrollStateChanged(state);
}

void dispatchOnScrollStateChanged(int state) {
    // Let the LayoutManager go first; this allows it to bring any properties into
    // a consistent state before the RecyclerView subclass responds.
    if (mLayout != null) {
        mLayout.onScrollStateChanged(state);
    }

    // Let the RecyclerView subclass handle this event next; any LayoutManager property
    // changes will be reflected by this time.
    onScrollStateChanged(state);

    // Listeners go last. All other internal state is consistent by this point.
    if (mScrollListener != null) {
        mScrollListener.onScrollStateChanged(this, state);
    }
    if (mScrollListeners != null) {
        for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
            mScrollListeners.get(i).onScrollStateChanged(this, state);
        }
    }
}

/**
 * Similar to {@link #stopScroll()} but does not set the state.
 */
private void stopScrollersInternal() {
    mViewFlinger.stop();
    if (mLayout != null) {
        mLayout.stopSmoothScroller();
    }
}

void stopSmoothScroller() {
        if (mSmoothScroller != null) {
            mSmoothScroller.stop();
        }
    }

上面代碼我們看到些有意思的東西,他先去調用我們的mLayout去設置狀態是IDLE閑置狀態,再不通知監聽的接口更新狀態。最后才是實際的調用mLayout的stopSmoothScroller()去停止,這個SmoothScroller是一個靜態的抽象內部類,具體干活的是LinearSmoothScroller
這個類最終是這mLayout是LayoutManager類,它是RecycleView的一個靜態的抽象內部類,主要負責的是Measuring和Positioning我們的Item views 。
干活的有三個StaggeredGridLayoutManagerLinearLayoutManagerGridLayoutManager

StaggeredGridLayoutManager mGridLayoutManager =
                    new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
//兩列豎直方向的瀑布流
mRecyclerView.setLayoutManager(mStaggeredGridLayoutManager);

相信使用過RecyclerView的應該對這么名字不陌生,經典的案例就是拿來修改方向燈。這個類有個2K行的就不深挖了,點到即可,繼續回主線。

    /**
     * Stops running the SmoothScroller in each animation callback. Note that this does not
     * cancel any existing {@link Action} updated by
     * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
     * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}.
     */
final protected void stop() {
        if (!mRunning) {
            return;
        }
        onStop();
        mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION;
        mTargetView = null;
        mTargetPosition = RecyclerView.NO_POSITION;
        mPendingInitialRun = false;
        mRunning = false;
        // trigger a cleanup
        mLayoutManager.onSmoothScrollerStopped(this);
        // clear references to avoid any potential leak by a custom smooth scroller
        mLayoutManager = null;
        mRecyclerView = null;
    } 

我們到一個有意思的事情了,他在運行了得情況下并沒有實際的去停止運行,就像我們的AsyncTask一樣,是個假停止。如果沒運行,才調用SmoothScroller.onStop()去實際的停止。

繼續回主線,我們看完 setLayoutFrozen(false)的過程
現在繼續下一步

setAdapterInternal(adapter, false, true);

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();
}

這個更改適配器 的界面,主要就更換了原來的適配器,然后注冊新的數據觀察者等操作
重要一句是調用Recycler的onAdapterChanged()方法。這個Recycler主要的工作是負責我們在RecyclerView上的各自小itemView的重用功能,所以我們更新了適配器需要告訴下人家。

void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
            boolean compatibleWithPrevious) {
        clear();
        getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious);
    }

這樣他就先去調用clear函數去清空原有的。再去調用RecycledViewPool的更新。
需要補充下,這個RecycledViewPool是RecyclerViews的靜態內部類,他可以讓你做到在不同的RecyclerViews內共享Views,這確實對我們的第一個問題有一定的解答作用,因為這是一個靜態內部類啊,而且我們的View都是繼承自ViewHolder的,就像我們java的object給人的感覺一樣。這樣用一個內部的ViewPool的做法,就像線程池,我們可以達到了更高的復用,提高滾動的效率。

private SparseArray<ArrayList<ViewHolder>> mScrap;

這個是RecycledViewPool內部使用稀疏數組來存儲我們的ViewHolder。嗯,稀疏,直覺好像覺得不對啊,后面看完再看下是怎么回事.

void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
            boolean compatibleWithPrevious) {
    if (oldAdapter != null) {
        detach();
    }
    if (!compatibleWithPrevious && mAttachCount == 0) {
         clear();
     }
    if (newAdapter != null) {
       attach(newAdapter);
    }
 } 

void detach() {
     mAttachCount--;
}
    
void attach(Adapter adapter) {
      mAttachCount++;//啊...這句讓我有點意外,傳的參數留著以后用?那就以后再加嘛.. 
}

public void clear() {
        mScrap.clear();
    }

這里記錄有多少個適配器,同時保存我們的ViewHolder,當我們的適配器都移除了,那就清空緩存的ViewHolder。
我們看下他存的方式

public void putRecycledView(ViewHolder scrap) {
     final int viewType = scrap.getItemViewType();
     final ArrayList scrapHeap = getScrapHeapForType(viewType);
     if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
         return;
     }
     if (DEBUG && scrapHeap.contains(scrap)) {
         throw new IllegalArgumentException("this scrap item already exists");
     }
     scrap.resetInternal();
     scrapHeap.add(scrap);
 }

private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
    ArrayList<ViewHolder> scrap = mScrap.get(viewType);
      if (scrap == null) {
          scrap = new ArrayList<ViewHolder>();
          mScrap.put(viewType, scrap);
          if (mMaxScrap.indexOfKey(viewType) < 0) {
              mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
          }
      }
      return scrap;
 }

他的存儲是用viewType來做key從而存儲對應的ViewHolder列表。
目前在我的開發項目中,這個ViewType存在感有點弱啊。
查看整個過程,發現這個itemViewType最后就是調用的是getItemViewType(int position),默認為0;

final int type = mAdapter.getItemViewType(offsetPosition);

這個補充一點,在前面的一篇比較RecyclerView和Listview的文章有提到,如果要給我們的RecyclerView添加頭和尾,不想Listview那樣可以 簡單的加,實際會負責一點,其中就需要用到這個函數。具體的看 Listview和RecycleView的簡單比較 這篇文章里面的缺點第一條。

看完大致的設置適配器部分內容,我們繼續回主線。
到了最后的一個函數

requestLayout();

因為我們的RecyclerView是直接繼承ViewGroup 的,那這句就會導致重畫等步驟,我們繼續看下去吧。
說道這里感覺也可以再開個貼,介紹下View的繪制流程和事件的傳遞流程,下次有空再寫吧,雖然現在介紹這個已是爛大街的了,但自己來寫應該有什么感覺呢?寫了才知道 _
繼續:

我們看下實際的繪制界面的部分吧


今天時間有限,下次繼續寫。。。

后記

那個layoutManager可以做很多文章啊,上次就看到一個有意思的項目叫倫敦眼的

LondonEyeLayoutManager

他的效果就像摩天輪一樣繞著轉動!


這里寫圖片描述
這里寫圖片描述
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容

  • 自從有了Recycleview,很多原本是我們的Listview業務都被替代了,關于兩者的簡單比較,可以看這篇文章...
    SanjayF閱讀 2,729評論 0 4
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,607評論 25 708
  • 文/麗子 史鐵生說:“發燒了,才知道不發燒的日子是多么清爽;咳嗽了,才體會不咳嗽的嗓子多么安詳。剛坐上輪椅時,我老...
    麗子a閱讀 336評論 8 6
  • 絲簾頻撫眉前,不平焉。 抿袖怨春寒兩次三番。 雖北地,經微雨,似淮南。 清霧催人愁每每無端。 ————————— ...
    陳婉_閱讀 473評論 0 3
  • 題記:Each man is the architect of his own fate.——每個人都是自己命運的...
    LynnXYT閱讀 873評論 1 4