这一篇blog是接着上一篇,上一篇有一些不完美,这里做出改进。
首先我们需要理清思路:使用ListView显示数据是很方便的,ListVIew的数据之间通过适配器adapter去作为桥梁连接起来。当我们需要使用listview显示大量数据的时候,我们需要使用到分页功能,比如我们有一千条数据,那么我们应该分开数据一点一点的显示,比如每次用户刷新我就增加20条数据额、展示给用户,每次都是增加一定量的数据给用户,直到数据没有为止。为了改善用户体验,我们还应该把上一次用户退出的时候显示最新的20条数据保留本地,用户下次点进来就可以直接看到那些数据,用户点击刷新的时候再去加载本次位于服务器的最新20数据给用户,然后把原来的数据覆盖掉。然后用户下拉到20条数据底端的时候再去加载那之后的20条数据,这样往复循环就可以实现了。本人这次只是模拟。
首先,如何判断用户需要加载新的数据,就是需要监听listview的滚动事件,当用户滚动到最底屏的时候,有一个可以唯一确定的是,当前用户屏幕显示的最后一条数据的position+1等于总的数据量,这时候,我们就应该去添加新的数据,开一条新的线程完成该工作。然后利用handler去把数据添加到适配器的data中,然后调用adapter.notifyDataSetChanged()就可以更新ListVIew了。但是这里有一些问题需要注意的
1,我们需要保证我们当期用户点击下拉刷新之后下载好的数据全部加载到适配器的data的时候才可以去加载新的数据,这是因为某些用户可能很急躁,不断的下拉listview,导致不断的是的屏幕最后一条数据+1等于总的数据量,不断去触发线程下载新数据,所以,我们需要有一个flag去控制。
2,所有的数据下载都需要开线程去完成或者是使用异步任务,下载图片的时候建议使用异步任务,以为当用户快速滚动的时候会开很多的线程下载图片,异步任务能控制线程数量,或者使用Imageloder框架
3,缓存图片,每一次我们去加载新的资源的时候我们就需要把最新的20条数据覆盖原来的,保存本地,他应该在每一次去请求数据的时候开一条线程去完成。主要注意的是每一次请求的输入流只能使用一次,所以这里既需要写入本地有需要写入内存,所以需要请求两次来获得两个数据流,这里可以看getData()方法。
详细代码:
MainActivity
public class MainActivity extends Activity { // 每次都保证把最新的数据保存到本地,下次用户点开的时候就可以直接显示这些以前最新的数据, // 当用户下拉刷新之后,在把下拉刷新之后的数据读入到该文件中,每次用户点开都是上次最新的不过现在没有刷新的文件 public static final File saveXMLToSD = new File(Environment.getExternalStorageDirectory(), "list.xml"); private ListView listview;// listview private File cache;// 缓存目录 public static final int OK = 1;// 成功获得数据 public static final int YES = 2;// 成功获得最新数据数据 public static final int ERROR = -1;// 失败获得数据 private boolean flag = true; private View footer; private ListAdapter adapter; private boolean isFinsh = false; // 负责当数据完成下载之后绑定适配器,用户首次使用点击屏幕就不会有异常 private Handler mHandler = new Handler() { @SuppressWarnings("unchecked") public void handleMessage(android.os.Message msg) { if (msg.what == OK) { adapter = new ListAdapter(MainActivity.this, R.layout.list_item, cache, (List<Contacts>) msg.obj); System.out.println("(List<Contacts>) msg.obj" + ((List<Contacts>) msg.obj).size()); listview.addFooterView(footer);// 添加页脚,用于改善用户体验 listview.setAdapter(adapter);// 绑定适配器 listview.removeFooterView(footer);// 首次不用显式页脚 } if (msg.what == YES)// 成功加载最新数据 { flag = true;//这时候才可以继续去下载数据 adapter.setData((List<Contacts>) msg.obj); adapter.notifyDataSetChanged();// 通知数据更改成功,更新ListView if (listview.getFooterViewsCount() > 0) listview.removeFooterView(footer);// 有页脚存在则去除,此时已经完全加载数据 } if (msg.what == ERROR) { Toast.makeText(getApplicationContext(), "网络连接失败", Toast.LENGTH_SHORT).show(); } }; }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listview = (ListView) findViewById(R.id.listview); cache = new File(Environment.getExternalStorageDirectory(), "cahce"); if (!cache.exists()) cache.mkdirs(); footer = getLayoutInflater().inflate(R.layout.footer, null);// 加载页脚 listview.setOnScrollListener(new ListViewScrollListener());// 监听滚动事件 new Thread(new Runnable() { public void run() { try { List<Contacts> data = new ArrayList<Contacts>(); if (!saveXMLToSD.exists()) { // data.addAll(ContactsService.getData());// // 首次文件不存在的时候加载数据 data.addAll(ContactsService.getData()); } else { // 首次数据存在的时候从用户的xml文件中读取数据,这样给用户比较快的感觉,每一次登陆都显示上一次登陆的结果 FileInputStream fis = new FileInputStream(saveXMLToSD); // data = ContactsService.parserXML(fis); data.addAll(ContactsService.parserXML(fis)); } Message msg = Message.obtain(); msg.what = OK; msg.obj = data; mHandler.sendMessage(msg);// 成功发送消息 } catch (Exception e) { mHandler.sendEmptyMessage(ERROR); e.printStackTrace(); } } }).start(); } class ListViewScrollListener implements OnScrollListener { public void onScrollStateChanged(AbsListView view, int scrollState) { } /** * firstVisibleItem 屏幕中第一个可见的item的position * visibleItemCount 屏幕中可见的item的数量, * totalItemCount 一共有的数据总量 */ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { int lastItemId = listview.getLastVisiblePosition();//屏幕中最后的一个可视的item的position System.out.println("lastItemId=" + lastItemId + "firstVisibleItem=" + firstVisibleItem + "visibleItemCount=" + visibleItemCount + "totalItemCount=" + totalItemCount); // 当可视的item的最后一条达到了总数目则说明用户已经达到了数据的最低部,这时候应该从网络获取最新数据 if (lastItemId + 1 == totalItemCount && totalItemCount > 0) { if (flag) { flag = false;//防止用户不断下拉listview,所以一旦有一次下拉到最低端之后数据下载之后,马上设为false,只有数据下载完成之后才在handlr中设置为true // 这时候把标志值为true,因为下拉底部的时候需要把这一次的下拉完全获取玩之前不去再次加载,而获取是使用线程的,需要时间,所以必须先要把该标志值为true listview.addFooterView(footer); // 开启线程加载最新的数据 new Thread(new Runnable() { @Override public void run() { try { Thread.currentThread().sleep(3000); List<Contacts> data = ContactsService.getData(); Message msg = Message.obtain(); msg.what = YES; msg.obj = data; mHandler.sendMessage(msg); } catch (Exception e) { mHandler.sendEmptyMessage(-1); e.printStackTrace(); } } }).start(); } } } } // 当用户退出当前应用的时候把所有的缓存图片删除 @Override protected void onDestroy() { if (cache.exists()) { for (File item : cache.listFiles()) { item.delete(); } cache.delete(); } super.onDestroy();// 一定需要这句,否则会失败 } }
adapter,下载图片,绑定数据,更新数据
public class ListAdapter extends BaseAdapter { public List<Contacts> data = new ArrayList<Contacts>(); private int listItem; private File cache; private LayoutInflater inflater; private Contacts contact; private static ImageView imageview; /** * @return the data */ public List<Contacts> getData() { return data; } /** * @param data the data to set */ public void setData(List<Contacts> data) { this.data.addAll(data); } public ListAdapter (Context mainActivity , int listItem , File cache , List<Contacts> data) { this.data.addAll(data); this.listItem = listItem; this.cache = cache; inflater = (LayoutInflater) mainActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public int getCount() { return data.size(); } @Override public Object getItem(int position) { return data.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (null == convertView) { convertView = inflater.inflate(listItem, null); holder = new ViewHolder(); holder.imageview = (ImageView) convertView.findViewById(R.id.imageview); holder.textView = (TextView) convertView.findViewById(R.id.textview); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } contact = data.get(position); holder.textView.setText(contact.name); imageview = holder.imageview; loadImageView(contact.path); return convertView; } static class ViewHolder { ImageView imageview; TextView textView; } private void loadImageView(String path) { new MyAsyncTask(imageview).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, path); } private class MyAsyncTask extends AsyncTask<String, Void, Uri> { private ImageView imageView; public MyAsyncTask (ImageView imageView) { super(); this.imageView = imageView; } //后台下载图片,使用线程池控制,也可以线程对象的重用 @Override protected Uri doInBackground(String... params) { String path = params[0]; try { Uri uri = ContactsService.loadSaveImage(path, cache); return uri; } catch (Exception e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(Uri result) { if(result!= null && imageView != null) imageView.setImageURI(result);//运行在UI线程直接更新ImageView else if(imageView != null) { imageView.setImageResource(R.drawable.ic_launcher);//默认图片 } } } }
service,下载数据,缓存图片,解析XML
public class ContactsService { /** * 返回最新数据 list集合返回 * * @return * @throws Exception * @throws IOException */ public static List<Contacts> getData() throws Exception, IOException { String pathXML = "http://10.10.117.197:8080/web/list2.xml"; HttpClient client = new DefaultHttpClient(); HttpPost post = new HttpPost(pathXML); HttpResponse httpResponse = client.execute(post); if (200 == httpResponse.getStatusLine().getStatusCode()) { HttpEntity entity = httpResponse.getEntity(); final InputStream content = entity.getContent(); new Thread(new Runnable() { public void run() { saveToSD(content);// 每一次实现下载最新的数据的同时需要去保存本地 } }).start(); return parserXML(client.execute(post).getEntity().getContent());// 返回最新的数据给listview显示 } return null; } /** * 解析XML数据,并以集合返回 * @param content * @return * @throws Exception */ public static List<Contacts> parserXML(InputStream content) throws Exception { XmlPullParser parser = Xml.newPullParser(); parser.setInput(content, "UTF-8"); int event = parser.getEventType(); List<Contacts> data = new ArrayList<Contacts>(); Contacts item = null; int i = 1; while (event != XmlPullParser.END_DOCUMENT) { switch (event) { case XmlPullParser.START_TAG: if ("contact".equals(parser.getName())) { item = new Contacts(); item.id = Integer.valueOf(parser.getAttributeValue(0)); break; } if ("name".equals(parser.getName())) { item.name = parser.nextText()+i++; break; } if ("image".equals(parser.getName())) { item.path = parser.getAttributeValue(0); break; } break; case XmlPullParser.END_TAG: if ("contact".equals(parser.getName())) { data.add(item); item = null; } break; } event = parser.next(); } return data; } /** * 保存最新的数据到本地 * * @param content * 输入流 */ private static void saveToSD(InputStream content) { try { FileOutputStream fos = new FileOutputStream(MainActivity.saveXMLToSD); int len; byte[] buffer = new byte[1024]; while ((len = content.read(buffer)) != -1) { fos.write(buffer, 0, len); } fos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * 缓存图片 * * @param path * 下载路径 * @param cache * 缓存目录 * @return * @throws Exception */ public static Uri loadSaveImage(String path, File cache) throws Exception { File localFile = new File(cache, MD5.getMD5(path) + path.substring(path.lastIndexOf("."))); if (localFile.exists()) { return Uri.fromFile(localFile); } else { BufferedOutputStream localFileBufferedOutputStream = null; HttpResponse httpResponse = null; FileOutputStream localFileOutputStream = new FileOutputStream(localFile); localFileBufferedOutputStream = new BufferedOutputStream(localFileOutputStream); HttpClient client = new DefaultHttpClient(); HttpPost post = new HttpPost(path); httpResponse = client.execute(post); if (200 == httpResponse.getStatusLine().getStatusCode()) { InputStream content = null; try { HttpEntity entity = httpResponse.getEntity(); content = entity.getContent(); int len; byte[] buffer = new byte[1024]; while ((len = content.read(buffer)) != -1) { localFileBufferedOutputStream.write(buffer, 0, len); localFileBufferedOutputStream.flush(); } return Uri.fromFile(localFile); } catch (IllegalStateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { localFileBufferedOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } return null; } }
domain
public class Contacts { public int id; public String name; public String path; public Contacts (){} public Contacts (int id , String name , String path) { super(); this.id = id; this.name = name; this.path = path; } @Override public boolean equals(Object o) { return false; } }
Util类 MD5命名
public class MD5 { public static String getMD5(String content) { try { MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(content.getBytes()); return getHashString(digest); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } private static String getHashString(MessageDigest digest) { StringBuilder builder = new StringBuilder(); for (byte b : digest.digest()) { builder.append(Integer.toHexString((b >> 4) & 0xf)); builder.append(Integer.toHexString(b & 0xf)); } return builder.toString(); } }
layout文件
main
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.asynctasklistdemo.MainActivity" > <ListView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/listview" /> </LinearLayout>
listview的item
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="horizontal" > <ImageView android:id="@+id/imageview" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="center" android:src="@drawable/ic_launcher" /> <TextView android:id="@+id/textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:textColor="#000000" android:textSize="40sp" /> </LinearLayout>
页脚footer
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="horizontal"> <ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="数据正在加载。。。" android:textSize="20sp" android:layout_gravity="center" /> </LinearLayout>
权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
结果截图: