Adapter最佳實踐

本文會不按期更新,推薦watch下項目javascript

若是喜歡請star,若是以爲有紕漏請提交issue,若是你有更好的點子能夠提交pull request。php

本文的示例代碼主要是基於CommonAdapter這個庫編寫的,若你有其餘的技巧和方法能夠參與進來一塊兒完善這篇文章。 html

本文固定鏈接:github.com/tianzhijiex…java


1、背景

  • 從維護角度看,大量的項目中的adapter都是雜亂不堪的
  • 從擴展角度看,在多個type的狀況下,不管是維護仍是擴展都變得十分複雜
  • 從設計角度看,咱們沒法明肯定義adapter屬於的層級
  • 從性能角度看,adapter的好壞對於list頁面的性能有着關鍵的做用

爲了下降項目代碼的複雜度,讓你們能專一於業務而不是考慮性能,咱們必需要對adapter進行一個封裝。android

2、需求

基礎:ios

  1. item必須是高內聚的,能處理本身的點擊事件,它獨立於adapter
  2. item自己能夠得到當前頁面的activity
  3. adapter不該是一個獨立的類,它更合適做沒有複用價值的內部類
  4. adapter能支持多種item類型,僅改動兩行代碼便可添加一個新的item
  5. adapter能對自身的item進行自動複用,無需手動判斷

性能:git

  1. adapter對findviewById()應有自動的優化策略,相似於ViewHolder
  2. item自身的setListener應僅設置一次,不在getView時new出多餘的listener
  3. adapter應提供item的局部刷新功能
  4. 若是一個item過於複雜,能夠將其拆分紅多個小的item
  5. 若是item中要加載網絡或本地圖片,先在線程中加載,加載好後切回主線程顯示
  6. 在快速滑動時不加載網絡圖片或中止gif圖和視頻的播放
  7. 若是item中文本過多,能夠採用textview的預渲染方案
  8. 若是發現item由於measure任務太重,則要經過自定義view來優化此item
  9. 經過判斷已經顯示的內容和須要顯示的新內容是否不一樣來決定要不要從新渲染view
  10. 適當的使用RecycledViewPool來緩存item對象
  11. 使用recycleView的預取(Prefetch)

擴展:程序員

  1. listview的adapter應在修改一兩行代碼後支持recyclerView
  2. 一個adapter中的不一樣item能夠接收不一樣的數據對象
  3. adapter應支持數據綁定,數據變了後界面應自動刷新

設計:github

  1. adapter應該有明確的層級定位,數據不該知道adapter和view的存在

其餘: 編程

  1. 根據項目的結構封裝一個統一的item的基類,它能夠減小大量的基礎代碼
  2. 多個type的時候item一般均可以再抽出一個父類,佈局也能夠用include標籤
  3. 能知道當前RecycleView的滑動距離和滑動方向
  4. adapter能支持添加hearder和footer,對於有/無header時的空態有不一樣的處理
  5. 容許用viewpager的notifyDataSetChanged()來更新界面

3、實現

本篇會大量利用CommonAdapter這個庫和其他的工具類進行實現,下文會直接使用CommonAdapter的api。

基礎

item高內聚

item要能獨立的處理自身的邏輯和事件,讓自身成爲一個獨立的ui模塊。假設你的item就是一個textView:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" />複製代碼

如今只須要這麼寫:

public class TextItem implements AdapterItem<JsonModel> {

    private TextView text;

    public int getLayoutResId() {
        return R.layout.demo_item_text;
    }

    public void bindViews(View root) {
        text = (TextView) root.findViewById(R.id.textView);
    }

    public void setViews() {}

    public void handleData(JsonModel model, int position) {
        text.setText(model.content);
    }

}複製代碼

如今,你能夠將它放入不一樣的界面,只須要給他一樣的數據模型便可。在一個item被多個頁面用的情形中還能夠作更多的優化,好比設置全局的緩存池等等。
分離後的item能夠更加易於維護,而且咱們能夠針對listview和item兩者進行獨立的性能優化,好比作一個通用的list頁面組件,item經過插拔的方式接入,item自身的數據進行diff優化等等。

我強烈建議不要用ItemOnListener作點擊的判斷,而是在每一個item中作判斷。在item中能夠經過root.getContext()來獲得當前頁面的activity,這樣就能夠處理各類頁面的跳轉了。

private Activity mActivity;

    @Override
    public void bindViews(View root) {
        mActivity = (Activity) root.getContext();
    }

    public Activity getActivity() {
        return mActivity;
    }複製代碼

好處:
item自身能知道本身的全部操做,而ListView僅僅作個容器。如今RecyclerView的設計思路也是如此的,讓item獨立性增長。並且若是要帶數據到別的頁面,也能夠直接拿到數據。
壞處:
外部對內部徹底不知情,對於統一的事件沒辦法作到很好的統一處理。

將adapter變成內部類

爲了說明,我創建了一個數據模型:

public class DemoModel {
    public String content;
    public String type;
}複製代碼

它就是一個POJO,沒有任何特別之處,它徹底不知道其餘對象的存在。

