• 一个ListView布局的不断演化


        刚出来工作,就负责一个APP的某块功能的编写,该功能就是类似微博那样的界面。微博界面的编写实际上是非常复杂的,虽然它只是一个ListView,但要想让这个ListView滑得动,是的,在一些配置低的手机,随便一个负载内容多的Item,都有可能导致OOM。。。如果只是简单的为了实现了效果,可以选择将所有内容都写在xml文件,如果布局不好的话,就会出现嵌套过多的情况,同样也会出现OOM的情况。。。就算不会出现OOM的情况,也能滑得动,也会面临是否能够滑得快。。。

         要想能滑得动,也能滑得快,就要动点脑筋了。

         一开始非常简单的代码就是这样:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.list_item, null);
            holder = new ViewHolder();
            ……
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder)convertView.getTag();
        }
    }
    /**
     * ViewHolder
     */
    private static class ViewHolder {
        ImageView appIcon;
        TextView  appName;
        TextView  appInfo;
    }

          这是谷歌推荐的方式,实际上也解决了很多性能上的问题。

          Android原生的ListView原本就做了相应的缓存机制,Recycler。

          Recycler的工作原理大致如下:

          假设屏幕最多能看到11个item,那么当第1个item滚出屏幕,这个item的View进入RecycleBin中,第12个要出现前,通过 getView从回收站(RecycleBin)中重用这个View,然后设置数据,而不必重新创建一个View。

          这样即使有上万个Item,inflate的次数最多就是n,也就是一个屏幕能够容纳的个数。

          大部分的情况都可以用这样的代码解决,但我觉得对于每个Adapter都要写一个ViewHolder实在是太麻烦了,于是进一步将代码改写为这样:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
         if (convertView == null) {
            convertView = inflater.inflate(R.layout.list_item, null);
            }
    
            ImageView icon = (ImageView)CommonViewHolder.get(convertView, R.id.image_view);
    }

         其中CommonViewHolder的代码如下:

    public class CommonViewHolder {
    
        /**
         * 用于获取ItemView中的控件
         *
         * @param view ItemView
         * @param id   要获取的控件的id
         * @param <T>  返回的控件的类型
         * @return 返回的控件
         */
        public static <T extends View> T get(View view, int id) {
            SparseArrayCompat<View> viewHolder = (SparseArrayCompat<View>) view.getTag();
            if (viewHolder == null) {
                viewHolder = new SparseArrayCompat<>();
                view.setTag(viewHolder);
            }
            View childView = viewHolder.get(id);
            if (childView == null) {
                childView = view.findViewById(id);
                viewHolder.put(id, childView);
            }
            return (T) childView;
        }
    }

          实际上,ViewHolder就是通过setTag方法将相应的控件作为View的属性保存起来,然后下次使用的时候就可以直接复用。

          既然明白了它的原理,就可以对它进行改造,CommonViewHolder是利用SparseArrayCompact存储控件。SparseArrayCompact是SparseArrayCompact是SparseArray的兼容类,本质上就是类似Map的键值对容器,谷歌宣称它的性能比Map要好,因为内在的算法已经优化了。

          这里的工作很简单,同样是利用setTag方法保存View中的控件,但是却把findViewById这样的工作放在了CommonViewHolder中,这样就不用每次都要调用findViewById方法了。

          为了考虑通用,还使用泛型。

          代码量瞬间就减少了很多,但这时就面临了一个问题:Item项错乱了。。。

          在快速滑动的时候,图片加载错了,仔细调试,就发现是Recycler机制出现了问题。当快速滑动的时候,原本应该开始加载图片的控件已经滑出屏幕,然后我的图片加载是异步的,所以图片就会加载在后面的Item上。

          解决这个问题的方法同样得利用setTag方法:

    icon.setTag(imageUrl)
    
    ....
    if(imageUrl.equals(icon.getTag()){
          ....
    }

         通过setTag保存ImageView要加载的url信息,然后在下次加载的时候判断是否相同。

         问题解决了,但看到getView方法中为了实现微博这样承载大量信息的界面,不得不编写大量的业务代码,而且这样复用性很差,因为微博详情的界面和列表项的界面基本一样,只是有一些不同而已。如果再写一个,就有点傻逼了,但如果不写,getView方法中肯定又要写更多的判断。

         为了解决复用的问题,就开始组件化。

         将微博界面拆成两个部分:HeaderView和BodyView。HeaderView负责微博作者的基本信息,而BodyView就是微博内容。

         这样,我只要在getView方法中这样写:

    add(new HeaderView());
    ...
    add(new BodyView());
    ...

         也就是说,我从一个静态布局改成动态布局,这样我在详情那里也可以复用。

         到了这里原本也应该结束了,但我又想要为微博业务编写测试,但Android中View和业务的代码是各种纠缠,很难完全脱离View来测试业务。

         经过思考和查找资料,我找到了一种方式:ViewModel。

         编写相应的ViewModel作为Controller,就可以将View和业务的代码解绑:

    public class ViewModel{
         private String text;
         ...
         public void setText(TextView view){
                view.setText(text);
         }
    }

          这样组件里面的代码就更少了,它只要声明好控件然后传进来就行了,数据的获取和绑定都在ViewModel这里。

          然后我们来写一个简单的测试:

    ViewModel model = new ViewModel();
    Button button = new Button(context);
    button.setText("你好");
    model.changeButtonText(button);
    assertEquals("我好",  button.getText());
    
    public void changeButtonText(Button button){
           button.setOnClickListener(new OnClickListener(){
                   button.setText("我好");
           });
    }

         利用ViewModel,我们可以方便的测试Android种的业务。

         这是到目前为止的思考和尝试,实际上,我认为代码还会不断演化下去,现在已经开始出现MVVM的一些思想的应用,到了最后,没准就会完全演化成MVVM模式。

         简单的代码,只要不断思考,慢慢的,所能学到的东西就会变得越来越多,最后甚至超出我们的想象。

  • 相关阅读:
    安装XMind如何安装到指定目录
    显示器AVG、DVI、HDMI、DisplayPort、Type-C、雷电接口
    SATA与PCI-E速度对比
    SRAM、DRAM、Flash、DDR有什么区别
    USB3.0与Type-C接口的关系
    一图明白ACHI,SATA之间的关系
    U.2与M.2接口
    遗传算法实例分析
    从零开始写代码-python解深度学习神经网络题目
    基于Yen算法的k最短路径问题的python实现
  • 原文地址:https://www.cnblogs.com/wenjiang/p/4185930.html
Copyright © 2020-2023  润新知