快速開發偷懶必備,搞定全部ViewGroup的Adapter . 支持自定義ViewGroup

概述

開發中,常常會用到動態在ScrollView、LinearLayout裏addView的事,尤爲是ItemView同樣時,每次都要寫一大堆代碼 inflater 動態addView,很煩。javascript

還有就是在嵌套ListView、ScrollView時,想採用LinearLayout替代(這樣性能更佳,不明白的看一個控件搞定嵌套ListView),但動態addView步驟神煩。java

這個時候就開始期待,能不能有一種快速爲任意ViewGroup添加子View的東西。git

我以前爲此事特地寫過一篇LinearLayout封裝博文,封裝了一個控件使用。試圖一個控件搞定嵌套ListView。可是後來發現,採用繼承某個ViewGroup作這個事情不夠優雅 ,對代碼有侵入性,若是有其餘ViewGroup須要動態addView,就會寫重複的代碼github

前幾天有人在羣裏問,如何方便的給ScrollView動態添加不一樣種類型的childView,相似RecyclerView那樣。我以前的封裝因爲內部有一個簡單的重用機制,只支持單一ItemType,也不支持多種類型的childView編程

那麼需求就來了:設計模式

  • 快速簡單使用
  • 支持任意ViewGroup
  • 無耦合
  • 無侵入性
  • Item支持多種類型

除此以外,我還加入:緩存

  • 爲ItemView設置OnItemClickListener
  • 爲ItemView設置OnItemLongClickListener

本文就封裝了這麼一個東西。數據結構

核心:ide

  • 利用Adapter模式封裝getView的操做
  • 搭配一個工具類,爲全部ViewGroup addView。
  • 再封裝出兩個使用快速簡單的Adapter 分別用於添加 單一Item佈局、多種Item佈局。

PS:因此本文也算是填了以前的一個坑,在以前適配器模式博文文末,我就提到要寫一篇爲流式佈局增長Adapter的文章,做爲Adapter的實戰演練。使用本文封裝的Adapter天然能夠達到這一點。函數

因爲採用Adapter隔離ViewGroup和ItemView,在切換ViewGroup時,十分方便。
如:在需求讓你把一個HorizontalScrollView包裹的水平標籤轉換成流式佈局時,只須要在xml替換控件便可。Adapter將自動完成適配的工做。其餘代碼一句不用修改。

不BB了,先看看之後如何使用吧,夠不夠簡單粗暴

轉載請標明出處:
gold.xitu.io/post/584d52…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/a…

使用預覽

單一Item類型:

Adapter泛型傳入JavaBean,構造函數傳入數據集和layout佈局,一句代碼搞定:

//單一ItemView
        ViewGroupUtils.addViews(mLinearLayout, new SingleAdapter<TestBean>(this, mDatas, R.layout.item_test) {
            @Override
            public void onBindView(ViewGroup parent, View itemView, TestBean data, int pos) {
                Glide.with(LinearLayoutActivity.this)
                        .load(data.getAvatar())
                        .into((ImageView) itemView.findViewById(R.id.ivAvatar));
                ((TextView) itemView.findViewById(R.id.tvName)).setText(data.getName());
            }
        });複製代碼

效果:


之前會用ScrollView嵌套ListView,如今只要用ScrollView套LinearLayout便可,性能更佳。

多種Item類型:

多種Item類型分兩種狀況:

數據結構相同:

數據結構相同依然能夠給Adapter傳入泛型,避免強轉:

//多種ItemViewType,可是數據結構相同,能夠傳入數據結構泛型,避免強轉
        ViewGroupUtils.addViews(linearLayout, new MulTypeAdapter<MulTypeBean>(this, initDatas()) {
            @Override
            public void onBindView(ViewGroup parent, View itemView, MulTypeBean data, int pos) {
                ((TextView) itemView.findViewById(R.id.tvWords)).setText(data.getName() + "");
                Glide.with(MulTypeActivity.this)
                        .load(data.getAvatar())
                        .into((ImageView) itemView.findViewById(ivAvatar));
            }
        });複製代碼