adapter作的事情是將數據和ui進行綁定,不一樣頁面的adapter基本是不可複用的狀態,並且如今主要的事情在item中處理了,因此adapter就一般是以一個內部類的形式出現,如:

listView.setAdapter(new CommonAdapter<DemoModel>(data) {
    @Override
    public AdapterItem<DemoModel> createItem(Object itemType) {
        return new Item();
    }
});複製代碼

支持多種item類型

listView.setAdapter(new CommonAdapter<DemoModel>(data) {
    @Override
    public Object getItemType(DemoModel demoModel) {
        // 返回item的類型,強烈建議是string,int,float之類的基礎類型,也容許class類型
        return demoModel.type;
    }

    @Override
    public AdapterItem<DemoModel> createItem(Object type) {
        switch ((String) type) {
            case "text":
                return new TextItem();
            case "button":
                return new ButtonItem();
            case "image":
                return new ImageItem();
        }
    }
});複製代碼

如今若是加了新的需求,要多支持一個item類型,你只須要在switch-case語句塊中新增一個case就行,簡單且安全。

自動複用內部的item

咱們以前對adapter的優化常常是須要在getView中判斷convertView是否爲null,若是不爲空就不new出新的view,這樣來實現item複用。先來看看上面已經出現屢次的AdapterItem是個什麼。

public interface AdapterItem<T> {

    /** * @return item佈局文件的layoutId */
    @LayoutRes
    int getLayoutResId();

    /** * 初始化views */
    void bindViews(final View root);

    /** * 設置view */
    void setViews();

    /** * 根據數據來設置item的內部views * * @param model 數據list內部的model * @param position 當前adapter調用item的位置 */
    void handleData(T model, int position);

}複製代碼
方法 描述 作的工做
getLayoutResId 你這個item的佈局文件是什麼 返回一個R.layout.xxx
bindViews 在這裏作findviewById的工做 btn = findViewById(R.id.xx)
setViews 在這裏初始化view各個參數 setcolor ,setOnClickListener...
handleData 數據更新時會調用(相似getView) button.setText(model.text)

其實這裏就是view的幾個過程,首先初始化佈局文件,而後綁定佈局文件中的各個view,接着進行各個view的初始化操做,最後在數據更新時進行更新的工做。

分析完畢後,我去源碼裏面翻了一下,發現了這個庫對item複用的優化:

LayoutInflater mInflater;

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // 不重複建立inflater對象,不管你有多少item,我都僅僅建立一次
    if (mInflater == null) {
        mInflater = LayoutInflater.from(parent.getContext());
    }

    AdapterItem<T> item;
    if (convertView == null) {
        // 當convertView爲null,說明沒有複用的item,那麼就new出來
        item = getItemView(mType);
        convertView = mInflater.inflate(item.getLayoutResId(), parent, false);
        convertView.setTag(R.id.tag_item, item);
        // 調用bindView進行view的findview,僅僅是新new出來的view纔會調用一次
        item.onBindViews(convertView); 
        // findview後開始setView。將綁定和設置分離,方便整理代碼結構
        item.onSetViews(); 
    } else {
        // 若是這個item是能夠複用的,那麼直接返回
        item = (AdapterItem<T>) convertView.getTag(R.id.tag_item);
    }
    // 不管你是否是複用的item,都會在getView時觸發updateViews方法,更新數據
    item.onUpdateViews(mDataList.get(position), position);
    return convertView;
}複製代碼

關鍵代碼就是這一段,因此只須要明白這一段代碼作的事情,不管在使用這個庫時遇到了什麼問題,你均可以沒必要驚慌,由於你掌握了它的原理。

明白了第三方庫的原理,才能夠放心大膽的使用

性能

對findviewById方法的優化

經過上述對源碼的分析,如今只須要在bindViews中寫findview的代碼便可讓這個庫自動實現優化。若是你用了databinding,一行代碼解決問題:

private DemoItemImageBinding b;

@Override
public void bindViews(View root) {
    b = DataBindingUtil.bind(root);
}複製代碼

傳統作法:

TextView textView;

@Override
public void bindViews(View root) {
    textView = (TextView) root.findViewById(R.id.textView);
}複製代碼

item自身的setListener應僅設置一次

咱們以前會圖省事在listview的getView中隨便寫監聽器,以致於出現了new不少多餘listener的現象。

public View getView(int positon, View convertView, ViewGroup parent){
    if(null == convertView){
        convertView = LayoutInflater.from(context).inflate(R.layout.item, null);
    }

    Button button = ABViewUtil.obtainView(convertView, R.id.item_btn);
    button.setOnClickListener(new View.OnClickListener(){ // 每次getView都會new一個listener
        @Override
        public void onClick(View v){
            Toast.makeText(context, "position: " + position, Toast.LENGTH_SHORT).show();
        }
    });

}複製代碼

如今,咱們在setViews中寫上監聽器就行。

public class ButtonItem implements AdapterItem<DemoModel> {

    /** * 由於這個方法僅僅在item創建時才調用,因此不會重複創建監聽器。 */
    @Override
    public void setViews() {
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // ...
            }
        });
    }

    @Override
    public void handleData(DemoModel model, int position) {
        // 這裏避免作耗時的操做
    }

}複製代碼

