Android自定义View(16) 《如何利用Bitmap加载一个8K高清图片》

概述

这两天在写一个控件用来显示多个图片组合,所以就仔细研究了一下Bitmap的加载问题,所以今天就写一下这个内容了。

从资源文件获取Bitmap

获取Bitmap我们主要通过BitmapFactory类来加载一张图片,那么接下来我们就用下面这个图片来演示各种情况下图片的加载吧~

girl.jpg

原图的分辨率是7680*4320,也就是说,如果按ARGB_8888的格式存储,每个像素占4个字节,那么这个图片转换成bitmap类后,足足会有7680x4320x4个字节,是一张实实在在的8k高清壁纸 (壁纸下载戳这里
这个大小如果完全加载的话是肯定会内存溢出的,那么接下来我们开始尝试几种加载方式

1.全部加载

获取bitmap

 private fun loadBitmapCompletely(){
        bitmap = BitmapFactory.decodeResource(resources,R.drawable.girl)
        Log.d("LargeImageView","bitmap size ---> ${bitmap!!.byteCount}")
    }

在画布中绘制

canvas.drawBitmap(bitmap!!,viewRect,viewRect,null)

控件宽高矩阵的初始化

 @SuppressLint("ResourceType")
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewRect = Rect(0,0,width,height)
        loadBitmapCompletely()
    }

那么运行结果是这样的

2021-09-29 20:45:22.287 15880-15880/com.tx.txcustomview E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.tx.txcustomview, PID: 15880
    java.lang.RuntimeException: Canvas: trying to draw too large(1003622400bytes) bitmap.
        at android.graphics.RecordingCanvas.throwIfCannotDraw(RecordingCanvas.java:280)
        at android.graphics.BaseRecordingCanvas.drawBitmap(BaseRecordingCanvas.java:88)
        at com.tx.txcustomview.view.LargeImageView.onDraw(LargeImageView.kt:33)
        at android.view.View.draw(View.java:22473)
        at android.view.View.updateDisplayListIfDirty(View.java:21341)
        at android.view.View.draw(View.java:22201)
        at android.view.ViewGroup.drawChild(ViewGroup.java:4540)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4299)
        at androidx.constraintlayout.widget.ConstraintLayout.dispatchDraw(ConstraintLayout.java:1882)
        at android.view.View.updateDisplayListIfDirty(View.java:21332)
        ...

好家伙,加载这么大一张图直接加载不出来了,所以我们现在开始打开正确的加载方式

2.压缩加载

首先,我们需要整理压缩加载图片的步骤

  • 1.首先需要获取Bimap的宽高,和view控件的宽高来确定采样率
  • 2.利用采样率来获取压缩后的图片
  • 3.将获取到的压缩后的图片跟控件宽高做对比,然后来再次进行bitmap缩放创建一个适应屏幕的bitmap
  • 4.最终这个适应控件宽高的bitmap就是我们要加载的图片了
    好,开始coding
    获取图片
 private fun loadBitmap(){
        // 获取图片的宽高
        var option = BitmapFactory.Options()
        // 设置inJustDecodeBounds为true可以解析出Bitmap的图片信息,比如宽高,但是不会获取bitmap对象,设为true以后调用
        // BitmapFactory.decodeResource()解析会返回null对象
        option.inJustDecodeBounds = true
        BitmapFactory.decodeResource(context.resources, R.drawable.girl,option)
        // 开始计算获取到的图片的宽高比,我们这里采用宽度优先,优先保证按宽度来显示图片,如果是垂直方向上的长图那就要再看情况了
        var fraction = option.outHeight.toFloat()/option.outWidth.toFloat()
        // 根据获取到的图片的宽高,和我们的实际控件的宽高来做对比,计算采样率
        // 比如我们有一张500x500的图,但是我们只需要显示100x100,那么我们采样率就可以设为4
        // 这样获取到的图片宽高就是125*125,然后我们可以再进行一次bitmap缩放来获取合适的bitmap,也可以缩小bitmap的占用内存
        option.inSampleSize = getSampleSize(option.outWidth,option.outHeight,width,(fraction*width).toInt())
        // 这里记住一定要设回false,否则获取不到图片
        option.inJustDecodeBounds = false
        // 开始获取压缩后的图片
        bitmap = BitmapFactory.decodeResource(context.resources,R.drawable.girl,option)
        // 对获取后的图片进行缩放处理,获取一个新的适合控件的图片
        var bmpTemp = postBitmap(bitmap!!,width, (fraction*width).toInt())
        if (bitmap!=null && !bitmap!!.isRecycled){
            bitmap!!.recycle()
        }
        bitmap = bmpTemp
        Log.d("LargeImageView","sampleSize ---> ${option.inSampleSize}")
        Log.d("LargeImageView","byteCount ---> ${bitmap!!.byteCount}")
    }