效果:

多種Item,數據結構相同。

數據結構不一樣:

若是數據結構不一樣,則不用傳入泛型,可是使用時須要強轉:

//多種Item類型:數據結構不一樣 不傳泛型了 使用時須要強轉javaBean,判斷ItemLayoutId
        ViewGroupUtils.addViews((ViewGroup) findViewById(R.id.activity_mul_type_mul_bean), new MulTypeAdapter(this, datas) {
            @Override
            public void onBindView(ViewGroup parent, View itemView, IMulTypeHelper data, int pos) {
                switch (data.getItemLayoutId()) {
                    case R.layout.item_mulbean_1:
                        MulBean1 mulBean1 = (MulBean1) data;
                        Glide.with(MulTypeMulBeanActivity.this)
                                .load(mulBean1.getUrl())
                                .into((ImageView) itemView);
                        break;
                    case R.layout.item_mulbean_2:
                        MulBean2 mulBean2 = (MulBean2) data;
                        TextView tv = (TextView) itemView;
                        tv.setText(mulBean2.getName());
                }
            }
        });複製代碼

數據結構:

public class MulBean1 implements IMulTypeHelper {
    private String url;
    @Override
    public int getItemLayoutId() {
        return R.layout.item_mulbean_1;
    }
}複製代碼
public class MulBean2 implements IMulTypeHelper {
    private String name;
    @Override
    public int getItemLayoutId() {
        return R.layout.item_mulbean_2;
    }
}複製代碼

Item1佈局是一個ImageView,Item2佈局是一個TextView
效果:

此次用橫向展現 多種Item,數據結構不一樣。

Item點擊事件

item的點擊和長按等事件,有兩種方法設置,這裏以點擊事件爲例,長按事件同理:

Adapter.onBindView()裏設置

Adapter.onBindView()方法裏能拿到ItemView,天然就能夠設置各類事件。相似RecyclerView。

在這裏設置優先級更高。緣由後文會提到。

@Override
            public void onBindView(ViewGroup parent, View itemView, final MulTypeBean data, int pos) {
                ....
                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Toast.makeText(mContext, "onBindView裏設置:文字是:" + data.getName(), Toast.LENGTH_SHORT).show();
                    }
                });
            }複製代碼

經過ViewGroupUtils設置

能夠在ViewGroupUtils.addViews直接做爲參數傳入.

也能夠用ViewGroupUtils.setOnItemClickListener()設置 。

優先級比Adapter.onBindView()裏設置低,緣由後文會提到。

//設置OnItemClickListener
        OnItemClickListener onItemClickListener = new OnItemClickListener() {
            @Override
            public void onItemClick(ViewGroup parent, View itemView, int position) {
                Toast.makeText(MulTypeActivity.this, "經過OnItemClickListener設置:" + position, Toast.LENGTH_SHORT).show();
            }
        };
        //能夠在`ViewGroupUtils.addViews`直接做爲參數傳入.\
        ViewGroupUtils.addViews(linearLayout, adapter ,onItemClickListener);
        //或者 也能夠用`ViewGroupUtils.setOnItemClickListener()`設置
        ViewGroupUtils.setOnItemClickListener(linearLayout,onItemClickListener);複製代碼

看起來仍是挺好的,嗯~至少我本身這麼以爲,我我的比較喜歡這種0耦合,每個庫都像可組裝拆卸的機關槍同樣,拿起來就用。而不是笨重功能繁多的重裝坦克。
搭配個人得意之做,每次必安利的史上集成最簡單側滑菜單控件
效果以下:

文首提到的,一開始是個水平ScrollView

替換成流式佈局

無特殊設置,僅僅替換ViewGroup爲流式佈局,替換Item根佈局爲我擼的側滑菜單庫,能感覺到這種0耦合的庫的魅力了麼。23333333 。

設計思路

下面就讓我手摸手帶你們實現它。
先看類圖。

UML類圖:

先簡要歸納

  • 咱們的頂層接口IViewGroupAdapter暴露出兩個方法供ViewGroup使用。

  • ViewGroupUtils 是爲任意ViewGroup 動態addView的工具類,只依賴於 IViewGroupAdapter 接口便可完成工做。

  • BaseAdapter是第二層,在這一層引入了數據集,用List<T>保存。實現IViewGroupAdapter的方法,重載一個三參數的getView()方法,供子類去實現。

  • SingleAdapter是第三層,一個簡化的Adapter,只支持單種Item,以LayoutId 構建View。實現getView()方法,並暴露出onBindView()供用戶快速使用。

  • MulTypeAdapter也同處第三層,一個支持多種Item的Adapter。依賴IMulTypeHelper接口,利用其getItemLayoutId() 方法去實現getView()方法,並暴露出onBindView()供用戶快速使用。

頂層接口設計

頂層接口,即IViewGroupAdapter

根據迪米特法則(最少知道原則),咱們應該抽象出一個頂層的接口,對ViewGroup暴露出最少的方法供使用。

咱們想一下,對於ViewGroup,它最少只須要哪些就能完成咱們的需求。

  • ChildView是什麼---> View
  • 有多少ChildView 須要 添加--->count
    因此,咱們的最頂層接口以下編寫:
public interface IViewGroupAdapter {
    /** * ViewGroup調用獲取ItemView * * @param parent * @param pos * @return */
    View getView(ViewGroup parent, int pos);

    /** * ViewGroup調用,獲得ItemCount * * @return */
    int getCount();
}複製代碼

ok,代碼寫到這裏,後面的咱們暫且不提,咱們就能夠寫動態addView的工具類了。由於咱們的ViewGroup依賴的全部信息都由IViewGroupAdapter這個接口提供了。

工具類

ViewGroupUtils 是爲任意ViewGroup 動態addView的工具類,不考慮點擊事件的狀況下,只依賴於 IViewGroupAdapter 接口便可完成工做。
以下編寫:

/** * 爲任意ViewGroup 添加ItemViews. * * @param viewGroup 必傳 * @param adapter 必傳,至少提供要add的View和須要add的count * @param removeViews 是否須要remove掉以前的Views */
    public static void addViews(final ViewGroup viewGroup, IViewGroupAdapter adapter
            , boolean removeViews) {
        if (viewGroup == null || adapter == null) {
            return;
        }
        //若是須要remove掉以前的Views
        if (removeViews && viewGroup.getChildCount() > 0) {
            viewGroup.removeAllViews();
        }
        //開始添加子Views,經過Adapter得到須要添加的Count
        int count = adapter.getCount();
        for (int i = 0; i < count; i++) {
            //經過Adapter得到ItemView
            View itemView = adapter.getView(viewGroup, i);
            viewGroup.addView(itemView);

        }
    }複製代碼

如此便可完成 動態給任意ViewGroup addView 的工做。

不過咱們開頭提過,我仍是想引入ItemView的點擊和長按事件的。可是關於這兩個ItemListener,仍是有一些東西要考慮的。

ItemListener的設計

爲ViewGroup提供OnItemClickListener,有個問題須要考慮:
若是使用者調用了setOnItemClickListener,且在Adapter裏本身又對ItemView設置了OnClickListener,那麼究竟該觸發哪一個Listener,即它們的優先級。

咱們不該該本身靠腦子想答案,仍是參照系統原有的設計比較好。既然ListView提供了OnItemClickListener,那麼咱們參照它的設計來就行。

先說結論:經過參照ListView的源碼,以及實驗得知,對ItemViewOnClickListener優先級 > ViewGroup的OnItemClickListener

爲何?答案在源碼中,感興趣看,不感興趣直接跳過。

從AbsListView的onTouchEvent()->onTouchUp()->PerformClick->performItemClick-> AdapterView.performItemClick() -> AdapterView.mOnItemClickListener,便可找到答案。