這樣setViews()保證了item只會new一次監聽器,在handleData()中若是要加載圖片,請在線程中加載,加載好了後切回主線程顯示(通常圖片庫都作了這樣的處理)。

若是咱們要在每次點擊的時候獲得當前item中的data和positon就比較麻煩了,因此只能寫幾個getXxxx()。

private T data;

    private int pos;

    @Override
    public void handleData(T t, int position) {
        data = t;
        pos = position;   
    }

    public T getData() {
        return data;
    }

    public int getPos() {
        return pos;
    }複製代碼

建議:這塊的代碼建議抽到baseItem中去寫。

提供局部刷新功能

這個功能在recyclerView中就已經提供了,我就不廢話了。網上流傳比較多的是用下面的代碼作listview的單條刷新:

private void updateSingleRow(ListView listView, long id) {  
        if (listView != null) {  
            int start = listView.getFirstVisiblePosition();  
            for (int i = start, j = listView.getLastVisiblePosition(); i <= j; i++)  
                if (id == ((Messages) listView.getItemAtPosition(i)).getId()) {  
                    View view = listView.getChildAt(i - start);  
                    getView(i, view, listView);  
                    break;  
                }  
        }  
    }複製代碼

其實就是手動調用了對應position的item的getView方法,我的以爲不是很好,如今直接使用recyclerView的notifyItemChanged(index)就行。

/** * Notify any registered observers that the item at <code>position</code> has changed. * Equivalent to calling <code>notifyItemChanged(position, null);</code>. * * <p>This is an item change event, not a structural change event. It indicates that any * reflection of the data at <code>position</code> is out of date and should be updated. * The item at <code>position</code> retains the same identity.</p> * * @param position Position of the item that has changed * * @see #notifyItemRangeChanged(int, int) */
    public final void notifyItemChanged(int position) {
        mObservable.notifyItemRangeChanged(position, 1);
    }複製代碼

上面提到的是對局部的某個item進行刷新,可是若是咱們須要對某個item中的某個view進行刷新呢?

/** * Notify any registered observers that the item at <code>position</code> has changed with an * optional payload object. * * <p>This is an item change event, not a structural change event. It indicates that any * reflection of the data at <code>position</code> is out of date and should be updated. * The item at <code>position</code> retains the same identity. * </p> * * <p> * Client can optionally pass a payload for partial change. These payloads will be merged * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the * item is already represented by a ViewHolder and it will be rebound to the same * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing * payloads on that item and prevent future payload until * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not * attached, the payload will be simply dropped. * * @param position Position of the item that has changed * @param payload Optional parameter, use null to identify a "full" update * * @see #notifyItemRangeChanged(int, int) */
    public final void notifyItemChanged(int position, Object payload) {
        mObservable.notifyItemRangeChanged(position, 1, payload);
    }複製代碼

notifyItemChanged(index, obj)這個方法的第一個參數用來肯定刷新的item位置,第二個參數一般用來傳遞一個標誌,來告訴item須要刷新的東西。

《RecyclerView animations done right》一文中經過點贊動畫舉出了一個很不錯的例子。

整個的item是巨大且複雜的,但咱們點贊後只須要對一個view進行動畫的操做,處理方式就須要從新考慮了。

  • item本身處理點擊事件,被點擊後findview找到那個view,而後進行動畫的操做
  • 經過外部adapter的notifyItemChanged(index, obj)來通知item當前是否要作動畫

一般狀況下咱們都會選擇方案一,可是若是要用第二種方式呢?

  1. 外部進行notify
    notifyItemChanged(adapterPosition, ACTION_LIKE_BUTTON_CLICKED);複製代碼
  2. 判斷payload
    @Override
    public void onBindViewHolder(ViewHolder holder, int position, List<Object> payloads) {
     if (payloads.isEmpty()) {
         // payloads爲空,說明是更新整個viewHolder
         onBindViewHolder(holder, position);
     } else {
         // payloads 不爲空,這隻更新須要更新的view便可
         if(payloas.get(0).equals(ACTION_LIKE_BUTTON_CLICKED)) {
             // ...
         }
     }
    }複製代碼

這裏的關鍵點在於payloads這個參數,往大里說你能夠通知某個item產生了某個事件,至於接收到事件後作什麼就看你了。
這個的關鍵思路是外部不該該知道內部的數據,而是產生一個事件,好比「點讚了」,而item內部是根據這個事件來進行本身的操做的,是面向對象的思路。

若是一個item過於複雜,能夠將其拆分紅多個小的item

關於這點是facebook提出的android優化技巧,後來我瞭解到ios自己也能夠這麼作。


如圖所示,這個item很複雜,並且很大。當你的item佔據三分之二屏幕的時候就能夠考慮這樣的優化方案了。右圖說明了將一個總體的item變成多個小item的效果。在這種拆分後,你會發現原來拆分後的小的item可能在別的界面或別的type中也用到了,這就出現了item模塊化的思想,總之是一個挺有意思的優化思路。

詳細的文章(中文)請參考《facebook新聞頁ListView的優化方案》,十分感謝做者的分享和翻譯!