获取采样率的方法

      /**
      * 获取采样率
      */
    private fun getSampleSize(bmpW :Int ,bmpH : Int,targetW:Int ,targetH:Int) : Int{
        var sampleSize = 1
        while ((bmpW/sampleSize>=targetW)||(bmpH/sampleSize>=targetH)){
            sampleSize *= 2
        }
        return sampleSize
    }

缩放bitmap的方法

private fun postBitmap(bitmap:Bitmap, targetW: Int, targetH: Int):Bitmap{
        return Bitmap.createScaledBitmap(bitmap,targetW,targetH,false)
    }

然后我们把图片绘制一下

override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        var matrix = Matrix()
        matrix.postTranslate(0f, (height/2).toFloat()-bitmap!!.height)
        canvas.drawBitmap(bitmap!!,matrix,null)
    }

接下来我们看一下运行结果

2021-09-29 21:16:19.388 23385-23385/com.tx.txcustomview D/LargeImageView: sampleSize ---> 8
2021-09-29 21:16:19.388 23385-23385/com.tx.txcustomview D/LargeImageView: byteCount ---> 2622240

根据日志的打印我们可以看到图片的采样率变成了8,也就是说缩小了64倍的内存大小,从我们打印的内存结果也可以看出来内存明显缩小了一大截,总之我们现在是可以完整显示这个图而且不变形了
显示结果


large_image_view.png

那么问题来了,如果现在我们必须要完整显示整个图片的高清画质呢?那么接下来我们就要来进行正确的完整加载高清大图了

3.局部加载

首先在写代码之前我们需要知道局部加载的原理,局部加载主要依靠了BitmapRegionDecoder类,这个类可以解析Bitmap的局部内容,那么知道了工具类,我们再来了解一下加载的方式


large_view.PNG

中间的矩形也就是我们的显示区域,这个区域的获取是相对于Bitmap的一个矩形区域,我们解析的时候也是通过不断解析并且显示这部分的图片来进行显示的,知道了加载过程,直接上完整源码

package com.tx.txcustomview.view

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import com.tx.txcustomview.R
import java.io.InputStream

/**
 * create by xu.tian
 * @date 2021/9/28
 */
