少年击剑更吹箫,剑气箫心一例消。这篇文章主要讲述Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup相关的知识,希望能为你提供帮助。
转载请标明出处:概述 开发中, 经常会用到动态在ScrollView、LinearLayout里addView的事, 尤其是ItemView一样时, 每次都要写一大堆代码 inflater 动态addView, 很烦。
http://blog.csdn.net/zxt0601/article/details/53576092
本文出自:【张旭童的博客】(http://blog.csdn.net/zxt0601)
代码传送门: 喜欢的话, 随手点个star。多谢
https://github.com/mcxtzhang/all-base-adapter
还有就是在嵌套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了, 先看看以后如何使用吧, 够不够简单粗暴。
使用预览 单一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 onItemClickListenerItem点击事件
* @
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。多谢
https://github.com/mcxtzhang/all-base-adapter
因为想支持任意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轮播图。
DataBinding篇隆重登场 感兴趣可以去阅读
http://blog.csdn.net/zxt0601/article/details/53618694
【Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup】转载请标明出处:
http://blog.csdn.net/zxt0601/article/details/53576092
本文出自:【张旭童的博客】(http://blog.csdn.net/zxt0601)
代码传送门: 喜欢的话, 随手点个star。多谢
https://github.com/mcxtzhang/all-base-adapter
推荐阅读
- 安卓ListView 如何设置item的高度
- Android WebView执行GPU命令的过程分析
- Android碎片Fragment总结
- Android实现多点触控,自由缩放图片
- Android自定义组件
- unity发布安卓 截图保存到本地
- Android种使用Notification实现通知管理以及自定义通知栏(示例四)
- 微信小程序wx.uploadFile在安卓手机上面the same task is working问题解决
- cocos2dx的android版FileUtils的坑