在Android开发中,我们经常会用到各种各样的图片加载框架来帮助我们加载网络图片,那有没有想过自己实现一个呢
本文记录了实现一个图片框架的整个流程,以及对代码的优化整理过程,文章比较长,如果只对其中的一部分感兴趣直接跳转到相应部分即可
提问环节
把大象装进冰箱分几步?是不是感觉在这初秋时节更加凉爽了呢
好吧,下面开始提问
- 大多数图片都是网络获取的,如何加载网络图片到本地imageview
- 图片有可能很大,怎么在加载前进行压缩
- 每次加载同一个url的图片都要请求网络,可以做一个缓存来防止过度请求吗
- 头像想显示圆角,怎么通过加载器显示圆角图片
- 在图片没加载出来之前,会显示placeholder图,加载完成之后切换会闪烁一下,很不美观,怎么处理
- 写好了代码,但是用起来很麻烦,想用设计模式优化一下,让它用起来像glide一样,减少学习成本,该怎么做
如果只想看其中某一部分,可以直接跳转到相应段落,完整的代码放到了我github上,点击这里可以直达
那么,女士们先生们,下面就开始我们的旅程吧
如何加载网络图片到本地imageview
这个功能很简单,怎么实现我不管,明天上线
这个问题确实很简单,开启一个线程,从网络请求中获取图片流,再组装成bitmap,加载到imageview里面去就行了,看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private fun getNetImg(imageView: ImageView): Bitmap? { var bitmap: Bitmap? = null val url = URL(params.imageURL) val connection = url.openConnection() as HttpURLConnection connection.requestMethod = "GET" connection.connectTimeout = 10000 val code = connection.responseCode if (code == 200) { val inputStream = connection.inputStream bitmap = bitmapCompressor.getCompressBitmap(inputStream, imageView)//获取到亚索(压缩,谐音梗扣钱)图片 inputStream.close() } else { Log.e("NetImageView", "server error") } return bitmap//获取到bitmap了,返回,直接扔给imageview用就行 } |
怎么在加载前进行压缩
网络图片有时候会很大,我们的ImageView就那么小一点,图片很大浪费了我们宝贵的内存资源,怎么办呢?我们勤劳的劳动人民有很多办法,没错,就是图片压缩,在加载前我们先获取到图片的宽高以及ImageView的宽高,根据比例来压缩图片,再加载,就没问题啦。
关键利用了
– val options = BitmapFactory.Options()这个Options里有个方法,叫inSampleSize,设置比例
- 还有一个参数,叫inJustDecodeBounds,只返回图片尺寸
这俩配合,就能让我们压缩图片了
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeStream(sizeInputStream, null, options) options.inSampleSize = getInSampleSize(options, imageView)//getInSampleSize就是算比理的函数 options.inJustDecodeBounds = false -----getInSampleSize的代码在下面----- private fun getInSampleSize(options: BitmapFactory.Options, imageView: ImageView): Int { var inSampleSize = 1 val (viewWidth, viewHeight) = getImageViewSize(imageView)//这个函数是什么?别急,它的代码就在下面,算ImageView尺寸的 val outWidth = options.outWidth val outHeight = options.outHeight if (outWidth > viewWidth || outHeight > viewHeight) { val widthRadio = (outWidth / viewWidth).toDouble().roundToInt() val heightRadio = (outHeight / viewHeight).toDouble().roundToInt() inSampleSize = if (widthRadio > heightRadio) widthRadio else heightRadio } return inSampleSize } //计算ImageView尺寸的函数 private fun getImageViewSize(imageView: ImageView): Pair<Int, Int> { val displayMetrics = context.resources?.displayMetrics val layoutParams = imageView.layoutParams var viewWidth = imageView.width var viewHeight = imageView.height if (viewWidth <= 0) { viewWidth = layoutParams.width } if (viewWidth <= 0) { viewWidth = imageView.maxWidth } if (viewWidth <= 0) { viewWidth = displayMetrics?.widthPixels!! } if (viewHeight <= 0) { viewHeight = layoutParams.height } if (viewHeight <= 0) { viewHeight = imageView.maxHeight } if (viewHeight <= 0) { viewHeight = displayMetrics?.heightPixels!! } return Pair(viewWidth, viewHeight)//返回一个pair,可以同时返回算好的宽和高 } |
看完了关键代码,是不是很简单呢,我们来看看完整的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
//图片压缩类 class BitmapCompressor(private val context:Context) { //计算imageview尺寸 private fun getImageViewSize(imageView: ImageView): Pair<Int, Int> { val displayMetrics = context.resources?.displayMetrics val layoutParams = imageView.layoutParams var viewWidth = imageView.width var viewHeight = imageView.height if (viewWidth <= 0) { viewWidth = layoutParams.width } if (viewWidth <= 0) { viewWidth = imageView.maxWidth } if (viewWidth <= 0) { viewWidth = displayMetrics?.widthPixels!! } if (viewHeight <= 0) { viewHeight = layoutParams.height } if (viewHeight <= 0) { viewHeight = imageView.maxHeight } if (viewHeight <= 0) { viewHeight = displayMetrics?.heightPixels!! } return Pair(viewWidth, viewHeight) } //计算压缩比例 private fun getInSampleSize(options: BitmapFactory.Options, imageView: ImageView): Int { var inSampleSize = 1 val (viewWidth, viewHeight) = getImageViewSize(imageView) val outWidth = options.outWidth val outHeight = options.outHeight if (outWidth > viewWidth || outHeight > viewHeight) { val widthRadio = (outWidth / viewWidth).toDouble().roundToInt() val heightRadio = (outHeight / viewHeight).toDouble().roundToInt() inSampleSize = if (widthRadio > heightRadio) widthRadio else heightRadio } return inSampleSize } //获取压缩后的bitmap fun getCompressBitmap(input: InputStream, imageView: ImageView): Bitmap? { val stream = ByteArrayOutputStream() val bufferSize = 1024 try { val buffer = ByteArray(bufferSize) var len: Int while (input.read(buffer).also { len = it } > -1) { stream.write(buffer, 0, len) } stream.flush() } catch (e: IOException) { e.printStackTrace() } val sizeInputStream: InputStream = ByteArrayInputStream(stream.toByteArray()) val bitmapInputStream: InputStream = ByteArrayInputStream(stream.toByteArray()) val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeStream(sizeInputStream, null, options) options.inSampleSize = getInSampleSize(options, imageView) options.inJustDecodeBounds = false return BitmapFactory.decodeStream(bitmapInputStream, null, options) } } |
这个功能稍微复杂一点点,不过也挺简单,相信大家都表示问题不大
可以做一个缓存来防止过度请求吗
当然可以,这个功能也非常简单,我们用文件来进行缓存,怎么确保缓存唯一呢——使用图片url来当文件名即可
这个功能太简单了,直接看代码吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//先来一个接口,为了以后可能的更多缓存形式做准备 interface ImageCache{ fun cacheImg(bitmap: Bitmap?,name:String); fun getCacheImage(name:String): Bitmap? } //文件缓存类 class FileImageCache(private val context: Context): ImageCache { //储存缓存文件 override fun cacheImg(bitmap: Bitmap?,name:String) { if (bitmap != null) try { val file = File(context.cacheDir, name) val out = FileOutputStream(file) bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) out.flush() out.close() } catch (e: IOException) { e.printStackTrace() } } //根据名字获取文件 override fun getCacheImage(name:String): Bitmap? { val file = File(context.cacheDir, name) var bitmap: Bitmap? = null if (file.length() > 0) { val inputStream: InputStream = FileInputStream(file) bitmap = BitmapFactory.decodeStream(inputStream) } return bitmap } } |
这个就不用多解释了吧,只是一个文件存取而已
怎么通过加载器显示圆角图片
现在我们的图片加载器以及初具雏形了,但是好多头像都有圆角,我们怎么给这个加载器添加这个功能呢?
这个稍微复杂一点,需要用到canvas的绘制功能,大体思路是这样的
– 绘制一个圆角的rect
– 设置xfermode为PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
– 绘制bitmap
上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
object BitmapRounder{ fun getRoundedCornerBitmap(bitmap: Bitmap, round: Float): Bitmap? { return try { val output = Bitmap.createBitmap( bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888 ) val canvas = Canvas(output) val paint = Paint() val rect = Rect( 0, 0, bitmap.width, bitmap.height ) val rectF = RectF( Rect( 0, 0, bitmap.width, bitmap.height ) ) paint.isAntiAlias = true canvas.drawARGB(0, 0, 0, 0) paint.color = Color.BLACK canvas.drawRoundRect(rectF, round, round, paint)//重点,绘制一个圆角的矩形 paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)//也是重点,设置xfermode为src_in val src = Rect( 0, 0, bitmap.width, bitmap.height ) canvas.drawBitmap(bitmap, src, rect, paint)//绘制bitmap output } catch (e: Exception) { bitmap } } } |
圆角功能也就完成了
在图片没加载出来之前,会显示placeholder图,加载完成之后切换会闪烁一下,很不美观,怎么处理
一句话,用动画
怎么用动画,这里就要引入一个类,drawable,想必大家都不陌生,我们直接自定义一个drawable,在里面执行显示内容的切换动画即可
看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<br />class MyAnimationDrawable(private val bitmap: Bitmap, private val placeholder: Bitmap) : Drawable(),Animatable { private var mValueAnimator = ValueAnimator() var placeholderAlpha = 250 var paint:Paint var isFinish = false init { mValueAnimator = ObjectAnimator.ofInt(this,"placeholderAlpha",0) mValueAnimator.duration = 1200 mValueAnimator.startDelay = 1000 paint = Paint() mValueAnimator.addUpdateListener { // 监听属性动画并进行重绘 invalidateSelf() } } override fun draw(canvas: Canvas){ if (!isFinish) { val rectF = RectF( 0f, 0f, bounds.width().toFloat(), bounds.height().toFloat() ) //w和h分别是屏幕的宽和高,也就是你想让图片显示的宽和高 paint.reset() canvas.drawBitmap(bitmap, null, rectF, paint) paint.alpha = placeholderAlpha canvas.drawBitmap(placeholder, null, rectF, paint) paint.reset() } if (placeholderAlpha == 0)isFinish = true } override fun setAlpha(p0: Int) { } @SuppressLint("WrongConstant") override fun getOpacity(): Int { return 1 } override fun setColorFilter(p0: ColorFilter?) { } override fun isRunning(): Boolean { return mValueAnimator.isRunning } override fun start() { mValueAnimator.start() } override fun stop() { } } |
用这个类替代bitmap,即可实现从一个图片到另一个图片的动画效果切换,是不是很简单呢
写好了代码,但是用起来很麻烦,想用设计模式优化一下,让它用起来像glide一样,减少学习成本,该怎么做
核心,使用builder模式
关于这个问题,我们就要去看看glide是怎么做的了
经过对glide的分析,我们发现它是用了builder模式,我们也整一个,我们还发现它的builder不直接设置加载器,而是设置了一个param类,最后加载的时候再传入参数,所以我们也这么玩
下面是我们的builder代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
class RequestBuilder { data class ImageParams( var roundPx: Float = 0f, val emptyPlaceHolderId: Int = -1, var placeHolder: Int = emptyPlaceHolderId, var imageURL: String = "", var imageMaxSideSize: Float = -1f, var useCache: Boolean = true, var context: Context? = null) private var params = ImageParams() fun withContext(context: Context): RequestBuilder { params.context = context return this } fun useCache(useCache: Boolean): RequestBuilder { params.useCache = useCache return this } fun placeholder(placeholder: Int): RequestBuilder { params.placeHolder = placeholder return this } fun load(url: String): RequestBuilder { params.imageURL = url return this } fun round(round:Float): RequestBuilder { params.roundPx = round return this } fun into(imageView: ImageView) { val realLoader = RealImageLoader(params) realLoader.loadImage(imageView) } fun adjustImageScale(imageMaxSideSize:Float): RequestBuilder { params.imageMaxSideSize = imageMaxSideSize return this } } |
是不是so easy呢
我们来看看具体使用的时候长什么样
1 2 3 4 5 6 |
ImageLoader.withContext(this) .placeholder(R.drawable.holder) .load("https://www.baidu.com/img/flexible/logo/pc/result.png") .useCache(true) .into(image) |
是不是感觉和glide一模一样呢
最后
前面说了那么多,可能有人想说,你的加载图片代码呢,不急,这里就放出全部的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
class RealImageLoader(private val params: RequestBuilder.ImageParams) { private var fileCache = FileImageCache(params.context!!)//缓存类 private var bitmapCompressor = BitmapCompressor(params.context!!)//压缩类 //加载图片的方法 fun loadImage(imageView: ImageView) { if (params.context == null) { Log.e("ImageLoader", "Empty context") return }//参数为空,直接返回 var bitmap: Bitmap? = null loadPlaceHolderImg(imageView)//加载placeholder GlobalScope.launch(Dispatchers.IO) {//开启一个协程,用来处理图片 try { if (params.useCache) { bitmap = fileCache.getCacheImage(getCacheFileName())//从缓存查找 } if (bitmap == null) { bitmap = getNetImg(imageView)//缓存没有,从网络获取 } if (bitmap != null) bitmap = roundBitmap(bitmap)//根据设置,进行圆角处理 } catch (e: IOException) { e.printStackTrace() Log.e("NetImageView", "Load image error") } withContext(Dispatchers.Main) {//切换到主线程,进行图片加载 if (bitmap != null) { imageView.setImageBitmap(bitmap) /* val placeholder = BitmapFactory.decodeResource( params.context?.resources, params.placeHolder ) val drawable = MyAnimationDrawable(bitmap!!, placeholder!!) imageView.setImageDrawable(drawable) drawable.start()*/ } } } } private fun loadPlaceHolderImg(imageView: ImageView) { if (params.placeHolder != params.emptyPlaceHolderId) { var bitmap = BitmapFactory.decodeResource(params.context?.resources, params.placeHolder) bitmap = roundBitmap(bitmap) imageView.setImageBitmap(bitmap) } } private fun roundBitmap(bitmap: Bitmap?): Bitmap? { var roundBitmap = bitmap if (params.roundPx != 0f) { if (roundBitmap != null) roundBitmap = BitmapRounder.getRoundedCornerBitmap(roundBitmap, params.roundPx) } return roundBitmap } private fun getNetImg(imageView: ImageView): Bitmap? { var bitmap: Bitmap? = null val url = URL(params.imageURL) val connection = url.openConnection() as HttpURLConnection connection.requestMethod = "GET" connection.connectTimeout = 10000 val code = connection.responseCode if (code == 200) { val inputStream = connection.inputStream bitmap = bitmapCompressor.getCompressBitmap(inputStream, imageView) if (bitmap != null) bitmap = changeScale(bitmap) if (params.useCache) fileCache.cacheImg(bitmap, getCacheFileName()) inputStream.close() } else { Log.e("NetImageView", "server error") } return bitmap } private fun getCacheFileName(): String { var name = "" val strings = params.imageURL.split("/") for (s in strings) { name += s } return name } private fun changeScale(bitmap: Bitmap): Bitmap { var mBitmap = bitmap if (params.imageMaxSideSize > 0) { var height = bitmap.height var width = bitmap.width if (width >= height) { width = params.imageMaxSideSize.toInt() height = (params.imageMaxSideSize * (bitmap.height.toFloat() / bitmap.width)).toInt() } else { height = params.imageMaxSideSize.toInt() width = (params.imageMaxSideSize * (bitmap.width.toFloat() / bitmap.height)).toInt() } mBitmap = zoomImg(bitmap, width, height) } return mBitmap } private fun zoomImg(bm: Bitmap, newWidth: Int, newHeight: Int): Bitmap { val width = bm.width val height = bm.height val scaleWidth = newWidth.toFloat() / width val scaleHeight = newHeight.toFloat() / height val matrix = Matrix() matrix.postScale(scaleWidth, scaleHeight) return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true) } } |
至此,我们的图片加载工具已经做好啦,大家可以根据自己的需求愉快的使用了,希望这篇文章能对你有帮助,我们下次再见