自己实现一个Android网络图片加载器
在Android开发中,我们经常会用到各种各样的图片加载框架来帮助我们加载网络图片,那有没有想过自己实现一个呢
本文记录了实现一个图片框架的整个流程,以及对代码的优化整理过程,文章比较长,如果只对其中的一部分感兴趣直接跳转到相应部分即可
提问环节
把大象装进冰箱分几步?是不是感觉在这初秋时节更加凉爽了呢
好吧,下面开始提问
- 大多数图片都是网络获取的,如何加载网络图片到本地imageview
- 图片有可能很大,怎么在加载前进行压缩
- 每次加载同一个url的图片都要请求网络,可以做一个缓存来防止过度请求吗
- 头像想显示圆角,怎么通过加载器显示圆角图片
- 在图片没加载出来之前,会显示placeholder图,加载完成之后切换会闪烁一下,很不美观,怎么处理
- 写好了代码,但是用起来很麻烦,想用设计模式优化一下,让它用起来像glide一样,减少学习成本,该怎么做
如果只想看其中某一部分,可以直接跳转到相应段落,完整的代码放到了我github上,点击这里可以直达
那么,女士们先生们,下面就开始我们的旅程吧
如何加载网络图片到本地imageview
这个功能很简单,怎么实现我不管,明天上线
这个问题确实很简单,开启一个线程,从网络请求中获取图片流,再组装成bitmap,加载到imageview里面去就行了,看代码
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,只返回图片尺寸
这俩配合,就能让我们压缩图片了
关键代码:
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,可以同时返回算好的宽和高
}
看完了关键代码,是不是很简单呢,我们来看看完整的代码
//图片压缩类
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来当文件名即可
这个功能太简单了,直接看代码吧
//先来一个接口,为了以后可能的更多缓存形式做准备
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
上代码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,在里面执行显示内容的切换动画即可
看代码
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代码
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呢
我们来看看具体使用的时候长什么样
ImageLoader.withContext(this)
.placeholder(R.drawable.holder)
.load("https://www.baidu.com/img/flexible/logo/pc/result.png")
.useCache(true)
.into(image)
是不是感觉和glide一模一样呢
最后
前面说了那么多,可能有人想说,你的加载图片代码呢,不急,这里就放出全部的代码
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)
}
}
至此,我们的图片加载工具已经做好啦,大家可以根据自己的需求愉快的使用了,希望这篇文章能对你有帮助,我们下次再见