坑!!!

若是你是作論壇的項目,會有各類樓層或者回復嵌套的狀況,你能夠考慮用這種方式,但確定會遇到不少坑。下面是《Android ListView中複雜數據流的高效渲染》中提到的一些坑。

  • item的拆分和拼湊是須要本身進行實現的,具體的type確定和json中的type不一樣,須要作邏輯屏蔽。極可能會加大同事之間的閱讀代碼的難度。
  • 因爲優化的需求,把邏輯上的一個Item拆分爲了多個item,所以每一個item上都要設置ItemClick事件。具體實現時能夠寫一個基類,在基類中對item click進行處理。
  • 在item 點擊時,通常須要有按壓效果,此時邏輯上的item已經進行了拆分,須要策略實現邏輯上item的總體按壓,而不是隻有某個拆分後的item被按壓。
  • 咱們知道listview的item之間是有divider的,此時須要設置divider爲null,咱們經過添加item的方式來實現divider效果。

在快速滑動時不加載網絡圖片或中止gif圖的播放

這個在QQ空間和微信朋友圈詳情頁中很常見,目前的小視頻列表也是大圖加文字的形式。滾動時自動中止的功能我但願交給圖片框架作,而不是手動處理,若是你要手動處理,那麼你還得考慮不一樣頁面的不一樣狀況,感受性價比過低。
若是你的圖片庫沒有作這樣的處理,能夠參考Android-Universal-Image-Loader中的實現方法。

@Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        switch (scrollState) {
            case OnScrollListener.SCROLL_STATE_IDLE:
                imageLoader.resume();
                break;
            case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                if (pauseOnScroll) {
                    imageLoader.pause();
                }
                break;
            case OnScrollListener.SCROLL_STATE_FLING:
                if (pauseOnFling) {
                    imageLoader.pause();
                }
                break;
        }
        if (externalListener != null) {
            externalListener.onScrollStateChanged(view, scrollState);
        }
    }複製代碼

採用textview的預渲染方案

若是你是作bbs或者作新聞的,你會發現item中會有大量的文字,而文字過多或者有着大量的表情和特殊符號的時候,列表確定會卡頓。textview實際上是一個很基本但不簡單的view,裏面作了大量的判斷和處理,因此並不是十分高效。

Instagram(現已在facebook旗下)分享了他們是如何優化他們的TextView渲染的效率的,在國內有做者也專門寫了一篇文章來講明其原理的。

當你有心想要優化textview的時候,你會發如今咱們知道這個item中textview的寬度和文字大小的狀況下能夠把初始化的配置作個緩存,每一個textview只須要用這個配置好的東西進行文字的渲染便可。下面是經過優化獲得的結果:

這裏測試的機器是MX3,左側是直接使用StaticLayout的方案,右側是系統的默認方案,Y軸是FPS,能夠看出來,使用優化以後的方案,幀率提高了許多。
我只推薦在measure成爲瓶頸的時候纔去使用這樣的優化策略,不要過分優化

原理

textview支持上下左右的drawable,並且支持超鏈和emoji表情,每次繪製的時候都會進行檢查,效率天然不會十分出衆。在Android中,文本的渲染是很慢的。即便在一個像Nexus 5這樣的新設備上,一段有十幾行復雜文本的圖片說明的初始繪製時間可能會達到50ms,而其文本的measure階段就須要30ms。這些都發生在UI線程,在滾動時會致使app跳幀。

textview的繪製本質是layout的繪製,setText()被調用後,就會選擇合適的layout進行繪製工做。textview的onDraw()中能夠看到以下方法:

void onDraw() {
        // ...
        if (mLayout == null) {
            assumeLayout();
        }
        Layout layout = mLayout;

        // ....

}複製代碼
/** * Make a new Layout based on the already-measured size of the view, * on the assumption that it was measured correctly at some point. */
    private void assumeLayout() {
        int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
        if (width < 1) {
            width = 0;
        }
        int physicalWidth = width;
        if (mHorizontallyScrolling) {
            width = VERY_WIDE;
        }
        makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
                      physicalWidth, false);
    }複製代碼

makeNewLayout(...)是個很長的方法,就補貼出來了。總之咱們能夠經過本身定義一個layoutView來進行文本的繪製,再配合Android在ICS中引入了TextLayoutCache實現text的預渲染。使人欣喜的是,目前facebook開源了一個至關不錯的layout的build,有了它就能夠幫助咱們快速創建一個高性能的textview了,感興趣的同窗能夠用起來了。

擴展閱讀:

《Instagram是如何提高TextView渲染性能的》

《TextView預渲染研究》

6. 經過自定義viewGroup來減小重複的measure

fb的的人發現目前項目中有不少穩定的item的繪製效率不高,因此就開始研究measure的耗時。

用linearlayout的時候:

