昨天刚写了个带缓存功能的ImageView(kyimageview),现在把他的实现原理说明下,有兴趣的可以参考下,当然如果有更好的方案也欢迎分享下。
利用图片大小判断图片是否更新
这是这个组件的核心,在下载图片前,先获取图片的大小(到字节),然后与本地缓存的图片比较,不一致则说明有更新。
我们知道,图片的大小不只是与像素数有关,还与透明度、色彩、压缩有关,不同的透明度、色彩 最后压缩出来的图片大小都是不一样的。所以,图片哪怕有稍微的改变,大小都是不一样的。
URL url = new URL(imageUrl); HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection(); httpConnection.setRequestProperty("User-Agent", "PacificHttpClient"); httpConnection.setConnectTimeout(10000); httpConnection.setReadTimeout(20000); httpConnection.connect(); if (httpConnection.getResponseCode() == 404) {//链接404 finishedHandler.sendEmptyMessage(1); return ; } final long updateTotalSize = httpConnection.getContentLength();//待下载的图片大小 //文件存在 File localFile = new File(cacheFolder, fileName);//本地缓存了的图片,localFile.length()获取本地图片大小 if(localFile.exists() && updateTotalSize == localFile.length()){//比较二者大小,相等则说明没变化 finishedHandler.sendEmptyMessage(2); return ; } File saveFile = new File(cacheFolder, fileName);//图片有变化,则下载图片,并保存到缓存中。这个实际与localFile重了,可以去掉 FileOutputStream output = new FileOutputStream(saveFile); byte buffer[] = new byte[1024]; int readsize = 0; final InputStream is = httpConnection.getInputStream(); while((readsize = is.read(buffer)) != -1){ output.write(buffer, 0, readsize); } is.close(); output.close(); finishedHandler.sendEmptyMessage(0);
如何让图片在画布上居中显示
要想让图片居中显示,首先要计算出图片left、和top。如上图可得出, left=画布的一半 - 图片的一半。top的计算同理。当然如果存在padding,也要考虑到。
如何实现ScaleType
首先来看下ScaleType各个值的含义:
public class ScaleType { /** * 不按比例缩放图片,目标是把图片塞满整个View。 * fitXY */ public static final int FIT_XY = 1; /** * FIT_START, FIT_END在图片缩放效果上与FIT_CENTER一样,只是显示的位置不同,FIT_START是置于顶部,FIT_CENTER居中,FIT_END置于底部。 * fitStart */ public static final int FIT_START = 2; /** * 把图片按比例扩大/缩小到View的宽度,居中显示 * fitCenter */ public static final int FIT_CENTER = 3; /** * FIT_START, FIT_END在图片缩放效果上与FIT_CENTER一样,只是显示的位置不同,FIT_START是置于顶部,FIT_CENTER居中,FIT_END置于底部。 * fitEnd */ public static final int FIT_END = 4; /** * 按图片的原来size居中显示,当图片长/宽超过View的长/宽,则截取图片的居中部分显示 * center */ public static final int CENTER = 5; /** * 按比例扩大图片的size居中显示,使得图片长(宽)等于或大于View的长(宽) * centerCrop */ public static final int CENTER_CROP = 6; /** * 将图片的内容完整居中显示,通过按比例缩小或原来的size使得图片长/宽等于或小于View的长/宽 * centerInside */ public static final int CENTER_INSIDE = 7; }
开始画:
/** * 根据ScaleType的center模式画 * @return */ private void drawByCenter(Canvas canvas, Bitmap bitmap){ int contentWidth = this.getWidth() - this.paddingLeft - this.paddingRight;//计算图片的可用空间,即减去padding后的 int contentHeight = this.getHeight() - this.paddingTop - this.paddingBottom; int left = this.paddingLeft + (contentWidth/2 - bitmap.getWidth()/2);//计算图片居中显示的left int top = this.paddingTop + (contentHeight/2 - bitmap.getHeight()/2);//计算图片居中显示的top canvas.drawBitmap(bitmap, left, top, null); bitmap.recycle(); }
/** * 根据ScaleType的centerCrop模式画 * @return */ private void drawByCenterCrop(Canvas canvas, Bitmap bitmap){ int contentWidth = this.getWidth() - this.paddingLeft - this.paddingRight; int contentHeight = this.getHeight() - this.paddingTop - this.paddingBottom; if(bitmap.getWidth() < contentWidth || bitmap.getHeight() < contentHeight){//判断图片是否需要放大 float widthRatio = ((float) contentWidth) / bitmap.getWidth();//计算宽度的放大比例 float heightRatio = ((float) contentHeight) / bitmap.getHeight();//计算高度的放大比例 float ratio = widthRatio; if(heightRatio > widthRatio)//取二者最大的,这样就能保证宽、高都能大于或等于图片显示区域的大小 ratio = heightRatio; bitmap = scaleBitmap(bitmap, ratio, ratio);//进行缩放 } int left = this.paddingLeft + (contentWidth/2 - bitmap.getWidth()/2); int top = this.paddingTop + (contentHeight/2 - bitmap.getHeight()/2); canvas.drawBitmap(bitmap, left, top, null); bitmap.recycle(); }
/** * 根据ScaleType的fitStart模式画 * @return */ private void drawByFitStart(Canvas canvas, Bitmap bitmap){ int contentWidth = this.getWidth() - this.paddingLeft - this.paddingRight; int contentHeight = this.getHeight() - this.paddingTop - this.paddingBottom; if(bitmap.getWidth() != contentWidth){//将图片宽度缩放到contentWidth的大小 float ratio = ((float) contentWidth) / bitmap.getWidth(); bitmap = scaleBitmap(bitmap, ratio, ratio); } int left = this.paddingLeft + (contentWidth/2 - bitmap.getWidth()/2); int top = this.paddingTop; canvas.drawBitmap(bitmap, left, top, null); bitmap.recycle(); }
其他的模式与上面的同理,想要更多了解的可以下载源码。
使用线程池
/** * 获得线程池 */ private static ExecutorService executorService ; private ExecutorService getExecutorService(){ if(executorService == null) executorService = Executors.newFixedThreadPool(threadPoolSize); return executorService; }
组件使用线程池来异步加载图片,所有KyImageView都共用一个静态的executorService对象。通过threadPoolSize设置线程池最大线程数。
通过消息刷新
线程加载完图片后是通过invalidate()来刷新组件的,为了避免出现混乱或死循环,定义了个ThreadWrapper,可以理解为是个消息:
/** * 线程wrapper * @return */ private class ThreadWrapper{ private boolean isLoading = false;//是否正在加载远程图片 private boolean isFinished = false;//是否加载完成 }
这样在onDraw(Canvas canvas)刷新时,就可以判断当前刷新是否来自线程:
if(!threadWrapper.isLoading){//不是来自线程的刷新 threadWrapper.isLoading = true; loadRemoteImage(); return ; } if(threadWrapper.isFinished){//是来自线程的刷新,同时重置threadWrapper threadWrapper.isLoading = false; threadWrapper.isFinished = false; }
完毕。 先不论写得如何,个人是比较喜欢写些自定义组件,希望有同样爱好的可以多多交流。
源码:http://www.see-source.com/androidwidget/list.html