public class LargeImageView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    // 解析的Bitmap对象
    private var bitmap:Bitmap? = null
    // 获取到的Bitmap矩阵
    private lateinit var bitmapRect : Rect
    // 解析的区域在Bitmap矩阵中的位置矩阵区域
    private lateinit var positionRect : Rect
    // 控件View矩阵
    private lateinit var viewRect : Rect
    // 从资源文件中获取的输入流,因为要支持滑动刷新bitmap,为了避免重复创建对象,进行复用
    private lateinit var inputStream : InputStream
    // 解析器
    private lateinit var decoder: BitmapRegionDecoder
    
    // 手指按下的初始位置x左标
    private var startX = 0f
    // 手指按下的初始位置的y坐标
    private var startY = 0f
    
    // 绘制bitmap
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawBitmap(bitmap!!,viewRect,viewRect,null)
    }
    
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when(event.action){
            MotionEvent.ACTION_DOWN ->{
                startX = event.x
                startY = event.y
            }
            MotionEvent.ACTION_MOVE ->{
                var dx = event.x - startX
                var dy = event.y - startY

                startX = event.x
                startY = event.y

                var left = positionRect.left-1.5*dx
                var top = positionRect.top-1.5*dy

                when {
                    left<0 -> {
                        positionRect.left = 0
                    }
                    left>bitmapRect.width()-width -> {
                        positionRect.left = bitmapRect.width()-positionRect.width()
                    }
                    else -> {
                        positionRect.left = left.toInt()
                    }
                }

                when {
                    top<0 -> {
                        positionRect.top = 0
                    }
                    top>bitmapRect.height()-height -> {
                        positionRect.top = bitmapRect.height()-positionRect.height()
                    }
                    else -> {
                        positionRect.top = top.toInt()
                    }
                }
                positionRect.right = positionRect.left+width
                positionRect.bottom = positionRect.top+height
                loadBitmapRegion()
                invalidate()
            }
        }
        return true
    }

    @SuppressLint("ResourceType")
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewRect = Rect(0,0,width,height)
        positionRect = Rect(0,0,width,height)
        inputStream = context.resources.openRawResource(R.drawable.girl)
        decoder = BitmapRegionDecoder.newInstance(inputStream,false)
        loadBitmapRegion()
    }
    @SuppressLint("ResourceType")
    private fun loadBitmapRegion(){
        var option = BitmapFactory.Options()
        option.inJustDecodeBounds = true
        BitmapFactory.decodeResource(context.resources, R.drawable.girl,option)
        bitmapRect = Rect(0,0,option.outWidth,option.outHeight)
        try {
            option.inJustDecodeBounds = false
            bitmap?.recycle()
            bitmap = decoder.decodeRegion(positionRect,option)
            Log.d("LargeImageView","byteCount ---> ${bitmap!!.byteCount}")
        }catch (e: Exception){
            e.printStackTrace()
        }

    }
    /**
     * 关闭输入流,回收bitmap
     */
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        if (inputStream!=null){
            inputStream.close()
        }
        if(bitmap!=null && !bitmap!!.isRecycled){
            bitmap!!.recycle()
        }
    }
}

运行结果


large_image_view_completetly.gif

日志结果

2021-09-29 21:55:52.439 28684-28684/com.tx.txcustomview D/LargeImageView: byteCount ---> 9357120

这里的bitmap大小比我们最开始的压缩加载要大很多,是因为我们对之前的图片进行了基于宽度上的压缩,不过现在我们已经可以很nice的加载一张完整的高清大图啦~这个内存上的消耗是没有办法避免的咯,毕竟是为了高清的原生画质

总结要点

    1. 如果Bitmap分辨率比显示的区域大,那么我们可以进行合适的压缩,采样率在设置时只可以设置为2的倍数,然后再把这个bitmap进行一次针对view的缩放,这样就可以尽可能的在保证图片正常显示的基础上使用最小的内存
    1. 如果Bitmap分辨率比显示区域小,这时候千万不要再去利用一个新的bitmap来进行对这个这个bitmap的缩放,因为bitmap的大小和像素点数是有直接关系的,我们要合理的在canvas中利用Matrix类来对Bitmap绘制时进行缩放,这样并不会改变bitmap本身所占用的内存大小,而且与直接操作bitmap缩放是效果一致的
  • 3.如果需要完整加载一张高清大图,那么我们需要使用局部解析的方法,但是在这个过程中我们需要注意
    (1)输入流对象的控制,尽可能复用
    (2) 控制解析的矩形范围,在我写的例子中其实是还有优化空间的,我没有提前解析出一块更大的区域,这样就会导致在滑动的过程中会频繁的触发bitmap的创建和老bitmap对象的回收,而且也很耗性能,我的建议是提前设计好一块稍大一点的区域,直到用户滑到已经解析好的区域的边界时,再进行下一个区 域的解析,这样就可以降低bitmap对象的更新次数,不过这样也会同时增加这个bitmap的内存占用,不过流畅度和缓存大小总得有一个做出牺牲嘛

最后划重点!!!

view销毁时释放资源!

view销毁时释放资源!

view销毁时释放资源!

好了今天就这样吧,下篇再看写点啥。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容