> LinearLayout [horizontal]       [w: 1080  exactly,       h: 1557  exactly    ]
    > ProfilePhoto                [w: 120   exactly,       h: 120   exactly    ]
    > LinearLayout [vertical]     [w: 0     unspecified,   h: 0     unspecified]
        > Title                   [w: 0     unspecified,   h: 0     unspecified]
        > Subtitle                [w: 0     unspecified,   h: 0     unspecified]
        > Title                   [w: 222   exactly,       h: 57    exactly    ]
        > Subtitle                [w: 222   exactly,       h: 57    exactly    ]
    > Menu                        [w: 60    exactly,       h: 60    exactly    ]
    > LinearLayout [vertical]     [w: 900   exactly,       h: 1557  at_most    ]
        > Title                   [w: 900   exactly,       h: 1557  at_most    ]
        > Subtitle                [w: 900   exactly,       h: 1500  at_most    ]複製代碼

用RelativeLayout的時候:

> RelativeLayout                  [w: 1080  exactly,   h: 1557  exactly]
    > Menu                        [w: 60    exactly,   h: 1557  at_most]
    > ProfilePhoto                [w: 120   exactly,   h: 1557  at_most]
    > Title                       [w: 900   exactly,   h: 1557  at_most]
    > Subtitle                    [w: 900   exactly,   h: 1557  at_most]
    > Title                       [w: 900   exactly,   h: 1557  at_most]
    > Subtitle                    [w: 900   exactly,   h: 1500  at_most]
    > Menu                        [w: 60    exactly,   h: 60    exactly]
    > ProfilePhoto                [w: 120   exactly,   h: 120   exactly]複製代碼

咱們都發現了對於menu,title,subtitle的重複測量。fb的工程師最終用自定義的viewgroup手動控制了佈局和測量參數,最終實現了每一個view僅僅測量一次的優秀結果。優化事後,facebook的工程師講解了他們對上面這個佈局的優化策略,內容翔實,是個很好的分享。

擴展閱讀:

原文:《Custom ViewGroups》
中文:《聽FackBook工程師講Custom ViewGroups》

使用RecycledViewPool來緩存item

Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. This can be useful if you have multiple RecyclerViews with adapters that use the same view types, for example if you have several data sets with the same kinds of item views displayed by a ViewPager.
RecyclerView automatically creates a pool for itself if you don’t provide one.

正如上文所說RecycledViewPool的主要做用是多個頁面的item共享,好比是能夠滑動的tab頁面,每一個頁面的vh都是同樣的,在這種狀況下用它就很合適了。

鬥魚的個tab的頁面裏面的item都是徹底同樣的,對於首頁這個多fragment的結構來講,採用viewpool會大大提性能。

Tips:

  • 由於commonAdapter幫助你將各類類型的type都轉換爲int知了,因此須要採用自定義的RecyclePool來作這樣的操做。
RecycledViewPool pool = new RecycledViewPool();

// ...

recyclerView.setRecycledViewPool(pool);
adapter.setTypePool(pool.getTypePool());複製代碼
  • RecycledViewPool是依據ItemViewType來索引ViewHolder的,因此不一樣頁面的相同的item的type必須是同樣的值才能被準確的複用。

  • RecycledViewPool也能夠經過mPool.setMaxRecycledViews(itemViewType, number)來設置緩存數目。

  • RecyclerView能夠經過recyclerView.setItemViewCacheSize(number)設置本身所須要的ViewHolder數量,只有超過這個數量的detached ViewHolder纔會丟進ViewPool中與別的RecyclerView共享。也就說每一個頁面能夠設置本身不想和別的頁面共用的viewholder數目。

  • 在合適的時機,RecycledViewPool會自我清除掉所持有的ViewHolder對象引用,固然你也能夠在你認爲合適的時機手動調用clear()。

判斷已有的數據和新數據的異同

若是是加載圖片,我仍是但願你去看看你用的圖片框架有沒有作這樣的優化,若是有就請放心,若是沒有那就本身處理吧。若是你的item中文字不少,常常有幾百個文字。那麼也能夠先判斷要顯示的文字和textview中已經有的文字是否一致,若是不一致再調用setText方法。

@Override
public void handleData(DemoModel model, int position) {
    if (b.imageView.getTag() != null) {
        mOldImageUrl = (int) b.imageView.getTag();
    }
    int imageUrl = Integer.parseInt(model.content);

    if (mOldImageUrl == 0 && mOldImageUrl != imageUrl) {
        b.imageView.setTag(imageUrl); // set tag
        b.imageView.setImageResource(imageUrl); // load local image
    }
}複製代碼

使用Prefetch特性

在滾動和滑動的時候,RecyclerView須要顯示進入屏幕的新item,這些item須要被綁定數據(若是緩存中沒有相似的item極可能還須要建立),而後把它們放入佈局並繪製。當全部這些工做慢吞吞進行的時候,UI線程會慢慢停下來等待其完成,而後渲染才能進行,滾動才能繼續。google的工程師看到在須要一個新的item時,咱們花了太多時間去準備這個item,但同時UI線程卻早早的完成了前一幀的任務,休眠了大量時間,因而修改了創建vh和繪製的工做流程。

詳細內容請參考:RecyclerView的新機制:預取(Prefetch) - 泡在網上的日子

擴展

Listview無痛遷移至recyclerView

現在recyclerView大有接替listview的趨勢,要知道listview的適配器和recyclerView的適配器的寫法是不一樣的。
listview的寫法以下:

listView.setAdapter(new CommonAdapter<DemoModel>(data,1) {

    @Override
    public AdapterItem<DemoModel> getItemView(Object type) {
        return new TextItem();
    }
});複製代碼

換成recyclerView的適配器應該須要不少步吧?不,改一行足矣。

recyclerView.setAdapter(new CommonRcvAdapter<DemoModel>(data) {

    public AdapterItem<DemoModel> getItemView(Object type) {
        return new TextItem();
    }
});複製代碼

這裏換了一個適配器的類名和容器名,其他的都沒變。

同一個adapter的不一樣item能夠接收不一樣的數據對象

咱們的adapter是有一個泛型的,item也是有泛型,通常狀況下adapter的泛型對象就是item的對象。

return new CommonAdapter<DemoModel>(data, 1) { // DemoModel
    public AdapterItem createItem(Object type) {
        // 若是就一種,那麼直接return一種類型的item便可。
        return new TextItem();
    }
};複製代碼
public class TextItem implements AdapterItem<DemoModel> { // DemoModel
    // ...
}複製代碼

但這並不是是必須的,因此你能夠經過adapter的getConvertedData(...)進行數據的轉換,讓adapter接收的數據和item的數據不一樣。

/** * 作數據的轉換,這裏算是數據的精細拆分 */
public Object getConvertedData(DemoModel data, Object type) {
    // 這樣能夠容許item自身的數據和list數據不一樣
    return data.content; // model -> string
}複製代碼

支持數據綁定

CommonAdapter能夠結合dataBinding中的ObservableList進行數據的自動綁定操做。源碼以下:

protected CommonRcvAdapter(@NonNull ObservableList<T> data) {
        this((List<T>) data);
        data.addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<T>>() {
            @Override
            public void onChanged(ObservableList<T> sender) {
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeChanged(ObservableList<T> sender, int positionStart, int itemCount) {
                notifyItemRangeChanged(positionStart, itemCount);
            }

            @Override
            public void onItemRangeInserted(ObservableList<T> sender, int positionStart, int itemCount) {
                notifyItemRangeInserted(positionStart, itemCount);
                notifyItemRangeChanged(positionStart, itemCount);
            }

            @Override
            public void onItemRangeRemoved(ObservableList<T> sender, int positionStart, int itemCount) {
                notifyItemRangeRemoved(positionStart, itemCount);
                notifyItemRangeChanged(positionStart, itemCount);
            }

            @Override
            public void onItemRangeMoved(ObservableList<T> sender, int fromPosition, int toPosition, int itemCount) {
                // Note:不支持一次性移動"多個"item的狀況!!!!
                notifyItemMoved(fromPosition, toPosition);
                notifyDataSetChanged();
            }
        });
    }複製代碼

如今只要咱們對list對象進行操做,adapter就會自動去更新界面,不再用去手動notify了。

咱們可能還記得support中的一個新的工具類——diffUtil,它能夠配合recycleview進行自動的notify操做,若是咱們要用它就須要作一些處理了。

public static abstract class DiffRcvAdapter<T> extends CommonRcvAdapter<T> {

    DiffRcvAdapter(@Nullable List<T> data) {
        super(data);
    }

    @Override
    public void setData(@NonNull final List<T> data) {
        DiffUtil.calculateDiff(new DiffUtil.Callback() {
            @Override
            public int getOldListSize() {
                return getItemCount();
            }

            @Override
            public int getNewListSize() {
                return data.size();
            }

            /** * 檢測是不是相同的item,這裏暫時經過位置判斷 */
            @Override
            public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                boolean result = oldItemPosition == newItemPosition;
                Log.d(TAG, "areItemsTheSame: " + result);
                return result;
            }

            /** * 檢測是不是相同的數據 * 這個方法僅僅在areItemsTheSame()返回true時,才調用。 */
            @Override
            public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                boolean result = isContentSame(getData().get(oldItemPosition), data.get(newItemPosition));
                Log.d(TAG, "areContentsTheSame: " + result);
                return result;
            }
        }).dispatchUpdatesTo(this); // 傳遞給adapter
        super.setData(data);

    }

    protected abstract boolean isContentSame(T oldItemData, T newItemData);
}複製代碼
final DiffRcvAdapter<DemoModel> adapter = new DiffRcvAdapter<DemoModel>(DataManager.loadData(this, 3)) {
    @NonNull
    @Override
    public AdapterItem createItem(Object type) {
        return new TextItem();
    }

    @Override
    protected boolean isContentSame(DemoModel oldItemData, DemoModel newItemData) {
        return oldItemData.content.equals(newItemData.content);
    }
};複製代碼

這裏須要多作的是手動判斷item的數據是否要更新,因此不如用ObservableArrayList比較簡單,並且是直接更新,不佔cpu。
須要注意的是,若是用diffutil,你的item必須是viewholder,由於它最終調用的是adapter.notifyItemRangeChanged(position, count, payload),因此就會調用adapter中的onBindViewHolder(VH holder, int position, List<Object> payloads)

[diffUtil]

