关于ListVIew加载数据混乱的问题分析和解决

时间:2021-12-02 09:30:14

---恢复内容开始---

ListView是每个开发者必玩的东西了,这里分析一个非常重要也是我们常常忽略的一个细节。

一、讲这个问题之前我们需要先知道ListView的加载显示原理

关于ListVIew加载数据混乱的问题分析和解决

 

这个图讲的便是显示原理(网上找的,版权问题不要找我,哈哈),看不懂不要紧,我具体说一下。ListView采用的是一种可复用的item布局的显示模型来加载布局的,也就是说,ListView有一个item回收站(Recycler),每当ListView里新出现一个item(例如向上滑动的时候,底部新增item),ListView都会从回收站里拿出一个之前存好的item的布局出来给新出现的item做布局,那么回收站里的Item从哪里来的呢?例如当你向上滑动的时候顶部的item(图item1)已经滚到屏幕外了,这个滚到屏幕外的item就随之被回收站拿走了,与此同时将这个item的布局给底部新进来的item(图item8)。当你继续向上滚,于是item2回收,回收站拿给item9,如此反复刚才这个过程,这就使得ListView可以通过回收站放很多item。如果你项下滑动,哈哈 ,当然是同理,下面滚出去的进了回收站给上面进来的用。

再结合代码分析这个过程:

1 //当向上滑动时adapter会调用getView这个方法
2 public View getView(int position, View convertView, ViewGroup parent){
3     //这个方法所返回的值便是底部新增的item的布局View,由adapter传进来的convertView便是从回收站里拿出来的刚滚出去的item的View,我们通过在代码里编辑convertView并返
    //回出去,来达到我们生成一个我们指定布局的新增item。parent是使用适配器的母体,这里当然是ListView了
    return covertView
4 }

那么我们思考一个特殊情况:初始状态

也就是说,刚才我们分析的是ListView已经铺满了item的情况下滑动时的过程,那么从空内容到铺满的过程又是怎样理解呢?实际上,过程还是和上面一样,只不过我们可以这么理解:最开始ListView里是空的,回收站也是空的,item1从最底部出现,它伸手给回收站要布局模型,回收站说我没有,这时getView传的convertView==null,这时ListView只能通过getView方法体里你代码生成一个View,比如我们可以用LayoutInflater来inflate一个xml得到一个View对象返回出去给item1。以此类推,item2伸手要模型,回收站还是没有,因为一然没有item滚出去给回收站啊,除非他喵的你一个item就满屏了。直到item7铺满整个ListView的长度,这个过程中一直都是convertView==null的。当item8从底部出现的时候 item1已经滚出屏幕,回收站拿到了item1,adapter调getView的时候终于convertView!=null了,接下来的过程便是文章开始讲的那个过程了。

 

二、回收站缓存机制下的数据加载重复的问题


