如何優(yōu)雅地實(shí)現(xiàn)Adapter多布局列表

前言

現(xiàn)在在實(shí)際開發(fā)中,越來越多的人選擇RecyclerView來實(shí)現(xiàn)列表布局,而RecyclerView寫多了,每次都要直接繼承Adapter實(shí)現(xiàn)onCreateViewHolderonBindViewHolder、getItemCount這三個(gè)方法,雖然代碼量不算很大,但每個(gè)XXXAdapter其實(shí)都長(zhǎng)得差不多,這種重復(fù)性的代碼,開發(fā)者是最不想寫的了,所以網(wǎng)上就出現(xiàn)了很多封裝Adapter的開源庫(kù)。所以本篇文章也介紹自己封裝的一個(gè)Adapter,幫你快速高效的添加一個(gè)列表(包括單Item列表和多Item列表)。

預(yù)覽

先簡(jiǎn)單看一下最終效果:

多Item列表

而Adapter的代碼量極少,感受一下:

public class MyMultiAdapter extends BaseMultiAdapter {

    @Override
    public void bind(BaseViewHolder holder, int layoutRes) {

    }
}

嗯,沒錯(cuò),你只需要實(shí)現(xiàn)bind方法就可以了,而bind方法是用來設(shè)置View的一些一次性設(shè)置的,例如開啟響應(yīng)點(diǎn)擊事件,長(zhǎng)按事件等。所以上面我就什么都沒寫。

總體思路

  • 實(shí)現(xiàn)一個(gè)通用的Adapter模版,避免寫Adapter中大量的重復(fù)代碼,抽象出幾個(gè)接口。
  • 通過讓數(shù)據(jù)類實(shí)現(xiàn)IMultiItem接口,把部分Adapter中的代碼轉(zhuǎn)移到具體的數(shù)據(jù)類中,而不用在Adapter去判斷數(shù)據(jù)類型和ViewType。這樣很容易添加新的Item(ViewType)類型,減少耦合,Adapter不用去感知IMultiItem的具體類型。
  • 高內(nèi)聚,低耦合,方便擴(kuò)展。
  • 封裝ViewHolder,將對(duì)View的常用操作都加上去。

實(shí)現(xiàn)

我們先看一下BaseViewHolderBaseViewHolder封裝了我們一些常用的操作,例如獲取子View,設(shè)置item的點(diǎn)擊事件,設(shè)置item的子View響應(yīng)點(diǎn)擊事件等。獲取子View我用了Object[]數(shù)組進(jìn)行緩存,沒有用SparseArray來緩存View,主要是我之前看了Agera的源碼,所以才用這種方式來緩存的,這里按下不表,下面是BaseAdapter的部分代碼:

public class BaseViewHolder extends RecyclerView.ViewHolder {

    private Object[] mIdsAndViews = new Object[0];

    /**
     * 設(shè)置響應(yīng)點(diǎn)擊事件,如果設(shè)置了clickable為true的話,在{@link BaseAdapter#setOnItemClickListener(OnItemClickListener)}
     * 中會(huì)得到響應(yīng)事件的回調(diào),詳情參考{@link BaseAdapter#setOnItemClickListener(OnItemClickListener)}
     * @param id 響應(yīng)點(diǎn)擊事件的View Id
     * @param clickable true響應(yīng)點(diǎn)擊事件,false不響應(yīng)點(diǎn)擊事件
     */
    public BaseViewHolder setClickable(@IdRes int id, boolean clickable){
        View view = find(id);
        if (view != null){
            if (clickable){
                view.setOnClickListener(mOnClickListener);
            }else{
                view.setOnClickListener(null);
            }
        }
        return this;
    }

    /**
     * 根據(jù)當(dāng)前id查找對(duì)應(yīng)的View控件
     * @param viewId View id
     * @param <T> 子View的具體類型
     * @return 返回當(dāng)前id對(duì)應(yīng)的子View控件,如果沒有,則返回null
     */
    @CheckResult
    public <T extends View> T find(@IdRes int viewId){
        int indexToAdd = -1;
        for (int i = 0; i < mIdsAndViews.length; i+=2) {
            Integer id = (Integer) mIdsAndViews[i];
            if (id != null && id == viewId){
                return (T) mIdsAndViews[i+1];
            }

            if (id == null){
                indexToAdd = i;
            }
        }

        if (indexToAdd == -1){
            indexToAdd = mIdsAndViews.length;
            mIdsAndViews = Arrays.copyOf(mIdsAndViews,
                    indexToAdd < 2 ? 2 : indexToAdd * 2);
        }

        mIdsAndViews[indexToAdd] = viewId;
        mIdsAndViews[indexToAdd+1] = itemView.findViewById(viewId);
        return (T) mIdsAndViews[indexToAdd+1];
    }
}

接下來我們來看一下BaseMultiAdapter里面做了什么?

public abstract class BaseMultiAdapter extends BaseAdapter<IMultiItem> {

    @Override
    public int getLayoutRes(int index) {
        final IMultiItem data = mData.get(index);
        return data.getLayoutRes();
    }

    @Override
    public void convert(BaseViewHolder holder, IMultiItem data, int index) {
        data.convert(holder);
    }
}

