百行之內——超輕量級的多類型列表視圖框架

名字有點唬人,其實就是組合了幾個封裝類可以方便實現RecyclerView的多視圖,畢竟「框架」這個詞在我看來仍是指具備必定規模量級及重點技術的代碼體系,但僅就解決特定問題而言也不妨被冠以這個名號。同時它真的是「超輕量」總共不過4個類,不超過130行代碼~java

視圖抽象

咱們已經有了一個無需類型強轉的通用ViewHolder(ItemViewHolder),一個ViewHolder對象能夠找到全部視圖實例。並且它是徹底獨立的, 沒有引入任何自定義類或者任何第三方依賴;即便沒有這個「框架」,也徹底能夠拆出來用在其餘地方。android

控件適配器

適配器(Adapter)是與控件關聯的, 是控件對其子視圖列表的一種抽象。抽象了什麼?由具體定義決定。好比列表控件的適配器(不管是之前的ListView, 如今RecyclerView, 以及其它的如ViewPager)通常抽象了三個屬性:git

  1. 數量 getItemCount()
  2. 操做 onBindView(ViewHolder holder, int position),onCreateView
  3. 類型 getViewType(int position)

控件適配是SDK關聯的,框架的ItemAdapter也是基於RecyclerView.Adaptergithub

元素抽象

適配器(Adapter) 是容器控件對子控件的總體抽象,相應位置的元素沒有做出任何限制,position對應的元素能夠是接口返回的一個具體數據,也能夠是從本地獲取的應用數據。框架要作的一個工做就是對元素數據類型進行抽象, 可是數據類型千差萬別,沒法對數據元素自己的屬性作統一操做,結果就是變成像MultiType庫那樣,用範型抽象全部的數據元素,而後經過註冊數據類型(.class)到數據綁定器類型(ItemViewBinder.class)的映射,反射獲得綁定器實例,其中有大量的對象類型強轉。微信

框架不對數據元素作抽象,而是針對操做做抽象,即adapter對每一個position元素的操做做抽象;用一個簡單的List數據結構持有抽象實例;由於一樣有綁定操做,因此姑且也叫作綁定器ItemBinder。ViewHolder就用咱們以前的通用ViewHolder(ItemViewHolder),結合前面說到adapter有三個重要屬性,因而有:數據結構

public interface ItemBinder {
    void onBindViewHolder(ItemViewHolder holder, int position);
    int getViewType();
}

public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
    private final List<ItemBinder> mBinders = new ArrayList<>(10);

    @Override
    public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
        ItemBinder binder = mBinders.get(position);
        binder.onBindViewHolder(holder, position);
    }

    @Override
    public int getItemCount() {
        return mBinders.size();
    }

    @Override
    public int getItemViewType(int position) {
        return mBinders.get(position).getViewType();
    }

    public void setBinders(List<ItemBinder> binders) {
        mBinders.clear();
        mBinders.addAll(binders);
    }
}
複製代碼

對於Adapter而言,元素僅僅是ItemBinder,它不關心ItemBinder是用哪一種數據類型,又是怎樣把數據填充到ViewHolder中。app

視圖類型

RecyclerView經過RecyclerView.AdaptergetItemViewType接口返回的數值來標識一個視圖類型。與ListView不一樣的是這個viewType能夠不是連續的RecyclerView能夠本身感知設置了多少種viewType(內部其實就是用了SparseArray)。經過viewType的標識, RecyclerView.AdapteronCreateViewHolder來建立相應的視圖類型。一般咱們不得不本身創建viewTypeRecyclerView.ViewHolder的映射關係,除了稍有點煩瑣以外並無多大的問題。框架

注意:咱們走到了到框架的一個關鍵點,就是創建viewType和視圖實例建立之間的關係。ide

已經找不到是在哪一個庫裏,當看到把視圖資源id(layoutId)直接做爲viewType返回的時候,被這種天才想法折服了。首先就是用資源id自己就能夠建立視圖;其次是充分利用了viewType能夠不連續的性質;再次是不一樣的資源id自然的對應不一樣的視圖類型,也就是說,自己就是多視圖類型的;最後的最後就是這種實現提供了巨大的靈活性,包括代碼複用和資源的複用,這點後面專門說一下。因而有:函數

public interface ItemBinder {
    void onBindViewHolder(ItemViewHolder holder, int position);

    @LayoutRes
    int getLayoutId();
}