這裏不詳細分析源碼,不是本文重點,簡單的說,從入口處是AbsListView的onTouchEvent() ,咱們能夠知道,ListView自己並無干預ItemView的點擊事件(即沒有爲其設置OnClickListener),是在ItemView不消耗Touch事件時 才進行Item點擊事件的觸發。
所以若ItemView設置了OnClickListener,AbsListView的onTouchEvent()將收不到MotionEvent.ACTION_UP事件,於是也不會觸發OnItemClickListener。因此這決定了ItemViewOnClickListener優先級高。

還有一個問題,咱們能夠經過View.hasOnClickListeners()這個方法來判斷View是否設置了OnClickListener,可是這個方法在API15才加入,爲了能兼容低版本,我採用了另外一種方法判斷,itemView.isClickable(),若是true,我當作有點擊事件,若是false,我當作沒有。

其實在ListView中這個問題也是同樣的,itemView.isClickable()爲true的話,點擊事件就被攔截住,不會分發至AbsListView的onTouchEvent()裏了。因此咱們這麼寫是沒問題,而且是正確的。

ItemListener的完整實現:

既然如此,那麼咱們在程序中,應該以下編寫:

for (int i = 0; i < count; i++) {
            View itemView = adapter.getView(viewGroup, i);
            viewGroup.addView(itemView);
            //添加點擊事件
            if (null != onItemClickListener && !itemView.isClickable()) {
                final int finalI = i;
                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        onItemClickListener.onItemClick(viewGroup, view, finalI);
                    }
                });
            }
            //添加點擊事件
            if (null != onItemLongClickListener && !itemView.isLongClickable()) {
                final int finalI = i;
                itemView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View view) {
                        return onItemLongClickListener.onItemLongClick(viewGroup, view, finalI);
                    }
                });
            }
        }複製代碼

因此完整的addViews()以下:

/** * 爲任意ViewGroup 添加ItemViews. * * @param viewGroup 必傳 * @param adapter 必傳,至少提供要add的View和須要add的count * @param removeViews 是否須要remove掉以前的Views * @param onItemClickListener Item點擊事件 * @param onItemLongClickListener Item長按事件 */
    public static void addViews(final ViewGroup viewGroup, IViewGroupAdapter adapter
            , boolean removeViews
            , final OnItemClickListener onItemClickListener
            , final OnItemLongClickListener onItemLongClickListener) {
        if (viewGroup == null || adapter == null) {
            return;
        }
        //若是須要remove掉以前的Views
        if (removeViews && viewGroup.getChildCount() > 0) {
            viewGroup.removeAllViews();
        }
        //開始添加子Views,經過Adapter得到須要添加的Count
        int count = adapter.getCount();
        for (int i = 0; i < count; i++) {
            //經過Adapter得到ItemView
            View itemView = adapter.getView(viewGroup, i);
            viewGroup.addView(itemView);
            //添加點擊事件,itemView以前沒有點擊事件纔會去設置
            if (null != onItemClickListener && !itemView.isClickable()) {
                final int finalI = i;
                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        onItemClickListener.onItemClick(viewGroup, view, finalI);
                    }
                });
            }
            //添加長按事件itemView以前沒有長按事件纔會去設置
            if (null != onItemLongClickListener && !itemView.isLongClickable()) {
                final int finalI = i;
                itemView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View view) {
                        return onItemLongClickListener.onItemLongClick(viewGroup, view, finalI);
                    }
                });
            }
        }
    }複製代碼

實際中,咱們可能不須要設置Listener,爲了快速使用,我又提供了兩個重載方法:

/** * 爲任意ViewGroup 添加ItemViews. * 而且會清除掉以前全部add過的View * * @param viewGroup 必傳 * @param adapter 必傳,至少提供要add的View和須要add的count */
    public static void addViews(final ViewGroup viewGroup, IViewGroupAdapter adapter) {
        addViews(viewGroup, adapter, true, null, null);
    }

    /** * 爲任意ViewGroup 添加ItemViews. * 而且會清除掉以前全部add過的View * * @param viewGroup 必傳 * @param adapter 必傳,至少提供要add的View和須要add的count * @param onItemClickListener Item點擊事件 */
    public static void addViews(final ViewGroup viewGroup, IViewGroupAdapter adapter
            , final OnItemClickListener onItemClickListener) {
        addViews(viewGroup, adapter, true, onItemClickListener, null);
    }複製代碼

