美文网首页自定义view相关Android开发Android 自定义控件
一起撸个朋友圈吧(step3) - ListAdapter篇

一起撸个朋友圈吧(step3) - ListAdapter篇

作者: Razerdp | 来源:发表于2016-02-16 17:58 被阅读870次

    项目地址:https://github.com/razerdp/FriendCircle
    一起撸个朋友圈吧这是本文所处文集,所有更新都会在这个文集里面哦,欢迎关注

    上篇链接:http://www.jianshu.com/p/dc5782a494b5
    下篇链接:http://www.jianshu.com/p/1f85d3978bb5

    文章开始之前,谈谈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<T> {
        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 CircleBaseAdapter<T> extends BaseAdapter {
        private static final String TAG = "FriendCircleAdapter";
        //数据
        protected List<T> datas = new ArrayList<>();
        //类型集合
        protected HashMap<Integer, Class<? extends BaseItemView<T>>> itemInfos;
        protected Activity context;
        protected LayoutInflater mInflater;
    
        public CircleBaseAdapter(Activity context, Builder<T> 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<T> {
            private HashMap<Integer, Class<? extends BaseItemView<T>>> itemInfos;
            private Activity context;
            private List<T> datas;
    
            public Builder() {
                itemInfos = new HashMap<>();
            }
    
            public Builder(List<T> datas) {
                itemInfos = new HashMap<>();
                this.datas = datas;
            }
    
            public Builder addType(int type, Class<? extends BaseItemView<T>> viewClass) {
                itemInfos.put(type, viewClass);
                return this;
            }
    
            public Builder setDatas(List<T> 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<T>的类,所以我们使用这个adapter只需要实现这个接口就可以 了。

    接下来我们实现一下这个接口:

    public abstract class BaseItemDelegate<T> implements BaseItemView<T>, 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 FriendCircleAdapterTest<TestBean> extends 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<TestBean> {
        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不用管,我们管理的只是对应类的操作就可以了。

    本篇结束。

    相关文章

      网友评论

      • 天之大任:建议,每次都会findview,这个方法位置放的不对,当不为空的时候再调用
        天之大任:@羽翼君 共同学习,觉得你封的挺好的,所以参考参考
        Razerdp:@天之大任 感谢提醒^ω^
        Razerdp:@天之大任 是的,当时我也留意到,现在已经补充
      • Brioal:赞
      • 码农仔:强力支持!

      本文标题:一起撸个朋友圈吧(step3) - ListAdapter篇

      本文链接:https://www.haomeiwen.com/subject/gpipkttx.html