public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
    private final List<ItemBinder> mBinders = new ArrayList<>(10);

    @NonNull
    @Override
    public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup container, int viewType) {
        return new ItemViewHolder(LayoutInflater.from(container.getContext()).inflate(
                viewType, container, false));
    }

    @Override
    public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
        ItemBinder binder = mBinders.get(position);
        binder.onBindViewHolder(holder, position);
    }

    @Override
    public int getItemCount() {
        return mBinders.size();
    }

    @Override
    public int getItemViewType(int position) {
        return mBinders.get(position).getLayoutId();
    }

    public void setBinders(List<ItemBinder> binders) {
        mBinders.clear();
        mBinders.addAll(binders);
    }
}
複製代碼

咱們以前被getItemViewType的默認值0給誤導了,思惟慣性讓咱們認爲viewType能夠和ViewHolder是割裂的,但其實它們能夠是統一的!

剩下的工做簡單明瞭,實現具體的ItemBinder類型,將具體的數據填充到視圖,好比:

public HomeBannerBinder implements ItemBinder {
    private final HomeBanner mItem;
    HomeBannerBinder(HomeBanner banner) {
        mItem = banner;
    }

    void onBinderViewHolder(ItemViewHolder holder, int position) {
        ImageView bg = holder.findViewById(R.id.background);
        if (bg != null) {
            ImageManager.load(bg, mItem.bg_url);
        }
    }
}
複製代碼

靈活複用

這裏的複用不是recyclerView對視圖內存對象的複用,而是代碼層面的複用,包括聲明資源的xml代碼。

把layoutId做爲viewType到底帶來怎樣的靈活複用呢?

能夠先舉例常見的微信朋友圈列表:顯然,不少朋友圈內容都是不一樣的,有視頻有圖片有文本,或者它們的結合,處理2張圖片的佈局和處理9張圖片的佈局顯示也是不一樣的;可是每一條朋友圈佈局有不少相同的地方:都有頂部的用戶頭像與用戶名稱,都有底部點贊和評論佈局。那麼問題來了:怎樣聲明不一樣的視圖類型,但沒必要重複書寫這些同樣的地方?

這固然不是難事,好比一個視頻朋友圈佈局可寫成這樣circle_item_video.xml

<RelativeLayout>
     <include layout="@layout/circle_item_top" />
     <include layout="@layout/circle_item_layer_video" />
     <include layout="@layout/circle_item_bottom" />
</RelativeLayout>
複製代碼

音頻朋友圈佈局circle_item_audio.xml就把@layout/circle_item_layer_video換成@layout/circle_item_layer_audio,依次類推。

這麼作徹底能夠實現,隨着類型的增多,佈局文件相應增長便可;然而一旦發生變動呢?只要涉及相同佈局的部分都必須改一遍!(好比把RelativeLayout變成android.support.constraint.ConstraintLayout)並且實際的狀況不必定這麼簡單,可能由於各類緣由視圖的層次比較深,而且都沒辦法放在include中,一旦視圖對象變多,視圖層次變深, 這種冗餘就讓人難以忍受了,對一個有追求的碼畜來講,確定但願只更改一處地方便可。

視圖複用

若是layoutId做爲viewType要如何實現剛纔的複用呢?顯然他們必須是不一樣的viewType(若是同樣會發生什麼?),那麼他們固然是不一樣的layoutId,但不一樣的layoutId就沒法避免上面那樣的問題,這時候就用到android的匿名資源(anonymous),就是對一個資源聲明一個引用,而這個引用自己做爲一個資源,即<item name="def" type="drawable">@drawable/abc</item>,結合以上的例子就是 circle_item.xml:

<RelativeLayout>
     <include layout="@layout/circle_item_top" />
     <ViewStub />
     <include layout="@layout/circle_item_bottom" />
</RelativeLayout>
複製代碼

中間的部分可經過延遲加載的方式設置成不一樣的View,甚至全部不一樣的部分均可以以ViewStub的形式嵌在佈局當中。 refs.xml:

<resources>
    <item name="circle_item_video" type="layout">@layout/circle_item</item>
    <item name="circle_item_audio" type="layout">@layout/circle_item</item>
    <item name="circle_item_pic_1" type="layout">@layout/circle_item</item>
    <item name="circle_item_pic_9" type="layout">@layout/circle_item</item>
</resources>
複製代碼

也就是說都引用同一份的佈局資源!可他們由於不一樣的layoutId進而能夠被recyclerView看成不一樣的viewType