若不在addViews()裏設置ItemListener,也能夠經過setOnItemClickListener()setOnItemLongClickListener() 設置,不過這兩個方法必須在addViews()方法以後調用:

/** * 爲任意ViewGroup設置OnItemClickListener. * 該方法必須在addViews()方法以後調用,不然無效。 * 由於ItemView 必須被添加在ViewGroup裏才能遍歷到。 * 建議直接在addViews()方法裏傳入OnItemClickListener進行設置,性能更高 * * @param viewGroup * @param onItemClickListener */
    public static void setOnItemClickListener(final ViewGroup viewGroup, final OnItemClickListener onItemClickListener) {
        if (viewGroup == null || onItemClickListener == null) {
            return;
        }
        int childCount = viewGroup.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View itemView = viewGroup.getChildAt(i);
            //itemView以前沒有點擊事件纔會去設置
            if (null != itemView && !itemView.isClickable()) {
                final int finalI = i;
                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        onItemClickListener.onItemClick(viewGroup, itemView, finalI);
                    }
                });
            }
        }
    }


    /** * 爲任意ViewGroup設置OnItemLongClickListener. * 該方法必須在addViews()方法以後調用,不然無效。 * 由於ItemView 必須被添加在ViewGroup裏才能遍歷到。 * 建議直接在addViews()方法裏傳入OnItemLongClickListener進行設置,性能更高 * * @param viewGroup * @param onItemLongClickListener */
    public static void setOnItemLongClickListener(final ViewGroup viewGroup, final OnItemLongClickListener onItemLongClickListener) {
        if (viewGroup == null || onItemLongClickListener == null) {
            return;
        }
        int childCount = viewGroup.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View itemView = viewGroup.getChildAt(i);
            //itemView以前沒有長按事件纔會去設置
            if (null != itemView && !itemView.isLongClickable()) {
                final int finalI = i;
                itemView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View view) {
                        return onItemLongClickListener.onItemLongClick(viewGroup, itemView, finalI);
                    }
                });
            }
        }
    }複製代碼

Adapter

終於到了咱們的重頭戲,Adapter。

BaseAdapter

BaseAdapter是第二層,在這一層引入了數據集,用List<T>保存。實現IViewGroupAdapter的方法,重載一個三參數的getView()方法,供子類去實現。

它和咱們平時寫的ListView、RecyclerView的Adapter就比較像了,我也是參照平時的寫法。

核心就是實現IViewGroupAdaptergetView(ViewGroup parent, int pos)方法,增長一個數據,工做轉交給三參數的getView(ViewGroup parent, int pos, T data)方法。

子類應該 實現 getView(ViewGroup parent, int pos, T data)方法,在其中inflate or new 出 ItemView,並綁定數據

public abstract class BaseAdapter<T> implements IViewGroupAdapter {
    protected List<T> mDatas;
    protected Context mContext;
    protected LayoutInflater mInflater;

    /** * ViewGroup調用獲取ItemView,create bind一塊兒作 * * @param parent * @param pos * @return */
    @Override
    public View getView(ViewGroup parent, int pos) {
        return getView(parent, pos, mDatas.get(pos));
    }

    /** * 實際的createItemView的地方 * * @param parent * @param pos * @param data * @return */
    public abstract View getView(ViewGroup parent, int pos, T data);

    /** * ViewGroup調用,獲得ItemCount * * @return */
    @Override
    public int getCount() {
        return mDatas != null ? mDatas.size() : 0;
    }

}複製代碼

SingleAdapter

SingleAdapter是第三層,一個簡化的Adapter,只支持單種Item,以LayoutId 構建View。實現getView()方法,並暴露出onBindView()供用戶快速使用。

使用時,通常將數據結構的泛型傳入,配合構造函數傳入的ItemLayoutId使用。

