图片加载是几乎每个客户端都要用到的功能,这几天闲来无事,以妹子图客户端为例学习了一下android的图片加载。现在整理一下,一来便于自己理解记忆,二来和同样希望学习这方面知识的同学交流,三来贴出自己的代码希望大家能够指点一二。
好了,废话不多说,进入正题。
数据接口来自干货集中营
网上的图片框架有很多,这里使用的是volley。不熟悉的同学可以看一下郭大神的博客
首先讲一下基本思路。
图片加载最见得粗暴的方式就是直接从网上下载图片并显示。但是每次从网上下载会非常耗时,所以一般都会加入图片的缓存来优化加载速度。缓存的话一般有两种,缓存在内存和缓存在本地磁盘。具体关于缓存的知识同样可以看一下郭大神的博客。所以整个加载图片的思路就是首先发送一次请求获取图片的url,然后加载图片,首先从内存取,内存中没有则从磁盘缓存中加载,还是没有的话则从网上下载并添加到磁盘缓存。
先上效果图
就是这样一个简单的瀑布流效果,下面的按钮我给它添加了一个返回最上面的功能。图片是默认的图片,不影响效果,我也懒得改了。大家将就一下。
好了,下面代码。
布局文件就是一个recyclerview,没什么好讲的,我就不贴了,直接上逻辑控制的代码。
mainactivity:
public class MainActivity extends AppCompatActivity
implements Callback{
private static final String TAG = "MainActivity";
private RecyclerView recyclerView;
private ImageWallAdapter imageWallAdapter;
private StaggeredGridLayoutManager layoutManager;
private List imageDataList;//图片url
private GankTask gankTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//返回顶部
layoutManager.smoothScrollToPosition(recyclerView,null,0);
}
});
imageDataList = new ArrayList<>();
// imageDataList = Arrays.asList(TestImageUrl.imageThumbUrls);
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true);
layoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
//上拉加载更多
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if(newState == RecyclerView.SCROLL_STATE_IDLE){
int[] posititons = new int[layoutManager.getSpanCount()];
layoutManager.findLastVisibleItemPositions(posititons);
for(int position : posititons){
if(position == imageDataList.size() - 1){
Log.d(TAG,"add more");
gankTask.nextPage();//获取下一页内容
}
}
}
}
});
recyclerView.setLayoutManager(layoutManager);
imageWallAdapter = new ImageWallAdapter(this, imageDataList);
recyclerView.setAdapter(imageWallAdapter);
gankTask = new GankTask(this,this);//每页20张图
gankTask.getData(1);//获得数据
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void addData(List list) {
//添加修改的数据
int pos = imageDataList.size();
// imageDataList.clear();
imageDataList.addAll(list);
// imageWallAdapter.notifyDataSetChanged();
imageWallAdapter.notifyItemRangeInserted(pos,list.size());
}
}
MyApplication:这个类用来全局控制RequestQueue
/**
* Created by Lee on 2015/11/22.
*/
public class MyApplication extends Application {
private static final String TAG = "MyApplication";
private RequestQueue requestQueue;
private static MyApplication applicationInstance;
@Override
public void onCreate() {
super.onCreate();
applicationInstance = this;
requestQueue = Volley.newRequestQueue(getApplicationContext());
}
public static synchronized MyApplication getInstance(){
return applicationInstance;
}
public RequestQueue getRequestQueue(){
if (requestQueue == null){
requestQueue = Volley.newRequestQueue(getApplicationContext());
}
return requestQueue;
}
public void addRequest(Request request){
request.setTag(TAG);
requestQueue.add(request);
}
public void cancleRequest(){
requestQueue.cancelAll(TAG);
}
}
Util:工具类,用来生成图片的key。因为图片url中会有特殊字符,直接用来作为文件名不太合适,所以通过md5加密后用来作为文件名。
/**
* 工具类
* Created by Lee on 2015/11/20.
*/
public class Util {
/**
* 通过md5 生成图片对应的key
*
* @param imagePath 图片路径
* @return 图片对应的key
*/
public static String keyForImage(String imagePath) {
String key = null;
try {
MessageDigest messageDigest = MessageDigest.getInstance("md5");
messageDigest.update(imagePath.getBytes());
key = byteToHex(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return key;
}
/**
* 将二进制数组转换成十六进制
*
* @param digest 二进制数组
* @return 十六进制字符串
*/
private static String byteToHex(byte[] digest) {
StringBuilder builder = new StringBuilder();
for (byte b : digest) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
builder.append("0");
}
builder.append(hex);
}
return builder.toString();
}
}
下面是两个比较重要的类。
GankTask:从干货集中营获取url并通过回调函数添加到数据集中
/**
* 获取数据 数据来自干货集中营 http://gank.io/
* Created by Lee on 2015/11/21.
*/
public class GankTask {
private static final String TAG = "GankTask";
//数据类型: 福利 | Android | iOS | 休息视频 | 拓展资源 | 前端 | all
//目前仅支持福利 剩下的日后扩充
private static final String TYPE_FL = "福利";
//分类数据: http://gank.avosapps.com/api/data/数据类型/请求个数/第几页
private static final String DATA_URL = "http://gank.avosapps.com/api/data/%s/%d/%d";
private static final int DEFAULT_COUNT = 10;//默认每页10个
private static final int DEFAULT_TIMEOUT = 5000;//默认超时请求
private List imageUrlList;//用于存放图片url
private int count = DEFAULT_COUNT;//每页数量
private static int currentPage = 1;//当前页数
private String dataType = TYPE_FL;//数据格式
private Context context;
private Callback callback;//回调函数
public GankTask(Context context, Callback callback) {
this(context, DEFAULT_COUNT, callback);
}
public GankTask(Context context, int count, Callback callback) {
imageUrlList = new ArrayList<>();
this.count = count;
this.context = context;
this.callback = callback;
try {
dataType = URLEncoder.encode(dataType, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
/**
* 获得第page页图片url
* @param page 页数
*/
public void getData(int page) {
String dataUrl = String.format(DATA_URL, dataType, count, page);
StringRequest request = new StringRequest(dataUrl, new Response.Listener() {
@Override
public void onResponse(String response) {
try {
JSONObject obj = new JSONObject(response);
JSONArray array = obj.getJSONArray("results");
imageUrlList.clear();
for (int i = 0; i < array.length(); i++) {
String url = array.getJSONObject(i).getString("url");//获得图片url
imageUrlList.add(url);
}
callback.addData(imageUrlList);
} catch (JSONException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
//加载出错
Log.e(TAG, "error:" + error.getMessage());
Toast.makeText(context, "加载出错", Toast.LENGTH_SHORT).show();
}
});
request.setRetryPolicy(new DefaultRetryPolicy(DEFAULT_TIMEOUT, 1, 1.0f));//设置请求超时
MyApplication.getInstance().addRequest(request);//将消息添加到消息队列
}
/**
* 获取下一页内容
*/
public void nextPage(){
currentPage += 1;
getData(currentPage);
}
/**
* 回调函数
* 向数据集中添加新增的数据
*/
public interface Callback {
void addData(List list);
}
}
GankImageTask:用来加载图片
/**
* 下载图片
* Created by Lee on 2015/11/22.
*/
public class GankImageTask {
private static final String TAG = "GankImageTask";
private LruCache lruCache;//内存缓存
private DiskLruCache diskLruCache;//磁盘缓存
private DiskLruCache.Editor editor;
private ImageView imageView;
private Callback callback;
public GankImageTask(DiskLruCache diskLruCache, ImageView imageView, LruCache lruCache, Callback callback) {
this.callback = callback;
this.diskLruCache = diskLruCache;
this.imageView = imageView;
this.lruCache = lruCache;
}
public void loadImage(String imageUrl) {
Bitmap bitmap = null;
final String key = Util.keyForImage(imageUrl);//获得url对应的key
try {
//没有缓存图片 下载图片并缓存
if (diskLruCache.get(key) == null) {
ImageRequest request = new ImageRequest(imageUrl, new Response.Listener() {
@Override
public void onResponse(Bitmap response) {
try {
editor = diskLruCache.edit(key);
final OutputStream os = editor.newOutputStream(0);
response.compress(Bitmap.CompressFormat.JPEG, 100, os);//保存图片到本地
editor.commit();
os.flush();
os.close();
addImageToCache(key, response);//将图片加入缓存
} catch (IOException e) {
e.printStackTrace();
}
callback.setImage(imageView, response);
}
}, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.RGB_565, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e(TAG, "load image fail:" + error.getMessage());
}
});
MyApplication.getInstance().addRequest(request);//将请求添加到请求队列
}
//再查询一次图片是否存在
if (diskLruCache.get(key) != null) {
//将图片解析出来
FileInputStream fis = (FileInputStream) diskLruCache.get(key).getInputStream(0);
bitmap = BitmapFactory.decodeFileDescriptor(fis.getFD());
if (bitmap != null)
addImageToCache(key, bitmap);//将图片加入缓存
callback.setImage(imageView,bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// /**
// * 将记录写入journal文件
// */
// void flushCache() {
// if (diskLruCache != null) {
// try {
// diskLruCache.flush();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
/**
* 将图片加入缓存
*
* @param key
* @param bitmap
*/
private void addImageToCache(String key, Bitmap bitmap) {
if (lruCache.get(key) == null) {
lruCache.put(key, bitmap);
}
}
public interface Callback {
void setImage(ImageView imageView, Bitmap bitmap);
}
}
最后就是适配器
ImageWallAdapter:recyclerview适配器
public class ImageWallAdapter extends RecyclerView.Adapter<MyViewHolder>
implements GankImageTask.Callback {
private static final String TAG = "ImageWallAdapter";
private static final int MAX_SIZE = 10 * 1024 * 1024;
private Context context;
private LruCache lruCache;//内存缓存
private DiskLruCache diskLruCache;//磁盘缓存
private List dataList;//图片数据
public ImageWallAdapter(Context context, List dataList) {
this.context = context;
this.dataList = dataList;
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int memorySize = maxMemory / 8;//内存的1/8作为缓存
lruCache = new LruCache(memorySize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
//图片缓存路径
File cacheFile = getCacheFile(context, "thumb");
// Log.d(TAG, "cacheFile:" + cacheFile.getPath());
if (!cacheFile.exists()) {
cacheFile.mkdirs();
}
//设置磁盘缓存
try {
diskLruCache = DiskLruCache.open(cacheFile, getAppVersion(context), 1, MAX_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 得到缓存文件夹
* @param context
* @param fileName
* @return
*/
private File getCacheFile(Context context, String fileName) {
String filePath;
if (!Environment.isExternalStorageRemovable()
|| Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
filePath = context.getExternalCacheDir().getPath();
} else {
filePath = context.getCacheDir().getPath();
}
return new File(filePath + File.separator + fileName);
}
/**
* 获得版本
* @param context
* @return 当前版本
*/
private int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.image_item, parent, false);
return new MyViewHolder(view);
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
String key = Util.keyForImage(dataList.get(position));
//缓存中不存在图片 下载图片
if (lruCache.get(key) == null) {
new GankImageTask(diskLruCache, holder.imageView, lruCache, this).loadImage(dataList.get(position));
// new ImageLoadTask(context,holder.imageView,diskLruCache,lruCache,this).execute(dataList.get(position));
} else {
holder.imageView.setImageBitmap(lruCache.get(key));
}
}
@Override
public int getItemCount() {
return dataList.size();
}
@Override
public void setImage(ImageView imageView, Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}
}
class MyViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
public MyViewHolder(View itemView) {
super(itemView);
imageView = (ImageView) itemView.findViewById(R.id.image_item_image);
}
}
好了,主要代码就是以上这些。
代码写完了,用的主要是volley框架的知识,还有图片缓存的知识。不得不说volley确实是个很好用的框架,之前没有用这个框架之前每次网络请求都要自己写httpurlconnection或者httpclient确实很麻烦,现在用了volley框架之后代码简化了不少,而且volley中自带imageloader,为图片加载也做了不少简化,用的真心方便。
虽然整个客户端还是比较简单的就是,就是一个recyclerview加载网络图片,再加上缓存和上拉加载而更多(下拉刷新这里没加,有空补上)。不过还是有一些问题。
代码中关键的部分都有注释,应该基本看得懂,如果不明白的地方欢迎留言。还有代码写的可能比较丑陋,不足之处还希望大家能够指点。