Androi手动实现ListView+ImagLoader功能
三级缓概念:
一级缓存:内存缓存,bitmap对象,Map<string,Bitmap> 结构保存,可以是url
二级缓存:本地(sd卡)缓存,缓存图片文件 /storage/sdcard/Android/packageName/file/图片.png
三级缓存: 1,2没有那么从服务器去取
三级缓存伪代码:
1. 根据url Key从本地中找Bitmap对象
如果有,那么显示
如果没有,那么进入2
2. 从二级缓存中查找,得到sd卡下的bitmap
如果有,显示,缓存到一级缓存
没有进入3
3. 现在正在加载图片,启动分线程联网请求bitmap
如果没有,显示错误图片
如果有: 显示
缓存到一级缓存、二级缓存
问题:ListVie使用三级缓存加载图片存在图片闪动bug
原因:conterView复用,conterView 现在A图片,去加载A图片,
滑动以后conterView被B复用,去加载B图片
滑动以后conterView别C复用,去加载C图片,
可能由于网络原因,显示B,显示A,在显示C,错误
解决方法:去加载C的时候,不要去加载A,B,即使加载完毕也不现实,存入缓存即可
问题代码分析:
// 1.ListView 复用,每次滑动重新绘制item都会执行
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView==null) {
convertView = View.inflate(MainActivity.this, R.layout.item_main, null);
}
imageLoader.loadImage(imagePath, imageView);
}
//2.Item复用以后为了可以以后 为了区分该item是否已经复用,使用保存tag方法,A用的保存A,那么B用的时候保存B
public void loadImage(String imagePath, ImageView imageView) {
//将需要显示的图片url保存到视图上
imageView.setTag(imagePath);
}
// 3.网络加载前:如果 A item被A 占据,但是此时滑动 Item已经B复用 【imageView.setTag(imagePath);】,那么不要去加载网络去图片了
String newImagePath = (String) imageView.getTag();
if(newImagePath!=imagePath) {//视图已经被复用了 【imagePath 还是A的、tag是比的,这里把1,2级别缓存都可以单做综合耗时】
return null;
}
// 4. 网络加载后,如果A item之前被A分配,网络延迟才执行完毕对于A[newImagePath], 此时已经被E复用,Tag已经E了,那么不显示A图片,把A图片缓存即可
String newImagePath = (String) imageView.getTag();
if(newImagePath!=imagePath) {//视图已经被复用了
return;
}
直接上图:
代码实现:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
1. ShopInfo 实体类保存数据
package com.example.listimageloader;
/*
* 商品的信息
*/
public class ShopInfo {
private int id;
private String name;
private String imagepath;
private double price;
public ShopInfo() {
super();
}
public ShopInfo(int id, String name, String imagepath, double price) {
super();
this.id = id;
this.name = name;
this.imagepath = imagepath;
this.price = price;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
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 String getImagepath() {
return imagepath;
}
public void setImagepath(String imagepath) {
this.imagepath = imagepath;
}
@Override
public String toString() {
return "ShopInfo [id=" + id + ", name=" + name + ", imagepath="
+ imagepath + ", price=" + price + "]";
}
}
2. ImageLoader 框架封装
package com.example.listimageloader;
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.os.SystemClock;
import android.util.Log;
import android.widget.ImageView;
/**
* 用于加载图片并显示的类
* @author 张晓飞
*
*/
/*
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<String, Bitmap> cacheMap = new HashMap<String, Bitmap>();
/**
* 加载图片并显示
* @param imagePath
* @param imageView
*/
public void loadImage(String imagePath, ImageView imageView) {
//将需要显示的图片url保存到视图上
imageView.setTag(imagePath);
/*
1). 根据url从一级缓存中取对应的bitmap对象
如果有, 显示(结束)
如果没有, 进入2)
*/
Bitmap bitmap = getFromFirstCache(imagePath);
if(bitmap!=null) {
imageView.setImageBitmap(bitmap);
return;
}
/*
2). 从二级缓存中查找: 得到文件名并在sd卡的缓存目录下加载对应的图片得到Bitmap对象
如果有: 显示, 缓存到一级缓存中(结束)
如果没有, 进入3)
/storage/sdcard/Android/data/packageName/files/图片文件名(xxx.jpg)
*/
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
* loadBitmapFromThirdCache("../b.jpg", imageView)
* loadBitmapFromThirdCache("../f.jpg", imageView)--->imageView.setTag("../f.jpg")
*/
private void loadBitmapFromThirdCache(final String imagePath, final ImageView imageView) {
new AsyncTask<Void, Void, Bitmap>() {
protected void onPreExecute() {
imageView.setImageResource(loadingImageRes);
}
//联网请求得到bitmap对象
@Override
protected Bitmap doInBackground(Void... params) {
//在分线程执行, 可能需要等待一定时间才会执行
//在等待的过程中imageView中的tag值就有可能改变了
//如果改变了, 就不应该再去加载图片(此图片此时不需要显示)
Bitmap bitmap = null;
try {
//在准备请求服务器图片之前, 判断是否需要加载
String newImagePath = (String) imageView.getTag();
if(newImagePath!=imagePath) {//视图已经被复用了
return null;
}
SystemClock.sleep(100);
//得到连接
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);
//缓存到二级缓存(分线程)
// /storage/sdcard/Android/data/packageName/files/
String filesPath = context.getExternalFilesDir(null).getAbsolutePath();
// http://192.168.10.165:8080//L05_Web/images/f10.jpg
String fileName = imagePath.substring(imagePath.lastIndexOf("/")+1);// f10.jpg
String filePath = filesPath+"/"+fileName;
Log.e("denganzhi","filePath:"+filePath);
// 格式
// 透明度 jpg 没有透明度 png 有透明度
// 质量0-100 50 压缩比例50%
bitmap.compress(CompressFormat.JPEG, 100, new FileOutputStream(filePath));
}
}
connection.disconnect();
} catch (Exception e) {
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) {
// /storage/sdcard/Android/data/packageName/files/
String filesPath = context.getExternalFilesDir(null).getAbsolutePath();
// http://192.168.10.165:8080//L05_Web/images/f10.jpg
String fileName = imagePath.substring(imagePath.lastIndexOf("/")+1);// f10.jpg
String filePath = filesPath+"/"+fileName;
return BitmapFactory.decodeFile(filePath);
}
/**
* 根据图片url从一级缓存中取对应的bitmap对象
* @param imagePath
* @return
*/
private Bitmap getFromFirstCache(String imagePath) {
return cacheMap.get(imagePath);
}
}
3. MainActivity 数据加载 设置适配器,显示数据
package com.example.listimageloader;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
List<ShopInfo> datas=new ArrayList<>();
private ShopInfoAdapter adapter;
ListView lv_main;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
List<ShopInfo> list=getAllShops();
Log.e("denganzhi",list.toString());
adapter = new ShopInfoAdapter();
lv_main = (ListView)findViewById(R.id.lv_main);
lv_main.setAdapter(adapter);
}
/*
* 得到所有商品信息对象的集合
*/
private List<ShopInfo> getAllShops() {
// 准备一个空集合
datas= new ArrayList<ShopInfo>();
// 得到images文件夹的真实路径
// String imagesPath = getServletContext().getRealPath("/images");
String imagesPath = "http://192.168.2.110:8080/Web001/images";
// 创建images文件夹File对象
// 遍历
for (int i = 1; i < 31; i++) {
// 得到商品的相关信息
int id = i + 1;
String name = "f"+i + "的商品名称";
String imagePath =imagesPath + "/f"+i+"."+"jpg";
float price = new Random().nextInt(20) + 20;
// 封装成对象
ShopInfo info = new ShopInfo(id, name, imagePath, price);
// 添加到集合中
datas.add(info);
}
return datas;
}
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 datas.size();
}
@Override
public Object getItem(int position) {
return datas.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_main, null);
}
//得到当前行的数据对象
ShopInfo shopInfo = datas.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;
}
}
}