概述
開發中,經常會用到動態在ScrollView、LinearLayout裡addView的事,尤其是ItemView一樣時,每次都要寫一大堆程式碼 inflater 動態addView,很煩。
還有就是在巢狀ListView、ScrollView時,想採用LinearLayout替代(這樣效能更佳,不明白的看一個控制元件搞定巢狀ListView),但動態addView步驟神煩。
這個時候就開始期待,能不能有一種快速為任意ViewGroup新增子View的東西。
我之前為此事特意寫過一篇LinearLayout封裝博文,封裝了一個控制元件使用。試圖一個控制元件搞定巢狀ListView。但是後來發現,採用繼承某個ViewGroup做這個事情不夠優雅 ,對程式碼有侵入性,如果有其他ViewGroup需要動態addView,就會寫重複的程式碼 。
前幾天有人在群裡問,如何方便的給ScrollView動態新增不同種型別的childView,類似RecyclerView那樣。我之前的封裝由於內部有一個簡單的重用機制,只支援單一ItemType,也不支援多種型別的childView。
那麼需求就來了:
- 快速簡單使用
- 支援任意ViewGroup
- 無耦合
- 無侵入性
- Item支援多種型別
除此之外,我還加入:
- 為ItemView設定
OnItemClickListener
- 為ItemView設定
OnItemLongClickListener
本文就封裝了這麼一個東西。
核心:
- 利用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型別:資料結構不同 不傳泛型了 使用時需要強轉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的點選和長按等事件,有兩種方法設定,這裡以點選事件為例,長按事件同理:
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耦合,每一個庫都像可組裝拆卸的機關槍一樣,拿起來就用。而不是笨重功能繁多的重灌坦克。
搭配我的得意之作,每次必安利的史上整合最簡單側滑選單控制元件。
效果如下:
無特殊設定,僅僅替換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的原始碼,以及實驗得知,對ItemView
的OnClickListener
優先順序 > 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
。所以這決定了ItemView
的OnClickListener
優先順序高。
還有一個問題,我們可以通過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就比較像了,我也是參照平時的寫法。
核心就是實現IViewGroupAdapter
的getView(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…