代碼複用

按照以前的思路也必然但願只在一處更改點贊和評論功能。因此有一個基類:

public class CircleItemBinder implements ItemBinder {
    @Override
    public getLayoutId() {
        return R.layout.circle_item;
    }

    @Override
    void onBindViewHolder(ItemViewHolder holder, int position) {
        bindComment(holder);
        bindLike(holder);
    }

    private void bindComment(ItemViewHolder holder) {
    }

    private void bindLike(ItemViewHolder holder) {
    }
}
複製代碼

各種型的binder相似:

public class CircleVideoBinder extends CircleItemBinder {
    private final YourVideoData mItem;

    public CircleVideoBinder(YourVideoData data) {
        mItem = data;
    }

    @Override
    public getLayoutId() {
        return R.layout.circle_item_video;
    }

    @Override
    void onBindViewHolder(ItemViewHolder holder, int position) {
        super.onBindViewHolder(holder, position);
        TextView title = holder.findViewById(R.id.video_title);
        if (title != null) {
            title.setText(mItem.title);
        }
        ...
    }
}

public class CircleAudioBinder extends CircleItemBinder {
    private final YourAudioData mItem;

    public CircleAudioBinder(YourAudioData data) {
        mItem = data;
    }

    @Override
    public getLayoutId() {
        return R.layout.circle_item_audio;
    }

    @Override
    void onBindViewHolder(ItemViewHolder holder, int position) {
        super.onBindViewHolder(holder, position);
        ImageView album = holder.findViewById(R.id.audio_album);
        if (album != null) {
            ImageLoader.load(album, mItem.album_background);
        }
        ...
    }
}
複製代碼

點贊和評論功能的代碼就可徹底複用!這一切只是用了layoutId做爲了viewType! 至此,框架的全貌已呈現:

public interface ItemBinder {
    @LayoutRes
    int getLayoutId();

    void onBindViewHolder(ItemViewHolder holder, int position);
}

public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
    private final List<ItemBinder> mBinders = new ArrayList<>(10);

    @NonNull
    @Override
    public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup container, int viewType) {
        return new ItemViewHolder(LayoutInflater.from(container.getContext()).inflate(
                viewType, container, false));
    }

    @Override
    public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
        ItemBinder binder = mBinders.get(position);
        binder.onBindViewHolder(holder, position);
    }

    @Override
    public int getItemCount() {
        return mBinders.size();
    }

    @Override
    public int getItemViewType(int position) {
        return mBinders.get(position).getLayoutId();
    }

    public void setBinders(List<ItemBinder> binders) {
        mBinders.clear();
        appendBinders(binders);
    }
}
複製代碼

咱們以前的通用ViewHolder也羅列在這裏:

public class ItemViewHolder extends RecyclerView.ViewHolder {
    private final SparseArrayCompat<View> mCached = new SparseArrayCompat<>(10);

    public ItemViewHolder(View itemView) {
        super(itemView);
    }

    public <T extends View> T findViewById(@IdRes int resId) {
        int pos = mCached.indexOfKey(resId);
        View v;
        if (pos < 0) {
            v = itemView.findViewById(resId);
            mCached.put(resId, v);
        } else {
            v = mCached.valueAt(pos);
        }
        @SuppressWarnings("unchecked")
        T t = (T) v;
        return t;
    }
}
複製代碼

通常都還要定義一個基礎類ItemBaseBinder,全部派生類的可能會共享某個操做, 這個基礎類接收資源id做爲構造函數參數:

public class ItemBaseBinder implements ItemBinder {
    private final int mLayoutId;

    public ItemBaseBinder(@layoutRes int layoutId) {
        mLayoutId = layoutId;
    }

    @Override
    public void onBindViewHolder(ItemViewHolder holder, int position) {
    }

    @Override
    public int getLayoutId() {
        return mLayoutId;
    }
}
複製代碼

其他的工做就只是派生具體的業務類了,就像以前舉例那樣!這一切不過130行代碼!

MutiType的差別

實例一對一

MutiType庫一樣有綁定器ItemViewBinder但注意他的綁定是隻有一個實例,而咱們的ItemAdapter是把綁定器做爲元素對象,一個數據對應一個綁定器因此他有多個實例,實際上這個綁定器是對數據的抽象。

無類型轉換無反射操做

真的,MutiType把這一切搞的太複雜了!可悲的是還有不少人在用……

