1、ListView基本概念
列表显示需要三个元素:
- ListView:用来展示列表的View。
- 适配器:用来把数据映射到ListView上
- 数据:具体的将被映射的字符串,图片或基本组件
适配器类型分为三种:ArrayAdapter,SimpleAdapter和SimpleCursorAdapter。
1.1、ArrayAdapter
ArrayAdapter是BaseAdapter的派生类,在BaseAdapter的基础上,添加了一项重大的功能:可以直接使用泛型构造。
ArrayAdapter内部维护了一个List<T>,getItem(position)时直接获取List.get(position)。
可添加text和object。
我们先来看一个简单的例子:
1 @Override 2 protected void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 setContentView(R.layout.activity_main); 5 ListView listView = (ListView) this.findViewById(R.id.list); 6 UserAdapter adapter = new UserAdapter(this, R.layout.list_item); 7 adapter.add(new User(10, "小智", "男")); 8 adapter.add(new User(10, "小霞", "女")); 9 listView.setAdapter(adapter); 10 } 11 12 @Override 13 public boolean onCreateOptionsMenu(Menu menu) { 14 // Inflate the menu; this adds items to the action bar if it is present. 15 getMenuInflater().inflate(R.menu.main, menu); 16 return true; 17 } 18 19 class UserAdapter extends ArrayAdapter<User> { 20 private int mResourceId; 21 22 public UserAdapter(Context context, int textViewResourceId) { 23 super(context, textViewResourceId); 24 this.mResourceId = textViewResourceId; 25 } 26 27 @Override 28 public View getView(int position, View convertView, ViewGroup parent) { 29 User user = getItem(position); 30 LayoutInflater inflater = getLayoutInflater(); 31 View view = inflater.inflate(mResourceId, null); 32 TextView nameText = (TextView) view.findViewById(R.id.name); 33 TextView ageText = (TextView) view.findViewById(R.id.age); 34 TextView sexText = (TextView) view.findViewById(R.id.sex); 35 36 nameText.setText(user.getName()); 37 ageText.setText(user.getAge()); 38 sexText.setText(user.getSex()); 39 40 return view; 41 } 42 } 43 44 class User { 45 private int mAge; 46 private String mName; 47 private String mSex; 48 49 public User(int age, String name, String sex) { 50 this.mAge = age; 51 this.mName = name; 52 this.mSex = sex; 53 } 54 55 public String getName() { 56 return this.mName; 57 } 58 59 public String getAge() { 60 return this.mAge + ""; 61 } 62 63 public String getSex() { 64 return this.mSex; 65 } 66 }
这里自定义了一个ArrayAdapter,有关于Adapter的使用在之前的SimpleAdapter中已经涉及到了,所以这里直接就是以自定义的ArrayAdapter作为例子。
我们这里需要将学生的信息罗列出来,需要三个TextView:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="vertical" > 6 7 <TextView 8 android:id="@+id/name" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" /> 11 12 <TextView 13 android:id="@+id/age" 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" /> 16 17 <TextView 18 android:id="@+id/sex" 19 android:layout_width="wrap_content" 20 android:layout_height="wrap_content" /> 21 22 </LinearLayout>
在自定义ArrayAdapter的时候,最神奇的地方就是我们可以指定ArrayAdapter绑定的数据类型,可以是基本数据类型,也可以是自定义的对象类型,像是这次的User类型。对于自定义的ArrayAdapter的构造方法,存在很多形式,这次是传进一个View的资源Id,但是我们也可以指定绑定的数据类型。
ArrayAdapter的神奇之处就是我们竟然可以像是操作Array一样来操作ArrayAdapter!像是例子中的添加操作,而其他的适配器都是需要传进一个容器的。ArrayAdapter为什么可以处理对象类型的数据呢?其实,ArrayAdapter是使用数组中对象的toString()方法来填充指定的TextView,所以我们可以通过重写对象的toString()方法来自定义ListView的显示。
1 @Override 2 public View getView(int position, View convertView, ViewGroup parent) { 3 User user = getItem(position); 4 LayoutInflater inflater = getLayoutInflater(); 5 View view = inflater.inflate(mResourceId, null); 6 7 TextView text = (TextView) view.findViewById(R.id.info); 8 text.setText(user.toString()); 9 return view; 10 } 11 12 class User { 13 private int mAge; 14 private String mName; 15 private String mSex; 16 17 public User(int age, String name, String sex) { 18 this.mAge = age; 19 this.mName = name; 20 this.mSex = sex; 21 } 22 23 @Override 24 public String toString() { 25 return "姓名:" + mName + " " + "年龄:" + mAge + " " + "性别:" + mSex; 26 } 27 }
这样我们可以只在一行中显示所有数据。
使用ArrayAdapter最大的疑问就是我们是否需要将一个现成的容器传入ArrayAdapter中?原本ArrayAdapter本身就用一般容器的基本操作,像是添加新的元素等,但它本身并不能完成当成容器使用,我们更多的时候是要将一个容器中的元素交给ArrayAdapter,由后者决定它的显示形式。
1 class UserAdapter extends ArrayAdapter<User> { 2 private int mResourceId; 3 4 public UserAdapter(Context context, int textViewResourceId, 5 List<User> users) { 6 super(context, textViewResourceId, users); 7 this.mResourceId = textViewResourceId; 8 } 9 10 @Override 11 public View getView(int position, View convertView, ViewGroup parent) { 12 User user = getItem(position); 13 LayoutInflater inflater = getLayoutInflater(); 14 View view = inflater.inflate(mResourceId, null); 15 16 TextView text = (TextView) view.findViewById(R.id.info); 17 text.setText(user.toString()); 18 return view; 19 } 20 }
1 List<User> users = new ArrayList<User>(); 2 users.add(new User(10, "小智", "男")); 3 users.add(new User(10, "小霞", "女")); 4 UserAdapter adapter = new UserAdapter(this, R.layout.list_item, users); 5 listView.setAdapter(adapter);
如果我们将ArrayAdapter绑定的数据类型定义为Object,我们可以自由的传入任何类型的容器而不需要任何有关类型转换的操作!
ArrayAdapter不仅仅是可以显示TextView,它当让也像是其他Adapter一样,可以显示任何其他非TextView的组件:
1 @Override 2 protected void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 setContentView(R.layout.activity_main); 5 ListView listView = (ListView) this.findViewById(R.id.list); 6 List<Object> users = new ArrayList<Object>(); 7 users.add(10); 8 users.add(11); 9 UserAdapter adapter = new UserAdapter(this, R.layout.list_item, 10 R.id.info, users); 11 listView.setAdapter(adapter); 12 } 13 14 @Override 15 public boolean onCreateOptionsMenu(Menu menu) { 16 // Inflate the menu; this adds items to the action bar if it is present. 17 getMenuInflater().inflate(R.menu.main, menu); 18 return true; 19 } 20 21 class UserAdapter extends ArrayAdapter<Object> { 22 private int mResourceId; 23 24 public UserAdapter(Context context, int resourceId, 25 int textViewResourceId, List<Object> users) { 26 super(context, resourceId, textViewResourceId, users); 27 this.mResourceId = resourceId; 28 } 29 30 @Override 31 public View getView(int position, View convertView, ViewGroup parent) { 32 Object user = getItem(position); 33 LayoutInflater inflater = getLayoutInflater(); 34 View view = inflater.inflate(mResourceId, null); 35 36 TextView text = (TextView) view.findViewById(R.id.info); 37 text.setText(user.toString()); 38 return view; 39 } 40 }
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="vertical" > 6 7 <Button 8 android:layout_width="wrap_content" 9 android:layout_height="wrap_content" 10 android:text="点击" /> 11 12 <TextView 13 android:id="@+id/info" 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" /> 16 17 </LinearLayout>
如果我们的布局中需要其他组件,必须指定该布局中用于显示ArrayAdapter中数据的TextView的Id。
如果只是方便绑定数据的话,其实是没有必要专门独立个ArrayAdapter出来,只要覆写getView()就可以,正如使用容器就是为了方便大量数据的处理一样的道理,使用ArrayAdapter也是为了处理数据较大的情况,像是超过100条或者频繁动态增删数据时,就可以使用ArrayAdapter,而且,为了方便我们刷新UI,ArrayAdapter也提供了setNotifyOnChange()方法,这样可以降低UI的处理量,使得刷新UI更加快速,主要是通过停止对add,insert,remove和clear的操作来实现这点。
1.2、SimpleAdapter
固定接口,直接传入一个List<? extends Map<String, ?>> data,无add接口。
要构造一个SimpleAdapter,需要以下的参数:
1.Context context:上下文,这个是每个组件都需要的,它指明了SimpleAdapter关联的View的运行环境,也就是我们当前的Activity。
2.List<? extends Map<String, ?>> data:这是一个由Map组成的List,在该List中的每个条目对应ListView的一行,每一个Map中包含的就是所有在from参数中指定的key。
3.int resource:定义列表项的布局文件的资源ID,该资源文件至少应该包含在to参数中定义的ID。
4.String[] from:将被添加到Map映射上的key。
5.int[] to:将绑定数据的视图的ID跟from参数对应,这些被绑定的视图元素应该全是TextView。
1 private ListView mListView; 2 private LinearLayout mLayout; 3 4 @Override 5 protected void onCreate(Bundle savedInstanceState) { 6 super.onCreate(savedInstanceState); 7 8 mLayout = new LinearLayout(this); 9 mLayout.setOrientation(LinearLayout.VERTICAL); 10 mListView = new ListView(this); 11 LinearLayout.LayoutParams param = new LinearLayout.LayoutParams( 12 LinearLayout.LayoutParams.MATCH_PARENT, 13 LinearLayout.LayoutParams.WRAP_CONTENT); 14 mLayout.addView(mListView, param); 15 setContentView(mLayout); 16 Map<String, String> keyValuePair = new HashMap<String, String>(); 17 keyValuePair.put("Name", "小智"); 18 keyValuePair.put("Age", "10"); 19 List<Map<String, String>> list = new ArrayList<Map<String, String>>(); 20 list.add(keyValuePair); 21 22 ListAdapter adapter = new SimpleAdapter(this, list, 23 android.R.layout.simple_list_item_2, new String[] { "Name", 24 "Age" }, new int[] { android.R.id.text1, 25 android.R.id.text2 }); 26 27 mListView.setAdapter(adapter); 28 } 29 30 @Override 31 public boolean onCreateOptionsMenu(Menu menu) { 32 // Inflate the menu; this adds items to the action bar if it is present. 33 getMenuInflater().inflate(R.menu.main, menu); 34 return true; 35 }
上面的例子中我们是手动的添加视图,然后使用的是系统默认的视图元素,像是android.R.id.text1。当然,我们也可以自定义TextView的样式,而且,说是应该全是TextView,也只是应该,并不是绝对的:
1 @Override 2 protected void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 setContentView(R.layout.activity_main); 5 6 ListView listView = (ListView) this.findViewById(R.id.list); 7 List<Map<String, ?>> list = new ArrayList<Map<String, ?>>(); 8 for (int i = 0; i < 5; i++) { 9 Map<String, String> keyValuePair = new HashMap<String, String>(); 10 keyValuePair.put("Text", "Text" + i); 11 keyValuePair.put("Button", "Button" + i); 12 list.add(keyValuePair); 13 } 14 15 ListAdapter adapter = new SimpleAdapter(this, list, R.layout.listitem, 16 new String[] { "Text", "Button" }, new int[] { R.id.text, 17 R.id.button }); 18 19 listView.setAdapter(adapter);
从这里我们可以看到,要想使用ListView,我们应用程序的主界面必须包含ListView,然后ListView的内容可以自己定义,而不仅仅是TextView。
要想知道这是什么回事,我们就要知道SimpleAdapter是如何绑定数据到视图的,这个过程我们甚至可以自定义:
1 @Override 2 protected void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 setContentView(R.layout.activity_main); 5 6 ListView listView = (ListView) this.findViewById(R.id.list); 7 List<Map<String, String>> list = new ArrayList<Map<String, String>>(); 8 for (int i = 0; i < 3; i++) { 9 Map<String, String> keyValuePair = new HashMap<String, String>(); 10 keyValuePair.put("text", "text" + i); 11 list.add(keyValuePair); 12 } 13 14 CustomSimpleAdapter adapter = new CustomSimpleAdapter(this, list, 15 R.layout.listitem); 16 17 listView.setAdapter(adapter);
1 class CustomSimpleAdapter extends SimpleAdapter { 2 private int mResource; 3 private List<? extends Map<String, ?>> mData; 4 5 public CustomSimpleAdapter(Context context, 6 List<? extends Map<String, ?>> data, int resource) { 7 super(context, data, resource, null, null); 8 this.mResource = resource; 9 this.mData = data; 10 } 11 12 @Override 13 public View getView(int position, View convertView, ViewGroup group) { 14 LayoutInflater layoutInflater = getLayoutInflater(); 15 View view = layoutInflater.inflate(mResource, null); 16 TextView text = (TextView) view.findViewById(R.id.text); 17 text.setText(mData.get(position).get("text").toString()); 18 if (position == 2) { 19 text.setTextColor(Color.RED); 20 } 21 return view; 22 } 23 }
要想实现自定义的ListView,最主要的是实现getView(),因为SimpleAdapter的数据绑定就是发生在这里。
现在我们可以总结一下SimpleAdapter的数据绑定是怎样的:利用传入的view(该view包含ListView每行要渲染的视图元素)的ResourceID得到该view,然后通过每个vie所在的索引,也就是它们的行数,得到data中相应内容的key,接着就是利用这些key的value填充这些视图元素,最后返回view作为ListView每行的内容显示出来。
由此可见,from和to并不是必须的,要想实现ListView,前三个参数才是必要的,也许大家会看到网上有些例子为了实现自定义的SimpleAdapter,会覆写它的许多方法,其实如果单纯只是想要利用SimpleAdapter来实现自定义的ListView,只要覆写getView()就行,其他的完全可以交给SimpleAdapter原先的方法来做,除非我们有特殊的要求。
SimpleAdapter并不仅仅用在ListView上,事实上,Spinner同样可以使用:
1 @Override 2 protected void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 setContentView(R.layout.activity_main); 5 6 Spinner spinner = (Spinner) this.findViewById(R.id.spinner); 7 List<Map<String, ?>> list = new ArrayList<Map<String, ?>>(); 8 for (int i = 0; i < 5; i++) { 9 Map<String, String> keyValuePair = new HashMap<String, String>(); 10 keyValuePair.put("Text", "Text" + i); 11 list.add(keyValuePair); 12 } 13 14 SimpleAdapter adapter = new SimpleAdapter(this, list, 15 R.layout.listitem, new String[] { "Text" }, 16 new int[] { R.id.text }); 17 18 spinner.setAdapter(adapter); 19 }
1.3、 SimpleCursorAdapter
SimpleCursorAdapter,用于将Cursor中的columns与XML文件中定义的TextView或者ImageView进行匹配的Adapter。
1 @Override 2 protected void onCreate(Bundle savedInstanceState) { 3 super.onCreate(savedInstanceState); 4 setContentView(R.layout.activity_main); 5 6 Map<String, String> map = new HashMap<String, String>(); 7 8 ListView listView = (ListView) this.findViewById(R.id.list); 9 Cursor cursor = getContentResolver().query( 10 ContactsContract.Contacts.CONTENT_URI, null, null, null, null); 11 if(cursor != null){ 12 13 startManagingCursor(cursor); 14 15 } 16 ListAdapter adapter = new SimpleCursorAdapter(this, 17 android.R.layout.simple_list_item_1, cursor, 18 new String[] { PhoneLookup.DISPLAY_NAME }, 19 new int[] { android.R.id.text1 }); 20 listView.setAdapter(adapter); 21 stopManagingCursor(); 22 }
这只是简单的获取联系人姓名的例子而已,当然,为了能够运行该例子,我们需要添加下面的权限:
<uses-permission android:name="android.permission.READ_CONTACTS" />
这里有一个方法很值得我们注意:startManagingCursor()。它的使用是基于这样的前提:游标结果集里有很多的数据记录,像是通讯录这样的结果集,肯定符合要求。使用该方法的目标主要是把获取的Cursor对象交给Activity管理,这样Cursor的生命周期就和Activity自动同步了,这样在Activity结束的时候就能自动结束Cursor的使用。使用前最好是先判断Cursor是否为空,以免发生错误,而且使用后也要用stopManagingCursor()方法来停掉它。
SimpleCursorAdapter除了数据来源指定是数据库之外,就和SimpleAdapter的用法几乎一样了。
1.4、ListView工作原理
ListView针对每个item,要求adapter返回一个视图(getView),也就是说ListView在开始绘制的时候,系统首先调用getCount()函数,根据其返回值得到ListView的长度,然后根据这个长度调用getView()一行一行的绘制ListView的每一项。如果getCount()返回值是0,则列表一行都不会显示,如果返回1,就只显示一行。如果有几千几万甚至更多的item要显示怎么办?为每个item创建一个新的View?不可能。实际上Android早已经缓存了这些视图,如下图所示。
- 如果有很多item时,只有可见的项目存在内存中,其他的在Recycler中。
- ListView先请求一个type1视图(getView()),然后请求其他可见的项目。convertView在getView中是null的。
- 当item1滚出屏幕,且一个新的项目从屏幕低端上来时,ListView再请求一个type1视图。convertView此时不是空值了,它的值是item1.只需设定新的数据然后返回convertView,不必重新创建一个视图。
下面通过示例代码来具体演示一下。
1 public class MultipleItemsList extends ListActivity { 2 private MyCustomAdapter mAdapter; 3 @Override 4 public void onCreate(Bundle savedInstanceState) { 5 super.onCreate(savedInstanceState); 6 mAdapter = new MyCustomAdapter(); 7 for (int i = 0; i < 50; i++) { 8 mAdapter.addItem("item " + i); 9 } 10 setListAdapter(mAdapter); 11 } 12 13 private class MyCustomAdapter extends BaseAdapter { 14 private ArrayList<String> mData = new ArrayList<String>(); 15 private LayoutInflater mInflater; 16 17 public MyCustomAdapter() { 18 mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 19 } 20 21 public void addItem(final String item) { 22 mData.add(item); 23 notifyDataSetChanged(); 24 } 25 26 @Override 27 public int getCount() { 28 return mData.size(); 29 } 30 31 @Override 32 public String getItem(int position) { 33 return (String) mData.get(position); 34 } 35 36 @Override 37 public long getItemId(int position) { 38 return position; 39 } 40 41 @Override 42 public View getView(int position, View convertView, ViewGroup parent) { 43 System.out.println("getView " + position + " " + convertView); 44 ViewHolder holder = null; 45 if (convertView == null) { 46 convertView = mInflater.inflate(R.layout.listview, null); 47 holder = new ViewHolder(); 48 holder.textView = (TextView) convertView.findViewById(R.id.text); 49 convertView.setTag(holder); 50 } else { 51 holder = (ViewHolder) convertView.getTag(); 52 } 53 holder.textView.setText(mData.get(position)); 54 return convertView; 55 } 56 } 57 58 public static class ViewHolder { 59 public TextView textView; 60 } 61 }
执行程序,查看日志:
getView 被调用 9 次 ,convertView 对于所有的可见项目是空值(如下):
然后稍微向下滚动List,直到item10出现:
convertView仍然是空值,因为recycler中没有视图(item1的边缘仍然可见,在顶端)再滚动列表,继续滚动:
convertView不是空值了!item1离开屏幕到Recycler中去了,然后item11被创建,再滚动下:
此时的convertView非空了,在item11离开屏幕之后,它的视图(…0f8)作为convertView容纳item12了,
2、ListView优化
2.1、复用convertView,减少findViewById的次数
- 复用convertView
Android系统本身为我们考虑了ListView的优化问题,在复写的Adapter的类中,比较重要的两个方法是getCount()和getView()。界面上有多少个条目显示,就好调用多少次的getView()方法;因此如果在每次调用的时候,如果不进行优化,每次都会使用View.inflate(...)的方法,都要将xml文件解析并显示到界面上,这是非常消耗资源的:因为有新的内容产生就会有旧的内容销毁,因此可以复用旧的内容。
优化:在getView()方法中,系统为我们提供了一个复用view的历史缓存对象convertView,当显示第一屏的时候,每一个item都会新创建一个view对象,这些view都是可以被复用的。所以可以在convertView不为null的时候,对其进行复用。
2.缓存item条目的引用——ViewHolder
findViewById()这个方法是比较耗性能的操作,因此这个方法要找到指定的布局文件,进行不断地解析每个节点:从最顶端的节点进行一层一层的解析查询,找到后再一层一层的返回,如果在左边没找到,就会继续解析右边。因此可以对findViewById进行优化处理,需要注意的是:
特点:xml文件被解析的时候,只要被创建出来了,其孩子的id就不会改变了。根据这个特点,可以将孩子id存人到制定的集合中,每次可以直接取出集合中对应的元素就可以了。
优化:在创建view对象的时候,减少布局文件转化成view对象的次数;即在创建view对象的时候,把所有孩子全部找到,并把孩子的引用存起来。具体方法如下:
1、定义存储控件引用的类ViewHolder
这里的ViewHolder类是否需要定义成static,要根据实际情况而定,如果item不是很多的话,可以使用,这样在初始化的时候只加载一次。不过如果item过多的话,建议不要使用。因此static是Java中的一个关键字,当用它来修饰成员变量时,该变量就属于该类,而不是该类的实例。所以用static修饰的变量生命周期很长,如果用它来引用一些资源消耗过多的实例,会出现问题。
1 class ViewHolder{ 2 //定义item中相应的控件 3 }
2、创建自定义的类:ViewHolder holder = null;
3、将子View添加到holder中。在创建新的listView时,创建新的ViewHolder,把所有孩子全部找到,并把孩子的引用存起来;通过view.setTag(holder)将引用设置到view中;通过holder,将孩子view设置到此holder中,从而减少以后查询的次数。
4、在复用listView中的条目时,通过view.getTag(),将view对象转化为holder,即转化成相应的引用,方便在下次使用的时候存入集合。通过view.getTag(holder)获取引用。
1 @Override 2 public View getView(int position, View convertView, ViewGroup parent) { 3 View view; 4 ViewHolder holder; 5 // 判断convertView的状态,来达到复用效果 6 if (null == convertView) { 7 // 如果convertView为空,则表示第一次显示该条目,需要创建一个view 8 view = View.inflate(MainActivity.this, R.layout.listview_item, 9 null); 10 //新建一个viewholder对象 11 holder = new ViewHolder(); 12 //将findviewbyID的结果赋值给holder对应的成员变量 13 holder.tvHolder = (TextView) view.findViewById(R.id.tv_item); 14 // 将holder与view进行绑定 15 view.setTag(holder); 16 } else { 17 // 否则表示可以复用convertView 18 view = convertView; 19 holder = (ViewHolder) view.getTag(); 20 } 21 // 直接操作holder中的成员变量即可,不需要每次都findViewById 22 holder.tvHolder.setText(list.get(position)); 23 return view; 24 }
根据Google的文档,实际优化效果在百分之5左右。
2.2、ListView中数据的分批及分页加载
需求:ListView有一万条数据,如何显示;如果将十万条数据加载到内存,很消耗内存。
解决方法:优化查询的数据,先获取几条数据显示到界面上。
进行分批处理——优化了用户体验。如:1000条新闻的List集合,一次加载20条,等到翻页到底部时,再添加下面20条。这样用户一次只需要等待20条数据的传输时间,不需要一次等待好几分钟把数据都加载玩再在ListView上显示。
进行分页处理——优化了内存空间。如:假如有10万条数据,如果顺利读到最后这个List集合中还是会累积海量条数据,有可能会造成OOM。这时要用到分页,比如可以将这10万条数据分为1000页,每一页100条数据,每一页加载时都覆盖掉上一页中List集合中的内容,然后每一页内再使用分批加载。
说明:一般数据都是从数据库中获取的,实现分批(分页)加载数据,就需要在对应的DAO中有相应的分批(分页)获取数据的方法,如findPartDatas()
- 准备数据:在dao中添加分批加载数据的方法findPartDatas(),在适配数据的时候先加载第一批的数据,需要加载第二批的时候,设置监听检测何时加载第二批。
- 设置ListView的滚动监听器:setOnScrollListener(new OnScrollListener{...})
- 在监听器中有两个方法:滚动状态发生变化的方法(onScrollStateChanged)和listView被滚动时调用的方法(onScroll)
- 在滚动状态发生改变的方法中,有三种状态:
手指按下移动的状态: SCROLL_STATE_TOUCH_SCROLL: // 触摸滑动
惯性滚动(滑翔(flgin)状态): SCROLL_STATE_FLING: // 滑翔
静止状态: SCROLL_STATE_IDLE: // 静止
- 对不同的状态进行处理:分批加载数据,只关心静止状态;关心最后一个可见的条目,如果最后一个可见条目就是数据适配器(集合)里的最后一个,此时可加载更多的数据。在在每次加载的时候,计算出滚动的数量,当滚动的数量大于等于总数量的时候,可以提示用户无更多数据。
2.3、复杂ListView的处理:
说明:ListView的界面显示是通过getCount和getView这两个方法来控制的。getCount返回有多少个条目;getView返回每个位置条目显示的内容。
提供思路:对于含有多个类型的item的优化处理:由于ListView只有一个Adapter的入口,可以定义一个总的Adapter入口,存放各种类型的Adapter。
- 定义两个(或多个)集合:每个集合中存入的是对应不同类型的内容
- 在初始化数据(填充数据)中初始化两个集合
- 在数据适配器中,复写对应的方法。getCount():计算所有需要显示的条目的个数,这里包括listView和textView;getView():对显示在不同位置的条目进行if处理。
- 数据类型的判断:需要注意的是,在复用view的时候,需要对convertView进行类型判断,是因为这里含有各种不同类型的view,在view滚动显示的时候,对于不同类型的view不能复用,所以需要类型判断。
2.4、ListView中图片的优化:
- 处理图片的方式:如果自定义Item中有涉及到图片等等的,一定要注意处理图片,图片占的内存是ListView项中最恶心的,处理图片的方法大致有以下几种:
- 不要直接拿路径就去循环decodeFile();使用Option保存图片大小、不要加载图片到内存中
- 拿到的图片一定要经过边界压缩
- 在ListView中取图片时也不要直接拿个路径去取图片,而是以WeakReference、SoftReference、WeakHashMap等来存储图片信息(注:是图片信息不是图片)
- 在getView中做图片转换时,产生的中间变量一定及时释放
- 异步加载图片基本思想:
- 先从内存缓存中获取图片显示(内存缓存)
- 获取不到的话从SD卡中获取(SD卡缓存)
- 都获取不到的话从网络下载图片并保存到SD卡同时加入内存并显示
原理:
优化一:先从内存中加载,没有则开启线程从SD卡或网络中获取,这里注意从SD卡获取图片是放在子线程里执行的,否则快速滑屏的话会不够流畅。
优化二:与此同时,在adapter里有个busy变量,表示listview是否处于滑动状态,如果是滑动状态则仅从内存中获取图片,没有的话无需再开启线程去外存或网络获取图片
优化三:ImageLoader里的线程使用了线程池,从而避免了过多线程频繁创建和销毁。不能每次总是new一个线程去执行,可以使用AsyncTask类。在从网络获取图片时,先是将其保存到sd卡,然后再加载到内存,这么做的好处是在加载到内存时可以做个压缩处理,以减少图片所占内存。
Tips:这里可能出现图片错位问题:
图片错位问题的本质源于我们的listview使用了缓存convertView,假设一种场景,一个listview一屏显示九个item,那么在拉出第十个item时,事实上该item是重复使用了第一个item,也就是说在第一个item从网络中下载图片并最终要显示的时候,其实该item已经不在当前显示区域内了,此时显示的后果将可能在第十个item上输出图像,这就导致了图片错位的问题。所以解决之道在于可见则显示,不可见则不显示。在ImageLoader里有个imageView的map对象,就是用于保存当前显示区域图像对应的url集,在显示前判断处理一下即可。
3.内存缓冲机制:
首先限制内存图片缓冲的堆内存大小,每次有图片往缓存里添加时,判断是否超过限制大小,超过的话就从中取出最少使用的图片并将其移除。
当然这里如果不采用这种方式,换做软引用也是可行的,二者目的皆是最大程度的利用已存在于内存中的图片缓存,避免重复制造垃圾增加GC负担。
OOM溢出往往皆因内存瞬时大量增加而垃圾回收不及时造成。只不过二者区别在于LinkedHashMap里的图片缓存在没有移除出去之前是不会被GC回收的,而SoftReference里的图片缓存在没有其他引用保存时随时都会被GC回收。所以在使用LinkedHashMap这种LRU算法缓存更有利于图片的有效命中,当然二者配合使用的话效果更佳,即从LinkedHashMap里移除出的缓存放到SoftReference里,这就是内存的二级缓存。
2.5、ListView的其他优化:
- 尽量避免在BashAdapter中使用static来定义全局静态变量。
static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(比如Context的情况最多),这时就要尽量避免使用了。 - 尽量使用getApplicationContext:
如果为了满足需求下必须使用Context的话,Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。 - 尽量避免在ListView适配器中使用线程:
因为线程产生内存泄露的主要原因在于线程生命周期的不可控制。之前使用的自定义ListView中适配数据时使用AsyncTask自行开启线程的,这个比用Thread更危险,因为Thread只有在run函数不结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了线程执行池(ThreadPoolExcutor),这个类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。
解决方法如下:- 将线程的内部类,改为静态内部类。
- 在线程内部采用弱引用保存Context引用
参考: http://mobile.51cto.com/abased-410889.htm
http://www.cnblogs.com/wenjiang/p/3196205.html
http://www.cnblogs.com/wenjiang/p/3189082.html