public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
    dispatchUpdatesTo(new ListUpdateCallback() {
        @Override
        public void onInserted(int position, int count) {
            adapter.notifyItemRangeInserted(position, count);
        }

        @Override
        public void onRemoved(int position, int count) {
            adapter.notifyItemRangeRemoved(position, count);
        }

        @Override
        public void onMoved(int fromPosition, int toPosition) {
            adapter.notifyItemMoved(fromPosition, toPosition);
        }

        @Override
        public void onChanged(int position, int count, Object payload) {
            adapter.notifyItemRangeChanged(position, count, payload);
        }
    });
}複製代碼

設計

Adapter不屬於UI層

當咱們讓adapter變成一個內部類的時候,剩下的問題就是adapter應該處於view層仍是presenter或model層了。在實際的運用當中,我最終定義adapter是處於presenter層(mvp)或者model層(mvvm)。

是否放在p或vm層有一個簡單的原則就是不可複用,p或vm的代碼複用性是極其低的,因此當你認爲有代碼是不可複用的時候,那麼你就能夠放在裏面。何況ui層面有可能會出現複用的狀況,並且adapter中還會出現和數據相關的一些操做,因此應該讓其與ui層隔離。

當你和ui隔離了,你徹底能夠實現一個list頁面統一的ui,進行空狀態等細節的處理,方便複用統一的ui,十分有用。

其餘

封裝baseItem

item的接口化提供了更大的靈活性,可是就實際項目而言,我強烈推薦去作一個baseItem,這樣能夠快速獲得activity,position,context等等對象。

public abstract class BaseAdapterItem<Bind extends ViewDataBinding, Model> implements AdapterItem<Model> {

    private View root;

    private int pos;

    protected Bind b;

    private Activity activity;

    public BaseAdapterItem(Activity activity) {
        this.activity = activity;
    }

    public BaseAdapterItem() {
    }

    @CallSuper
    @Override
    public void bindViews(View view) {
        root = view;
        b = DBinding.bind(view);
        beforeSetViews();
    }

    protected void beforeSetViews() {

    }

    @CallSuper
    @Override
    public void handleData(Model t, int position) {
        pos = position;
    }

    public View getRoot() {
        return root;
    }

    public int getCurrentPosition() {
        return pos;
    }

    protected static void setVizOrInViz(View view, CharSequence str) {
        if (TextUtils.isEmpty(str)) {
            view.setVisibility(View.INVISIBLE);
        } else {
            view.setVisibility(View.VISIBLE);
        }
    }

    protected static void setVizOrGone(View view, CharSequence str) {
        if (TextUtils.isEmpty(str)) {
            view.setVisibility(View.GONE);
        } else {
            view.setVisibility(View.VISIBLE);
        }
    }

    protected int getColor(@ColorRes int colorResId) {
        return root.getResources().getColor(colorResId);
    }

    protected Context getContext() {
        return root.getContext();
    }

}複製代碼

我經過上面的base和databinding結合後,快速的實現了findview的操做,十分簡潔。

多type的時候抽取父類

若是list頁面中有多個type,你確定會發現不一樣type的item的有相同的邏輯,最多見的是點擊跳轉的邏輯。對於這樣的狀況我建議再抽取一個base來作,之後修改的時候你會發現十分方便。對於ui層面的類似,我也但願能夠適當的使用include標籤進行復用。
我以前偷懶常常不抽取公共部分,由於以爲作基類複雜,公共部分的代碼也很少,可是後面維護的時候處處都要改,因此就給出了這條實踐經驗。

監聽滑動的距離和方向

OnRcvScrollListener是我經常使用的一個監聽類,能夠監聽滾動方向、滾動距離、是否混動到底。

/** * @author Jack Tony * recyle view 滾動監聽器 * @date 2015/4/6 */
public class OnRcvScrollListener extends RecyclerView.OnScrollListener {

    private static final int TYPE_LINEAR = 0;

    private static final int TYPE_GRID = 1;

    private static final int TYPE_STAGGERED_GRID = 2;

    /** * 最後一個的位置 */
    private int[] mLastPositions;

    /** * 最後一個可見的item的位置 */
    private int mLastVisibleItemPosition;

    /** * 觸發在上下滑動監聽器的容差距離 */
    private static final int HIDE_THRESHOLD = 20;

    /** * 滑動的距離 */
    private int mDistance = 0;

    /** * 是否須要監聽控制 */
    private boolean mIsScrollDown = true;

    /** * Y軸移動的實際距離(最頂部爲0) */
    private int mScrolledYDistance = 0;

    /** * X軸移動的實際距離(最左側爲0) */
    private int mScrolledXDistance = 0;

    private int mOffset = 0;