它繼承自BaseAdapter,因此它了實現getView(ViewGroup parent, int pos, T data)方法。在根據傳入的itemLayoutId inflate出ItemView後,會回調onBindView(ViewGroup parent, View itemView, T data, int pos)方法,並返回ItemView供ViewGroup使用。

onBindView(ViewGroup parent, View itemView, T data, int pos)方法裏,咱們完成數據綁定的工做。例如加載圖片,也能夠設置點擊事件。

public abstract class SingleAdapter<T> extends BaseAdapter<T> {

    private int mItemLayoutId;

    @Override
    public View getView(ViewGroup parent, int pos, T data) {
        //實現getView
        View itemView = /*onCreateView(parent, pos)*/mInflater.inflate(mItemLayoutId, parent, false);
        onBindView(parent, itemView, data, pos);
        return itemView;
    }

    /** * 暴漏這個 讓外部bind數據 * * @param parent * @param itemView * @param data * @param pos */
    public abstract void onBindView(ViewGroup parent, View itemView, T data, int pos);

}複製代碼

MulTypeAdapter

MulTypeAdapter也同處第三層,一個支持多種Item的Adapter。依賴IMulTypeHelper接口,利用其getItemLayoutId() 方法去實現getView()方法,並暴露出onBindView()供用戶快速使用。

public abstract class MulTypeAdapter<T extends IMulTypeHelper> extends BaseAdapter<T> {

    @Override
    public View getView(ViewGroup parent, int pos, T data) {
        View itemView = mInflater.inflate(data.getItemLayoutId(), parent, false);
        onBindView(parent, itemView, data, pos);
        return itemView;
    }

    /** * 暴漏這個 讓外部bind數據 * * @param parent * @param itemView * @param data * @param pos */
    public abstract void onBindView(ViewGroup parent, View itemView, T data, int pos);
}複製代碼

IMulTypeHelper接口,一樣簡單:

public interface IMulTypeHelper {
    int getItemLayoutId();
}複製代碼

總結

代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/a…

由於想支持任意ViewGroup,對ViewGroup代碼無侵入性,於是對部分功能進行了取捨,例如設置OnItemXXXListener,若是採用繼承ViewGroup,嵌入代碼,能夠作到不強制addViews()setOnItemClickListener()順序。

經過上文咱們能感覺到一些面向接口編程的奧義,例如咱們只須要設計好頂層的接口IViewGroupAdapter,在沒寫剩下的xxxAdapter時,工具類均可以寫好,編譯經過。
這在之後擴展時,例如,我數據集不能用List來保存了,我多是多個List or Map 等等,只須要實現IViewGroupAdapter接口便可定製一個Adapter。其餘代碼不需任何修改。

有人說過,設計模式怎麼學,就是先學完一遍全部的設計模式。而後再所有忘掉他們,只要記住SOLID原則便可。
我以爲頗有道理,就像我以前一直在糾結代理模式和裝飾者模式的區別,後來我想,編碼時,我管它是什麼模式,只要寫出來的是易維護的代碼便可。

to do list

  • 考慮加入複用緩存池
  • 考慮替換onBindView()ItemView->通用的ViewHolder,這樣能夠少寫一些findViewById()代碼
  • 整合DataBinding 的通用Adapter入庫。
  • 整合 RecyclerView、ListView的通用Adapter入庫。
  • 加入一些自定義ViewGroup入庫,例如流式佈局,九宮格,Banner輪播圖。

下文預告:

一個用ScrollView很容易實現,但RecyclerView、ListView就沒法實現的動畫效果,仿淘寶會員中心等級動畫。凸顯爲全部ViewGroup增長Adapter模式的重要性。

DataBinding篇隆重登場

感興趣能夠去閱讀
gold.xitu.io/post/584fbd…

轉載請標明出處:
gold.xitu.io/post/584d52…
本文出自:【張旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代碼傳送門:喜歡的話,隨手點個star。多謝
github.com/mcxtzhang/a…

相關文章
相關標籤/搜索