了解了这个ListView的回收站缓存机制,我们大概知道自定义adapter里怎么玩了:核心不就是重写getView的方法么,于是我们很自然的就上网搜索到了ViewHolder模式写getView,例如下面这这种

  1 package com.example.testlistview;
  2 
  3 import java.util.ArrayList;
  4 import java.util.HashMap;
  5 
  6 import android.os.Bundle;
  7 import android.app.Activity;
  8 import android.view.LayoutInflater;
  9 import android.view.Menu;
 10 import android.view.View;
 11 import android.view.ViewGroup;
 12 import android.widget.AdapterView;
 13 import android.widget.AdapterView.OnItemClickListener;
 14 import android.widget.BaseAdapter;
 15 import android.widget.CheckBox;
 16 import android.widget.ImageView;
 17 import android.widget.ListView;
 18 import android.widget.Toast;
 19 
 20 public class Test extends Activity {
 21     ListView li;
 22     @Override
 23     protected void onCreate(Bundle savedInstanceState) {
 24         super.onCreate(savedInstanceState);
 25         setContentView(R.layout.activity_test);
 26         li=(ListView) findViewById(R.id.listView);
 27         ArrayList<HashMap<String,Object>> dataList=CreateData();
 28         adapter ad=new adapter(dataList, R.layout.item);
 29         li.setAdapter(ad);
 30         li.setOnItemClickListener(new OnItemClickListener() {
 31 
 32             @Override
 33             public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
 34                     long arg3) {
 35                 Toast.makeText(Test.this, "第"+arg2+"行", 3000).show();
 36                 
 37             }
 38         });
 39     }
 40 
 41     public ArrayList<HashMap<String,Object>> CreateData(){
 42         ArrayList<HashMap<String,Object>> dataList=new ArrayList<HashMap<String,Object>>();
 43         for (int i = 0; i < 17; i++) {
 44             HashMap<String,Object> m=new HashMap<String, Object>();
 45             m.put("id",""+i);
 46             m.put("des", "des");
 47             dataList.add(m);
 48         }
 49         return dataList;
 50     }
 51     
 52     class adapter extends BaseAdapter{
 53         ArrayList<HashMap<String,Object>> data ;
 54         int itemLayout;
 55         LayoutInflater inf;
 56         public adapter(ArrayList<HashMap<String,Object>> data ,int itemLayout) {
 57             this.data=data;
 58             this.itemLayout=itemLayout;
 59             inf=LayoutInflater.from(Test.this);
 60         }
 61 
 62         @Override
 63         public int getCount() {
 64             // TODO Auto-generated method stub
 65             return data.size();
 66         }
 67 
 68         @Override
 69         public Object getItem(int position) {
 70             // TODO Auto-generated method stub
 71             return data.get(position);
 72         }
 73 
 74         @Override
 75         public long getItemId(int position) {
 76             
 77             return position;
 78         }
 79 
 80         @Override
 81         public View getView(int position, View convertView, ViewGroup parent) {
 82             HashMap<String,Object> d=(HashMap<String, Object>) getItem(position);
 83             ViewHolder vh=null;
 84             if(convertView==null){
 85                 vh=new ViewHolder();
 86                 convertView=inf.inflate(itemLayout, null);
 87                 vh.cb=(CheckBox) convertView.findViewById(R.id.cb);
 88                 vh.iv=(ImageView) convertView.findViewById(R.id.iv);
 89                 convertView.setTag(vh);
 90             }
 91             else{
 92                 vh=(ViewHolder) convertView.getTag();
 93             }
 94             vh.cb.setFocusable(false);
 95             return convertView;
 96         }
 97         
 98         class ViewHolder{
 99             ImageView iv;
100             CheckBox cb;
101         }
102     }
103 
104 }

 

这是个以checbox和ImageView为item元素的ListView,可是等我们跑程序来却发现问题了:当我们选择item1的checkbox时你向下滚动却发现下面的某项的checkbox也被选上了,通过我们刚才对listView显示原理的分析,不难理解这个现象:当我们点选item1的checkbox之后,进行滑动,item1滚出后被回收站回收,checkbox状态当然也是被回收的,当下面某项滚进屏幕时恰好调用回收站里的item1时,便带着checkbox的状态一起拿来了,这就导致了item1和itemx一选同选,一清除同清除,这种重复性问题。

所以,对于item上的每一个控件,必须制定各自显示规则。在Holder模式下我们在getView里操作convertView对象始终是回收站里的对象,只有制定出合理的显示规则进行更新回收站对象,才能使得返回出去的convertView被新滚进屏幕的item用来正确展现其所携带的数据层

这里大概有三种情况:

1、比较简单的,比如TextView我们一般根据position的不同来修改Text更新到convertView里返回出去给新item。这种其实就是数据层根据postion取出相应数据给TextView。

2、当遇到含有状态的控件例如CheckBox。这种和简单TextView根本区别在于,TextView完全由程序控制显示,而CheckBox是交由用户来控制状态的,如果我们不做处理。就会出现上面说的一荣俱荣,一损俱损的局面,因为convertView是回收站里的缓存对象,item1 item8 item15都用的是同一个缓存对象,当然一动全动了,所以,必须在getVIew里区分开使用着同一个convertView的不同item哪个该勾选,哪个不该,而在getView里区别不同item的东西就只有position,于是我们自然而然的想到一种办法——将状态根据position保存起来,下次getView到此item时调取其状态进制赋值。如下

  1 package com.example.testlistview;
  2 
  3 import java.util.ArrayList;
  4 import java.util.HashMap;
  5 
  6 import android.os.Bundle;
  7 import android.app.Activity;
  8 import android.view.LayoutInflater;
  9 import android.view.Menu;
 10 import android.view.View;
 11 import android.view.ViewGroup;
 12 import android.widget.AdapterView;
 13 import android.widget.AdapterView.OnItemClickListener;
 14 import android.widget.BaseAdapter;
 15 import android.widget.CheckBox;
 16 import android.widget.CompoundButton;
 17 import android.widget.CompoundButton.OnCheckedChangeListener;
 18 import android.widget.ImageView;
 19 import android.widget.ListView;
 20 import android.widget.Toast;
 21 
 22 public class Test extends Activity {
 23     ListView li;
 24     @Override
 25     protected void onCreate(Bundle savedInstanceState) {
 26         super.onCreate(savedInstanceState);
 27         setContentView(R.layout.activity_test);
 28         li=(ListView) findViewById(R.id.listView);
 29         ArrayList<HashMap<String,Object>> dataList=CreateData();
 30         adapter ad=new adapter(dataList, R.layout.item);
 31         li.setAdapter(ad);
 32         li.setOnItemClickListener(new OnItemClickListener() {
 33 
 34             @Override
 35             public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
 36                     long arg3) {
 37                 Toast.makeText(Test.this, "第"+arg2+"行", 3000).show();
 38                 
 39             }
 40         });
 41     }
 42 
 43     public ArrayList<HashMap<String,Object>> CreateData(){
 44         ArrayList<HashMap<String,Object>> dataList=new ArrayList<HashMap<String,Object>>();
 45         for (int i = 0; i < 17; i++) {
 46             HashMap<String,Object> m=new HashMap<String, Object>();
 47             m.put("id",""+i);
 48             m.put("des", "des");
 49             dataList.add(m);
 50         }
 51         return dataList;
 52     }
 53     
 54     class adapter extends BaseAdapter{
 55         ArrayList<HashMap<String,Object>> data ;
 56         int itemLayout;
 57         LayoutInflater inf;
 58         public adapter(ArrayList<HashMap<String,Object>> data ,int itemLayout) {
 59             this.data=data;
 60             this.itemLayout=itemLayout;
 61             inf=LayoutInflater.from(Test.this);
 62         }
 63 
 64         @Override
 65         public int getCount() {
 66             // TODO Auto-generated method stub
 67             return data.size();
 68         }
 69 
 70         @Override
 71         public Object getItem(int position) {
 72             // TODO Auto-generated method stub
 73             return data.get(position);
 74         }
 75 
 76         @Override
 77         public long getItemId(int position) {
 78             
 79             return position;
 80         }
 81 
 82         @Override
 83         public View getView(final int position, View convertView, ViewGroup parent) {
 84             HashMap<String,Object> d=(HashMap<String, Object>) getItem(position);
 85             
 86             ViewHolder vh=null;
 87             if(convertView==null){
 88                 vh=new ViewHolder();
 89                 convertView=inf.inflate(itemLayout, null);
 90                 vh.cb=(CheckBox) convertView.findViewById(R.id.cb);
 91                 vh.iv=(ImageView) convertView.findViewById(R.id.iv);
 92                 convertView.setTag(vh);
 93             }
 94             else{
 95                 vh=(ViewHolder) convertView.getTag();
 96             }
 97             vh.cb.setFocusable(false);
 98             System.out.println(CBstate);
 99             //要先起一个新的状态监听
100             vh.cb.setOnCheckedChangeListener(new CBcheckedListener(position));
101             //然后读取状态,顺序不能弄反,一旦反了就相当于setChecked完用了上一次position的监听,把上一次保存的结果覆盖了
102             vh.cb.setChecked(getState(position));
103             return convertView;
104         }
105         //注意这里便是储存状态的仓库
106         HashMap<Integer,Boolean> CBstate=new HashMap<Integer, Boolean>();
107         //存储状态
108         public void setState(int pos,Boolean state){
109             CBstate.put(pos, state);
110         }                
111         //读取状态
112         public Boolean getState(int pos){
113             Boolean s=CBstate.get(pos);
114             return s==null?false:s;
115         }
116         
117         //这里自定义一个勾选监听的实现类,方便传递position
118         class CBcheckedListener implements OnCheckedChangeListener{
119             int pos=0;
120             public CBcheckedListener(int pos) {
121                 this.pos=pos;
122             }
123             @Override
124             public void onCheckedChanged(CompoundButton buttonView,
125                     boolean isChecked) {
126                 //当用户点选时触发此事件进行存储状态,下次再滚到这个position的item时会在执行上面的代码读出状态正确展示给用户
127                 setState(pos, isChecked);
128                 notifyDataSetChanged();
129                 
130             }
131             
132         }
133         
134         
135         
136         class ViewHolder{
137             ImageView iv;
138             CheckBox cb;
139         }
140     }
141 
142 }

