项目地址:https://github.com/razerdp/FriendCircle
文章开始之前,谈谈JSON这个数据合集的问题吧,关于JSON,因为在下在公司实习的时候写了好多次解析,说实话,在发现GsonFormat插件这个东东之前,,,我写JSON解析都是这么写的。。:
xxx=json.optXXX("xxx",xxx);
唉,现在真的挺佩服当时的耐心,面对这么多的JSON Array,Object我居然有如此耐心一个一个去手动解析。
这个一起撸朋友圈文章写到这里,其实我也发现了,似乎没有后端支持,还真挺难搞的,同时因为我的毕业设计也需要一些后端的支持,所以在下决定两者同时并行。。。希望有一天,可以撸一个简单的服务器来支撑我们这个项目-V-
废话完了,进入今天的主题吧
首先感谢同事的思想(这是他的git哦:https://github.com/wenjiahui ),这篇文章扩展于他的idea.
关于一个Adapter,我们应该也写过很多很多了,无非就是继承一个BaseAdapter,实现那些getXXX,然后getView里面用viewholder装起来。
确实,我们的朋友圈adapter也是基于这个思想,但是略有改变。
其一,我们有多个type,比如图文,文字什么的,我们需要区分这些type。这个好办,ItemViewType解决,嗯这没问题。
其二,我们有各种各样的ClickEvent,比如点击头像,点击图片,长按文字复制,点击评论,点击名字什么的。这个好办,不同的clickListener,嗯,这么问题。
那么,现在问题来了,如果我们在adapter真按照平时的写法来实现上面两点,那么我们将会看到这样的代码:
...getItem什么的此处略过 public View getView(final int position, View convertView, ViewGroup parent) { ViewHolder1 xxx;//图文viewholder ViewHolder2 xxx;//文字viewholder ViewHolder3 xxx;//网页viewholder ...好多viewholder ...好多viewholder初始化 switch(getItemViewType(position)){ case xxx: ...好多代码; ...xxx.setOnClickListener(xxx);//点击xxx的listener,下面也许还有n个 break; case xxx: ...好多代码; ...xxx.setOnClickListener(xxx);//点击xxx的listener,下面也许还有n个 break; ...还好好多case..... } class ViewHolder 1{ ... } class ViewHolder 2{ ... } class ViewHolder 3{ ... } OnClickListener xxx1=new OnClickListener{ onClick(View v) } OnClickListener xxx2=new OnClickListener{ onClick(View v) } OnClickListener xxx3=new OnClickListener{ onClick(View v) } ... }
我相信,没有几个人愿意看到一个adapter一千多行或者两千行代码吧。。。其中还混有N个viewholder和N个点击事件。
回归本源,在android里面,adapter(适配器)到底是干嘛用的?
适配器就是用来将数据(data)和视图(view)绑定的工具,如果更加简单的说,就是根据需求展示不同数据集数据,再更简单的说,他喵的就是一个渲染器。(←这个非权威描述,需要权威描述的请自行谷歌“适配器模式”)
那么作为一个渲染器,我们需要他做的,就是渲染画面就好了,其他的不要管(实现控制和展示两者分离,易于维护)。
于是我们就有了以下方案:
- 抽象一个viewholder,该holder用于告诉adapter:“我的心是属于这个类型的”(这个类型用这个xml布局)
- adapter用一个集合,存入所有类型的holder,并实现将viewType和holder对应起来。
- adapter只负责渲染,其他的在holder里面完成。
文字版也许不那么清晰,我们看看思维导图吧:
导图
大致结构如上
接下来实现一下大致的结构雏形,具体的代码如下:
public interface BaseItemView{ int getViewRes(); void onFindView(@NonNull View parent); void onBindData(final int position, @NonNull View v, @NonNull T data,final int dynamicType); Activity getActivityContext(); void setActivityContext(Activity context); }
我们采取接口的方式,该接口实现以下两个功能:(此处没有遵循单一职责原则)
- 得到对应的布局id
- 数据绑定
然后抽象我们的adapter:
/** * Created on 2016/2/16. * 适配器抽象 */ public abstract class CircleBaseAdapterextends BaseAdapter { private static final String TAG = "FriendCircleAdapter"; //数据 protected List datas = new ArrayList<>(); //类型集合 protected HashMap >> itemInfos; protected Activity context; protected LayoutInflater mInflater; public CircleBaseAdapter(Activity context, Builder mBuilder) { this.context = context; mInflater = LayoutInflater.from(context); datas.clear(); datas.addAll(mBuilder.datas); itemInfos = mBuilder.itemInfos; } @Override public int getCount() { return datas.size(); } @Override public T getItem(int position) { return datas.get(position); } @Override public long getItemId(int position) { return position; } @Override public abstract int getItemViewType(int position); @Override public int getViewTypeCount() {return 15;} @Override public View getView(int position, View convertView, ViewGroup parent) { final int dynamicType = getItemViewType(position); BaseItemView view = null; if (convertView == null) { Class viewClass = itemInfos.get(dynamicType); Log.d(TAG,""+viewClass); try { view = (BaseItemView) viewClass.newInstance(); } catch (InstantiationException e) { Log.e(TAG, "反射创建失败!!!"); e.printStackTrace(); } catch (IllegalAccessException e) { Log.e(TAG, "反射创建失败!!!"); e.printStackTrace(); } if (view != null) { convertView = mInflater.inflate(view.getViewRes(), parent, false); convertView.setTag(view); } else { throw new NullPointerException("view是空的哦~"); } } else { view = (BaseItemView) convertView.getTag(); } view.setActivityContext(context); view.onFindView(convertView); view.onBindData(position, convertView, getItem(position), dynamicType); return convertView; } public static class Builder { private HashMap >> itemInfos; private Activity context; private List datas; public Builder() { itemInfos = new HashMap<>(); } public Builder(List datas) { itemInfos = new HashMap<>(); this.datas = datas; } public Builder addType(int type, Class extends BaseItemView > viewClass) { itemInfos.put(type, viewClass); return this; } public Builder setDatas(List datas) { this.datas = datas; return this; } public Builder build() {return this;} } }
我们主要讲注意力放到getView方法里面:
可以看得出,我们的getView其实大致来说都是跟平时写法一样的,都是
if(converview==null){ ...viewholder }else{ viewholder=converview.getTag(); }
不过有点不同的是这里我们用反射来将viewholder给new一个出来,这样我们就可以将所有的其他操作都放在对应的class里面执行,比如实现点击接口什么的。
这么做的好处就是。。。。。起码代码看起来没那么多对吧- –
其次就是易于维护。
另外有一点需要注意的是getViewTypeCount()必须比getItemViewType要大,否则会出现越界的error,而我们上一篇定义的类型最大的是14,所以我们给个15的定值。(这里实现还是不太好啊。。。。以后做优化)
存储集合的地方我们使用builder模式,毕竟不知道啥时候也许会增加一些新的viewType,对吧。
我们集合存的是一个继承BaseItemView
接下来我们实现一下这个接口:
public abstract class BaseItemDelegateimplements BaseItemView , View.OnClickListener { protected Activity context; public BaseItemDelegate() { } public BaseItemDelegate(Activity context) { this.context = context; } @Override public void onClick(View v) { } @Override public void onBindData(int position, @NonNull View v, @NonNull T data, final int dynamicType) { // TODO: 2016/2/16 初始化共用部分 bindData(position, v, data, dynamicType); } @Override public Activity getActivityContext() { return context; } @Override public void setActivityContext(Activity context) { this.context=context; } protected abstract void bindData(int position, @NonNull View v, @NonNull T data, final int dynamicType); }
因为通过反射方法创建,所以我们需要保留一个空的构造器哦,这个抽象类将作为基本的item,这里以后会实现共有部分的操作,这样我们的其他不同的view只需要继承它就可以了。
实现完这几个类,最后就是进行测试了。
新建一个adapter继承我们的父类:
public class FriendCircleAdapterTestextends CircleBaseAdapter { private static final String TAG = "FriendCircleAdapterTest"; public FriendCircleAdapterTest(Activity context, Builder mBuilder) { super(context, mBuilder); } @Override public int getItemViewType(int position) { razerdp.friendcircle.test.TestBean bean= (razerdp.friendcircle.test.TestBean) datas.get(position); Log.d(TAG,"当前type------- "+bean.type); return bean.type; } }
然后新建一个view继承我们的baseitem类,并实现初步的点击方法:
public class TestItem1 extends BaseItemDelegate{ private TextView testTx; private Button testBtn; public TestItem1() {} @Override protected void bindData(int position, @NonNull View v, @NonNull TestBean data, int dynamicType) { testBtn.setTag(data); testTx.setText(data.testStr); } @Override public int getViewRes() { return R.layout.test_type_one; } @Override public void onFindView(@NonNull View parent) { testTx = (TextView) parent.findViewById(R.id.test_1); testBtn = (Button) parent.findViewById(R.id.btn_test_1); testTx.setOnClickListener(this); testBtn.setOnClickListener(this); } @Override public void onClick(View v) { super.onClick(v); switch (v.getId()) { case R.id.test_1: break; case R.id.btn_test_1: TestBean bean = (TestBean) v.getTag(); Toast.makeText(getActivityContext(), "你点的是类型 " + bean.type, Toast.LENGTH_SHORT).show(); break; default: break; } } }
最后放进我们的测试数据:
测试数据
如你所见,我们的adapter需要通过builder弄进去,builder里面的addType方法也很直观:类型-对应的class。对于使用者来说,需要用到的就这么多,adapter不用管,我们管理的只是对应类的操作就可以了。
本篇结束。