---恢复内容开始---
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(简称)。待续。。。困死了有空再写。