是不是發(fā)現(xiàn)這里面也很少代碼,因?yàn)楹艽笠徊糠执a都在BaseAdapter中實(shí)現(xiàn)了,
這里我們發(fā)現(xiàn)了一個(gè)IMultiItem,我們看一下它倆的源代碼:

public interface IMultiItem {

    /**
     * 不同類型的item請(qǐng)使用不同的布局文件,
     * 即使它們的布局是一樣的,也要copy多一份出來。
     * @return 返回item對(duì)應(yīng)的布局id
     */
    @LayoutRes int getLayoutRes();

    /**
     * 進(jìn)行數(shù)據(jù)處理,顯示文本,圖片等內(nèi)容
     * @param holder Holder Helper
     */
    void convert(BaseViewHolder holder);

    /**
     * 在布局為{@link android.support.v7.widget.GridLayoutManager}時(shí)才有用處,
     * 返回當(dāng)前布局所占用的SpanSize
     * @return 如果返回的SpanSize <= 0 或者 > {@link GridLayoutManager#getSpanCount()}
     *  則{@link BaseAdapter} 會(huì)在{@link BaseAdapter#onAttachedToRecyclerView(RecyclerView)}
     *  自適應(yīng)為1或者{@link GridLayoutManager#getSpanCount()},詳情參考{@link BaseAdapter#onAttachedToRecyclerView(RecyclerView)}
     */
    int getSpanSize();
}


public abstract class BaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {

    protected final List<T> mData = new ArrayList<>();

    private BaseViewHolder.OnItemClickListener mOnItemClickListener;

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int layoutRes) {
        BaseViewHolder baseViewHolder = new BaseViewHolder(LayoutInflater.from(parent.getContext())
                    .inflate(layoutRes, parent, false));
        bindData(baseViewHolder,layoutRes);
        return baseViewHolder;
    }

    @Override
    public final void onBindViewHolder(BaseViewHolder holder, int position) {
        //數(shù)據(jù)布局
        final T data = mData.get(position);
        convert(holder, data, position);
    }

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

    @Override
    public int getItemViewType(int position) {
        return getLayoutRes(position);
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if (manager == null || !(manager instanceof GridLayoutManager)) return;
        final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
        gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                final T data = getData(position);
                if (data != null && data instanceof IMultiItem){
                    int spanSize = ((IMultiItem)data).getSpanSize();
                    return spanSize <= 0 ? 1 :
                            spanSize > gridLayoutManager.getSpanCount()?
                            gridLayoutManager.getSpanCount():spanSize;
                }
                return 1;
            }
        });
    }

    protected void bindData(BaseViewHolder baseViewHolder, int layoutRes) {
        baseViewHolder.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(@NonNull View view, int adapterPosition) {
                if (mOnItemClickListener != null){
                    mOnItemClickListener.onItemClick(view, adapterPosition);
                }
            }
        });
       
        bind(baseViewHolder, layoutRes);
    }
    /**
     * 返回布局layout
     */
    @LayoutRes
    public abstract int getLayoutRes(int index);

    /**
     * 在這里設(shè)置顯示
     */
    public abstract void convert(BaseViewHolder holder, T data, int index);

    /**
     * 開啟子view的點(diǎn)擊事件,或者其他監(jiān)聽
     */
    public abstract void bind(BaseViewHolder holder,int layoutRes);
}

看到這里我們就能發(fā)現(xiàn)了,BaseAdapter已經(jīng)寫了大部分的代碼,就留下getLayoutRes,convert,bind給子類去實(shí)現(xiàn),而它的子類BaseMultiAdapter直接把getLayoutResconvert丟給了IMultiItem去實(shí)現(xiàn)。
getLayoutRes是返回item對(duì)應(yīng)的布局文件id,同時(shí)它在BaseAdapter也作為ViewType來使用,所以如果是不同類型的item,不建議共用同個(gè)布局文件。
所以,我們的數(shù)據(jù)類只要實(shí)現(xiàn)IMultiItem接口即可,例如上面的文本類item:

public class Text implements IMultiItem{
    public String mText;

    private int mSpanSize;

    public Text(String text,int spanSize) {
        mText = text;
        mSpanSize = spanSize;
    }

    @Override
    public int getLayoutRes() {
        return R.layout.item_text;
    }

    @Override
    public void convert(BaseViewHolder holder) {
        holder.setText(R.id.text,mText);
    }

    @Override
    public int getSpanSize() {
        return mSpanSize;
    }
}

getLayoutResconvert交給IMultiItem處理的好處就是實(shí)現(xiàn)多布局列表變得很簡(jiǎn)單,數(shù)據(jù)各自對(duì)應(yīng)自己的布局文件,自己在convert方法中顯示數(shù)據(jù)。

源碼

上面的具體全部代碼在都在我的開源庫(kù)里,一個(gè)封裝了RecyclerView.Adapter一些常用功能的庫(kù):SherlockAdapter
文章寫得有點(diǎn)簡(jiǎn)單了點(diǎn),更好的學(xué)習(xí)方式是閱讀源碼,如果您喜歡的話,給我的github加個(gè)star吧,或者能提出建議更好。

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

推薦閱讀更多精彩內(nèi)容