    /** * @param offset 設置:倒數幾個才斷定爲到底,默認是0 */
    public OnRcvScrollListener(int offset) {
        mOffset = offset;
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        int firstVisibleItemPosition = 0;
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        // 判斷layout manager的類型
        int type = judgeLayoutManager(layoutManager);
        // 根據類型來計算出第一個可見的item的位置,由此判斷是否觸發到底部的監聽器
        firstVisibleItemPosition = calculateFirstVisibleItemPos(type, layoutManager, firstVisibleItemPosition);
        // 計算並判斷當前是向上滑動仍是向下滑動
        calculateScrollUpOrDown(firstVisibleItemPosition, dy);
        // 移動距離超過必定的範圍,咱們監聽就沒有啥實際的意義了
        mScrolledXDistance += dx;
        mScrolledYDistance += dy;
        mScrolledXDistance = (mScrolledXDistance < 0) ? 0 : mScrolledXDistance;
        mScrolledYDistance = (mScrolledYDistance < 0) ? 0 : mScrolledYDistance;
        onScrolled(mScrolledXDistance, mScrolledYDistance);
    }


    /** * 判斷layoutManager的類型 */
    private int judgeLayoutManager(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager instanceof GridLayoutManager) {
            return TYPE_GRID;
        } else if (layoutManager instanceof LinearLayoutManager) {
            return TYPE_LINEAR;
        } else if (layoutManager instanceof StaggeredGridLayoutManager) {
            return TYPE_STAGGERED_GRID;
        } else {
            throw new RuntimeException("Unsupported LayoutManager used. Valid ones are "
                    + "LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager");
        }
    }

    /** * 計算第一個元素的位置 */
    private int calculateFirstVisibleItemPos(int type, RecyclerView.LayoutManager layoutManager, int firstVisibleItemPosition) {
        switch (type) {
            case TYPE_LINEAR:
                mLastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
                firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
                break;
            case TYPE_GRID:
                mLastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
                firstVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
                break;
            case TYPE_STAGGERED_GRID:
                StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                if (mLastPositions == null) {
                    mLastPositions = new int[staggeredGridLayoutManager.getSpanCount()];
                }
                mLastPositions = staggeredGridLayoutManager.findLastVisibleItemPositions(mLastPositions);
                mLastVisibleItemPosition = findMax(mLastPositions);
                staggeredGridLayoutManager.findFirstCompletelyVisibleItemPositions(mLastPositions);
                firstVisibleItemPosition = findMax(mLastPositions);
                break;
        }
        return firstVisibleItemPosition;
    }

    /** * 計算當前是向上滑動仍是向下滑動 */
    private void calculateScrollUpOrDown(int firstVisibleItemPosition, int dy) {
        if (firstVisibleItemPosition == 0) {
            if (!mIsScrollDown) {
                onScrollDown();
                mIsScrollDown = true;
            }
        } else {
            if (mDistance > HIDE_THRESHOLD && mIsScrollDown) {
                onScrollUp();
                mIsScrollDown = false;
                mDistance = 0;
            } else if (mDistance < -HIDE_THRESHOLD && !mIsScrollDown) {
                onScrollDown();
                mIsScrollDown = true;
                mDistance = 0;
            }
        }
        if ((mIsScrollDown && dy > 0) || (!mIsScrollDown && dy < 0)) {
            mDistance += dy;
        }
    }

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        int visibleItemCount = layoutManager.getChildCount();
        int totalItemCount = layoutManager.getItemCount();

        int bottomCount = totalItemCount - 1 - mOffset;
        if (bottomCount < 0) {
            bottomCount = totalItemCount - 1;
        }

        if (visibleItemCount > 0 && newState == RecyclerView.SCROLL_STATE_IDLE
                && mLastVisibleItemPosition >= bottomCount && !mIsScrollDown) {
            onBottom();
        }
    }

    protected void onScrollUp() {

    }

    protected void onScrollDown() {

    }

    protected void onBottom() {
    }

    protected void onScrolled(int distanceX, int distanceY) {
    }

    private int findMax(int[] lastPositions) {
        int max = lastPositions[0];
        for (int value : lastPositions) {
            max = Math.max(max, value);
        }
        return max;
    }
}複製代碼

支持添加頭/底和空狀態

CommonAdapter中提供了RcvAdapterWrapper來作頭部、底部、空狀態的處理,方法也就是setXxx()。值得一提的是,當有頭部的時候,空狀態的view會自動佔用屏幕-頭部的空間,不會阻礙到頭部的顯示。

4、尾聲

用不用一個第三方庫我有下面的幾點建議:

  1. 若是你不瞭解其內部的實現,那麼儘量少用,由於出了問題無從查找。
  2. 若是你遇到一個很好的庫,不妨看下內部的實現,既能學到東西,又能夠在之後出問題的時候快速定位問題。
  3. 若是遇到複雜的庫,好比網絡和圖片庫。所有知道其原理是很難的,也須要成本,而你本身寫也是不現實的,因此須要挑選頗有名氣的庫來用。這樣即便遇到了問題,也會有不少資料能夠搜到。
  4. 不要抵觸國人的庫,國人的庫更加接地氣,說不定還更好,還能夠更加方便的提出issue。

探索無止境,優化沒底線,我仍是但願能有庫在庫中作好不少的優化操做,下降對程序員的要求,最終但願誰均可以寫代碼。簡單編程,快樂生活。本文的完成離不開朋友們的支持和幫助,感謝:MingleArch、豪哥的批評和建議。

developer-kale@foxmail.com

微博:@天之界線2010

參考文章:

相關文章
相關標籤/搜索