三.过程分析 :
1. 搭建服务器端
2. 界面布局
整体 : ListView+提示视图
Item : LinearLayout
3. 动态显示列表
使用Handler+Thread处理联网请求, 得到json数据, 解析成List
使用BaseAdapter显示文本列表
根据url, 异步请求显示图片(使用三级缓存)
4. 动态显示列表中的图片
--->Bitmap--->手机本地的图片文件--->服务器端的图片文件
1). 图片的三级缓存
一级缓存: 内存缓存, 缓存的是bitmap对象, 用Map
二级缓存: 本地(sd卡)缓存, 缓存的是图片文件, /storage/sdcard/Android/data/packageName/files/图片文件名(xxx.jpg)
三级缓存: 远程服务器缓存, 缓存的是图片文件, 远程服务器上的应用中
2). 如何使用三级缓存? -----如何根据图片的url动态显示图片?
String iamgePath = http://192.168.10.165:8080//L05_Web/images/f10.jpg和ImageView对象
1). 根据url从一级缓存中取对应的bitmap对象
如果有, 显示(结束)
如果没有, 进入2)
2). 从二级缓存中查找: 得到文件名并在sd卡的缓存目录下加载对应的图片得到Bitmap对象
如果有: 显示, 缓存到一级缓存中(结束)
如果没有, 进入3)
3). 显示代表提示正在加载的图片, 启动分线程联网请求得到Bitmap对象
如果没有: 显示提示错误的图片(结束)
如果有:
显示
缓存到一级缓存
缓存到二级缓存
3). 在ListView使用图片三级缓存会存在图片闪动的bug
1). 原因
converView被复用了
2). 解决
a. 每次getView()都将图片的url保存到ImageView上: imageView.setTag(imagePath)
b. 在分线程准备请求服务器加载图片之前, 比较准备加载图片的url与ImageView中保存的最新图片的url是同一个,
如果不是同一个, 当前加载图片的任务不应该再执行
如果相同, 继续执行加载远程图片
c. 在主线程准备显示图片之前, 比较加载到图片的url与ImageView中保存的最新图片的url是同一个
如果不是同一个, 不需要显示此图片
如果相同, 显示图片
四.代码实现:
1.MainActivity.java
package com.example.apphandler;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
public class MainActivity extends Activity {
protected static final int WHAT_REQUEST_SUCCESS = 1;
protected static final int WHAT_REQUEST_ERROR = 2;
private ListView lv_main;
private LinearLayout ll_main_loading;
private List data = new ArrayList();
private ShopInfoAdapter adapter;
private Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case WHAT_REQUEST_SUCCESS:
ll_main_loading.setVisibility(View.GONE);
//显示列表
lv_main.setAdapter(adapter);
break;
case WHAT_REQUEST_ERROR:
ll_main_loading.setVisibility(View.GONE);
Toast.makeText(MainActivity.this, "加载数据失败", 1).show();
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
lv_main = (ListView) findViewById(R.id.lv_main);
ll_main_loading = (LinearLayout) findViewById(R.id.ll_main_loading);
adapter = new ShopInfoAdapter();
//1. 主线程, 显示提示视图
ll_main_loading.setVisibility(View.VISIBLE);
//2. 分线程, 联网请求
//启动分线程请求服务器动态加载数据并显示
new Thread(){
public void run() {
//联网请求得到jsonString
try {
String jsonString = requestJson();
Log.i("TAG",jsonString);
//解析成List
data = new Gson().fromJson(jsonString, new TypeToken>(){}.getType());
Log.i("TAG", data+"");
//3. 主线程, 更新界面
handler.sendEmptyMessage(WHAT_REQUEST_SUCCESS);//发请求成功的消息
} catch (Exception e) {
e.printStackTrace();
handler.sendEmptyMessage(WHAT_REQUEST_ERROR);//发送请求失败的消息
}
}
}.start();
}
/**
* 联网请求得到jsonString
* @return
* @throws Exception
*/
private String requestJson() throws Exception {
String result = null;
String path = "http://192.168.51.65:8080/L05_Web/ShopInfoListServlet";
//1. 得到连接对象
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//2. 设置
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
//连接
connection.connect();
//发请求并读取服务器返回的数据
int responseCode = connection.getResponseCode();
if(responseCode==200) {
InputStream is = connection.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.close();
is.close();
result = baos.toString();
} else {
//也可以抛出运行时异常
}
connection.disconnect();
return result;
}
class ShopInfoAdapter extends BaseAdapter {
private ImageLoader imageLoader;
public ShopInfoAdapter() {
imageLoader = new ImageLoader(MainActivity.this, R.drawable.loading, R.drawable.error);
}
@Override
public int getCount() {
return data.size();
}
@Override
public Object getItem(int position) {
return data.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView==null) {
convertView = View.inflate(MainActivity.this, R.layout.item_mian, null);
}
//得到当前行的数据对象
ShopInfo shopInfo = data.get(position);
//得到当前行的子View
TextView nameTV = (TextView) convertView.findViewById(R.id.tv_item_name);
TextView priceTV = (TextView) convertView.findViewById(R.id.tv_item_price);
ImageView imageView = (ImageView) convertView.findViewById(R.id.iv_item_icon);
//设置数据
nameTV.setText(shopInfo.getName());
priceTV.setText(shopInfo.getPrice()+"元");
String imagePath = shopInfo.getImagePath();
//根据图片路径启动分线程动态请求服务加载图片并显示
imageLoader.loadImage(imagePath, imageView);
return convertView;
}
}
}
2.ImageLoader.java
package com.example.apphandler;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;
/**
* 用于加载图片并显示的类
*
* @author Xiaocici String iamgePath =
* http://192.168.10.165:8080//L05_Web/images/f10.jpg和ImageView对象 1).
* 根据url从一级缓存中取对应的bitmap对象 如果有, 显示(结束) 如果没有, 进入2)
* 2). 从二级缓存中查找:
* 得到文件名并在sd卡的缓存目录下加载对应的图片得到Bitmap对象 如果有: 显示, 缓存到一级缓存中(结束) 如果没有, 进入3)
* 3). 显示代表提示正在加载的图片, 启动分线程联网请求得到Bitmap对象 如果没有: 显示提示错误的图片(结束) 如果有: 显示
* 缓存到一级缓存 缓存到二级缓存
*/
public class ImageLoader {
private Context context;
private int loadingImageRes;
private int errorImageRes;
public ImageLoader(Context context, int loadingImageRes, int errorImageRes
) {
super();
this.context = context;
this.loadingImageRes = loadingImageRes;
this.errorImageRes = errorImageRes;
}
// 用于缓存bitmap的容器对象
private Map cacheMap = new HashMap();
/**
* 加载图片并显示
*
* @param imagePath
* @param imageView
*/
public void loadImage(String imagePath, ImageView imageView) {
//将需要显示的图片保存在视图上
imageView.setTag(imagePath);
/**
* 1). 根据url从一级缓存中取对应的bitmap对象 如果有, 显示(结束) 如果没有, 进入2)
*/
Bitmap bitmap = getFormFirstCache(imagePath);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
/*
* 2). 从二级缓存中查找: 得到文件名并在sd卡的缓存目录下加载对应的图片得到Bitmap对象 如果有: 显示, 缓存到一级缓存中(结束)
* /storage/sdcard/Android/data/packageName/files/图片文件名(xxx.jpg) 如果没有,
* 进入3)
*/
bitmap = getFromSecondCache(imagePath);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
cacheMap.put(imagePath, bitmap);
return;
}
/*
* 3). 显示代表提示正在加载的图片, 启动分线程联网请求得到Bitmap对象 如果没有: 显示提示错误的图片(结束) 如果有:
* 缓存到一级缓存(分线程) 缓存到二级缓存 (分线程) 显示(主线程)
*/
loadBitmapFromThirdCache(imagePath, imageView);
}
/**
* 根据图片url从三级缓存中取对应的bitmap对象并显示
*
* @param imagePath
* @param imageView
* AsyncTask
*/
private void loadBitmapFromThirdCache(final String imagePath, final ImageView imageView) {
new AsyncTask(){
protected void onPreExecute() {
imageView.setImageResource(loadingImageRes);
};
//联网请求得到bitmap对象
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap bitmap = null;
try {
//在准备请求服务器图片之前, 判断是否需要加载
String newImagePath = (String) imageView.getTag();
if(newImagePath!=imagePath) {//视图已经被复用了
return null;
}
//得到连接
URL url = new URL(imagePath);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//设置
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
//连接
connection.connect();
//发请求读取返回的数据并封装为bitmap
int responseCode = connection.getResponseCode();
if(responseCode==200){
InputStream is = connection.getInputStream();//图片文件流
//将is封装为bitmap
bitmap = BitmapFactory.decodeStream(is);
is.close();
if(bitmap != null){
//缓存到一级缓存(分线程)
cacheMap.put(imagePath, bitmap);
//缓存到二级缓存 (分线程)
String filesPath = context.getExternalFilesDir(null).getAbsolutePath();
String fileName = imagePath.substring(imagePath.lastIndexOf("/") + 1);
String filePath = filesPath + "/" + fileName;
bitmap.compress(CompressFormat.JPEG, 100, new FileOutputStream(filePath));
}
}
connection.disconnect();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return bitmap;
}
protected void onPostExecute(Bitmap bitmap) {
//在主线程准备显示图片之前, 需要判断是否需要显示
String newImagePath = (String) imageView.getTag();
if(newImagePath!=imagePath) {//视图已经被复用了
return;
}
//如果没有: 显示提示错误的图片(结束)
if(bitmap == null){
imageView.setImageResource(errorImageRes);
} else {
imageView.setImageBitmap(bitmap);
}
};
}.execute();
}
/**
* 根据图片url从二级缓存中取对应的bitmap对象
*
* @param imagePath
* @return
*/
private Bitmap getFromSecondCache(String imagePath) {
Log.i("TAG", imagePath+"");
// /storage/sdcard/Android/data/packageName/files/图片文件名(xxx.jpg)
String filesPath = context.getExternalFilesDir(null).getAbsolutePath();
Log.i("TAG", imagePath+"");
String fileName = imagePath.substring(imagePath.lastIndexOf("/") + 1);
Log.i("TAG", "后");
String filePath = filesPath + "/" + fileName;
return BitmapFactory.decodeFile(filePath);
}
/**
* 根据图片url从一级缓存中取对应的bitmap对象
*
* @param imagePath
* @return
*/
private Bitmap getFormFirstCache(String imagePath) {
// TODO Auto-generated method stub
return cacheMap.get(imagePath);
}
}
package com.example.apphandler;
public class ShopInfo {
private int id;
private String name;
private double price;
private String imagePath;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public String getImagePath() {
return imagePath;
}
public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}
public ShopInfo() {
super();
// TODO Auto-generated constructor stub
}
public ShopInfo(int id, String name, double price, String imagePath) {
super();
this.id = id;
this.name = name;
this.price = price;
this.imagePath = imagePath;
}
@Override
public String toString() {
return "ShopInfo [id=" + id + ", name=" + name + ", price=" + price
+ ", imagePath=" + imagePath + "]";
}
}