Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup

少年击剑更吹箫,剑气箫心一例消。这篇文章主要讲述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
概述 开发中, 经常会用到动态在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了, 先看看以后如何使用吧, 够不够简单粗暴。
使用预览 单一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()); } });

效果:
Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup

文章图片

以前会用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)); } });

效果:
Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup

文章图片

数据结构不同: 如果数据结构不同, 则不用传入泛型, 但是使用时需要强转:
//多种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
效果:
Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup

文章图片

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耦合, 每一个库都像可组装拆卸的机关枪一样, 拿起来就用。而不是笨重功能繁多的重装坦克。
搭配我的得意之作, 每次必安利的史上集成最简单侧滑菜单控件。
效果如下:
Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup

文章图片

Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup

文章图片

无特殊设置, 仅仅替换ViewGroup为流式布局, 替换Item根布局为我撸的侧滑菜单库, 能感受到这种0耦合的库的魅力了么。23333333 。
设计思路 下面就让我手摸手带大家实现它。
先看类图。
UML类图:
Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup

文章图片

先简要概括
* 我们的顶层接口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 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就比较像了, 我也是参照平时的写法。
核心就是实现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。多谢
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轮播图。
下文预告: 一个用ScrollView很容易实现, 但RecyclerView、ListView就无法实现的动画效果, 仿淘宝会员中心等级动画。凸显为所有ViewGroup增加Adapter模式的重要性。
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

    推荐阅读