再运行一遍。程序正常跑是没有问题了,但是有个不爽的地方,那就是每次滑动我都要new一个新的勾选监听对象,继续进行优化:还是这个思路,将监听器对象保存起来,做成可复用的!因为这个例子比较简单看不出来效果,一旦你的业务逻辑复杂了,new一个对象便成了一种累赘,影响ListView的顺滑。

  1 package com.example.testlistview;
  2 
  3 import java.util.ArrayList;
  4 import java.util.HashMap;
  5 
  6 import android.os.Bundle;
  7 import android.app.Activity;
  8 import android.view.LayoutInflater;
  9 import android.view.Menu;
 10 import android.view.View;
 11 import android.view.ViewGroup;
 12 import android.widget.AdapterView;
 13 import android.widget.AdapterView.OnItemClickListener;
 14 import android.widget.BaseAdapter;
 15 import android.widget.CheckBox;
 16 import android.widget.CompoundButton;
 17 import android.widget.CompoundButton.OnCheckedChangeListener;
 18 import android.widget.ImageView;
 19 import android.widget.ListView;
 20 import android.widget.Toast;
 21 
 22 public class Test extends Activity {
 23     ListView li;
 24     @Override
 25     protected void onCreate(Bundle savedInstanceState) {
 26         super.onCreate(savedInstanceState);
 27         setContentView(R.layout.activity_test);
 28         li=(ListView) findViewById(R.id.listView);
 29         ArrayList<HashMap<String,Object>> dataList=CreateData();
 30         adapter ad=new adapter(dataList, R.layout.item);
 31         li.setAdapter(ad);
 32         li.setOnItemClickListener(new OnItemClickListener() {
 33 
 34             @Override
 35             public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
 36                     long arg3) {
 37                 Toast.makeText(Test.this, "第"+arg2+"行", 3000).show();
 38                 
 39             }
 40         });
 41     }
 42 
 43     public ArrayList<HashMap<String,Object>> CreateData(){
 44         ArrayList<HashMap<String,Object>> dataList=new ArrayList<HashMap<String,Object>>();
 45         for (int i = 0; i < 107; i++) {
 46             HashMap<String,Object> m=new HashMap<String, Object>();
 47             m.put("id",""+i);
 48             m.put("des", "des");
 49             dataList.add(m);
 50         }
 51         return dataList;
 52     }
 53     
 54     class adapter extends BaseAdapter{
 55         ArrayList<HashMap<String,Object>> data ;
 56         int itemLayout;
 57         LayoutInflater inf;
 58         public adapter(ArrayList<HashMap<String,Object>> data ,int itemLayout) {
 59             this.data=data;
 60             this.itemLayout=itemLayout;
 61             inf=LayoutInflater.from(Test.this);
 62         }
 63 
 64         @Override
 65         public int getCount() {
 66             // TODO Auto-generated method stub
 67             return data.size();
 68         }
 69 
 70         @Override
 71         public Object getItem(int position) {
 72             // TODO Auto-generated method stub
 73             return data.get(position);
 74         }
 75 
 76         @Override
 77         public long getItemId(int position) {
 78             
 79             return position;
 80         }
 81 
 82         @Override
 83         public View getView(final int position, View convertView, ViewGroup parent) {
 84             HashMap<String,Object> d=(HashMap<String, Object>) getItem(position);
 85             
 86             ViewHolder vh=null;
 87             if(convertView==null){
 88                 vh=new ViewHolder();
 89                 convertView=inf.inflate(itemLayout, null);
 90                 vh.cb=(CheckBox) convertView.findViewById(R.id.cb);
 91                 vh.iv=(ImageView) convertView.findViewById(R.id.iv);
 92                 convertView.setTag(vh);
 93             }
 94             else{
 95                 vh=(ViewHolder) convertView.getTag();
 96             }
 97             vh.cb.setFocusable(false);
 98             
 99             //先判断仓库里有没有position对应的监听器
100             if(getCBL(position)==null){
101                 setCBL(position, new CBcheckedListener(position));
102             }
103             //再起状态监听
104             vh.cb.setOnCheckedChangeListener(getCBL(position));
105             //然后读取状态,顺序不能弄反,一旦反了就相当于setChecked完用了上一次position的监听,把上一次保存的结果覆盖了
106             vh.cb.setChecked(getState(position));
107             System.out.println(CBstate);
108             return convertView;
109         }
110         //注意这里便是储存状态的仓库
111         HashMap<Integer,Boolean> CBstate=new HashMap<Integer, Boolean>();
112         //存储状态
113         public void setState(int pos,Boolean state){
114             CBstate.put(pos, state);
115         }                
116         //读取状态
117         public Boolean getState(int pos){
118             Boolean s=CBstate.get(pos);
119             return s==null?false:s;
120         }
121         
122         //保存监听器的仓库
123         HashMap<Integer,CBcheckedListener> CBLStore=new HashMap<Integer, Test.adapter.CBcheckedListener>();
124         //存储
125         public void setCBL(int pos,CBcheckedListener cbl){
126             CBLStore.put(pos, cbl);
127         }
128         public CBcheckedListener getCBL(int pos){
129             return CBLStore.get(pos);
130         }
131         //这里自定义一个勾选监听的实现类,方便传递position
132         class CBcheckedListener implements OnCheckedChangeListener{
133             int pos=0;
134             public CBcheckedListener(int pos) {
135                 this.pos=pos;
136             }
137             @Override
138             public void onCheckedChanged(CompoundButton buttonView,
139                     boolean isChecked) {
140                 //当用户点选时触发此事件进行存储状态,下次再滚到这个position的item时会在执行上面的代码读出状态正确展示给用户
141                 setState(pos, isChecked);
142                 notifyDataSetChanged();
143                 
144             }
145             
146         }
147         
148         
149         
150         class ViewHolder{
151             ImageView iv;
152             CheckBox cb;
153         }
154     }
155 
156 }

 

3,、绑定着点击、滑动、触摸事件的控件,这种情况比第二种更为复杂,也是大项目最频繁遇到的。这里我举一个简单的场景:item里有一个ImageView(简称)。待续。。。困死了有空再写。