結語

有了這個框架,靈活性不只一點沒有損失,並且更加簡潔,MutiType那坨類型強轉和反射操做能夠進博物館了。

一大篇說下來有點累贅,直接上代碼就能看明白的,關鍵是思考的過程與解決問題的思路。全部的框架到底解決了什麼問題,這纔是最須要了解和學習的,不然框架是學不完的。而一旦咱們有了思路與目標,實現一個框架也並非難事。這套小框架實踐已經很長時間了,能夠覆蓋絕大多數狀況,效果出奇的好,比MutiType那坨「不知道高到哪裏去了」。

須要注意的有2點

  1. onBindViewHolder方法只作數據填充不該該作數據處理 這點其實和框架沒有關係,照樣仍是有許多人在Adapter的onBindViewHolder作着數據處理
  2. 動態的更換視圖類型 由於方法getLayoutId是接口,意味着在運行時能夠返回不一樣的layoutId,從而動態的更改視圖類型,不過須要與Adatper的notifyItemChanged配合使用
  3. 外部通知更新 ItemAdapter.setBinders的方法實現體在更新了實例後沒有調用notifyDataSetChanged, 這個操做應該由外部決定,雖然此處是必要的,但很容易形成冗餘的更新。

擴展

框架也很是容易根據具體的須要和場景進行擴展。

嵌套

列表嵌套列表的狀況下,要如何抽象呢,其實只要對應視圖就行。最外層的列表(一級列表)有一個特殊ItemBinder類型,這個類型自己也能夠持有多個ItemBinder提供給內層列表(二級列表):

public class ItemContainerBinder extends ItemBaseBinder {
    private final ItemAdapter mAdapter = new ItemAdapter();

    @Override
    public void onBinderViewHolder(ItemViewHolder holder, int position) {
        RecyclerView secondary = holder.findViewById(R.id.secondary);
        if (secondary != null) {
            if (secondary.getAdapter() != mAdapter) {
                secondary.setAdapter(mAdapter);
            }
            if (secondary.getLayoutManager() == null) {
                secondary.setLayoutManager(new LinearLayoutManager(secondary.getContext());
            }
        }
    }

    public void setBinders(List<ItemBinder> binders) {
        mAdapter.setBinders(binders);
    }
...
}
複製代碼

在這裏還能夠利用之前提過的重用LayoutManager

局部更新

在運行過程當中只須要更新列表某一項的狀況其實很是常見,不少時候不能只經過調用視圖對象的方法來直接更新視圖,還要調用Adapter.notifyItemChanged(像前文所提的動態更新列表視圖類型)。也就是Adapter持有ItemBinder,而ItemBinder須要再調用Adapter的方法,若是再讓ItemBinder去引用Adapter,這種強耦合必然不是一個好的設計。

針對這個框架的實現,這時候首先須要將ItemBinder內部的變化通知出來,可是通知的時機應該由ItemBinder實現體來決定,外部去被動響應。這固然是最簡單的觀察者模式了,因而有:

public interface ItemBinder {
...
    void setOnChangeListener(OnChangeListener listener);

    interface OnChangeListener {
        void onItemChanged(ItemBinder item, int payload);
    }
}

public class ItemBaseBinder implements ItemBinder {
...
    private OnChangeListener mChangeListener;

    @Override
    publi final void setChangeListener(OnChangeListener listener) {
        mChangeListener = listener;
    }

    public final void notifyItemChange(int payload) {
        if (mChangeListener != null) {
            mChangeListener.onItemChanged(this, payload);
        }
    }
}
複製代碼

這裏的payload借鑑了RecyclerView.Adapter,只不過類型由Object變成了int,表明了局部更新須要攜帶的信息。在ItemBinder實現體內部,由於某項數據變動須要通知到外部就只需調用notifyItemChange方法,將變動傳遞出去,由外部做出具體響應:

List<ItemBinder> binders = new ArrayList<>();
...
ItemBinder special = new XXXYYYBinder(...);
specail.setChangeListener(new ItemBinder.OnChangeListener() {
    @Override
    public void onItemChanged(ItemBinder item, int payload) {
        int pos = mAdapter.indexOf(item);
        if (pos >= 0) {
            mAdapter.notifyItemChanged(pos,...);
        }
    }
});
binders.add(special);
...
mAdapter.setBinders(binders);
複製代碼
相關文章
相關標籤/搜索