How to load a large image in Android and display it normally without OOM?

question

In Android, get a 1000*20000( 宽1000px,高20000px) large image, how to load and display normally and not happen OOM?

analyze

AndroidThe system will allocate a certain size of heap memory for the application
, and if it encounters a high-resolution image, if its configuration is ARGB(each pixel occupies 4Byte)
, then the memory it will consume 1000200004=800000000is about 100, and 80MB
the memory is easily exhausted. appear OOM.

Of course, this is loaded with the native method of the system Bitmap. In most cases, we use third-party libraries like Glide, Freso, Picasso, which do a certain amount of processing for large image loading, but we can’t just stop at using it, but also figure out how to solve it. Image loading OOMissue.

There are two ways to solve this problem

  • Image sampling rate scaling
  • Use BitmapRegionDecoderpart of the loaded image

Let's talk about it

Solution 1: Image sampling rate scaling

BitmapFactory.OptionsThe principle of this method is to scale the picture by a certain ratio and reduce the resolution, thereby reducing the memory usage. Objects are specifically used here .

BitmapFacotry.Options为BitmapFactoryAn internal class, it is mainly used to set and store BitmapFactorysome information about loading pictures.
The following are Optionsthe attributes that need to be used in:

  • inJustDecodeBounds: If set to true, the pixel array of the picture will not be loaded into memory
  • outHeight: the height of the image
  • outWidth: the width of the picture
  • inSampleSize: After setting this value, the picture will be loaded according to this sampling rate. It cannot be set to a 1number smaller than that. For example, if it is set to 4, the resolution, width and height will be the original 1/4, and the overall memory occupied at this time will be the original1/16

It should be noted that
if it inSampleSizeis set to 1be less than it will be considered 1, and if it is 2a multiple ,
if it is not then it will be rounded down to 2a multiple of integers (but this is not Androidtrue for all versions)

code example

First, we need to put the long picture in assetsthe folder
and then read the file stream through the code

var inputStream = assets.open("image.jpg")  

Then, by BitmapFactory.Optionssetting inJustDecodeBounds = true, do not read the image to the memory, only read the image information (this can avoid the occurrence during the reading process OOM), so that we can get the width and height of the image.

val opts = BitmapFactory.Options()
//注意这里是关键 --> 不读取图像到内存中,仅读取图片的信息
opts.inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, opts)
//获取图片的宽高
val imgWidth = opts.outWidth
val imgHeight = opts.outHeight

Next, we need to calculate the appropriate sampling rate.
By passing 图片实际宽高 / 目标ImageView宽高, we can get the appropriate sampling rate.
The final sampling rate is subject to the maximum direction.

val targetImageWidth = targetImageView.width
val targetImageHeight = targetImageView.height
//计算采样率
val scaleX = imgWidth / targetImageWidth
val scaleY = imgHeight / targetImageHeight
//采样率依照最大的方向为准
var scale = max(scaleX, scaleY)
if (scale < 1) {
    
    
    scale = 1
}

Finally, we open the file stream of the long image again, and inJustDecodeBoundsset falseit inSampleSizeto the specified sampling rate
to get the final scaled Bitmap and display it in the ImageView

 // false表示读取图片像素数组到内存中,依照指定的采样率
opts.inJustDecodeBounds = false
opts.inSampleSize = scale
//由于流只能被使用一次,所以需要再次打开
inputStream = assets.open("image.jpg")
val bitmap = BitmapFactory.decodeStream(inputStream, null, opts)
targetImageView.setImageBitmap(bitmap)

Let's take a look at the complete code

private fun loadBigImage(targetImageView: ImageView) {
    
    
    var inputStream = assets.open("image.jpg")
    val opts = BitmapFactory.Options()
    //注意这里是关键 --> 不读取图像到内存中,仅读取图片的信息
    opts.inJustDecodeBounds = true
    BitmapFactory.decodeStream(inputStream, null, opts)
    //获取图片的宽高
    val imgWidth = opts.outWidth
    val imgHeight = opts.outHeight
    val targetImageWidth = targetImageView.width
    val targetImageHeight = targetImageView.height
    //计算采样率
    val scaleX = imgWidth / targetImageWidth
    val scaleY = imgHeight / targetImageHeight
    //采样率依照最大的方向为准
    var scale = max(scaleX, scaleY)
    if (scale < 1) {
    
    
        scale = 1
    }
    Log.i(TAG, "loadBigImage:$scale")
    // false表示读取图片像素数组到内存中,依照指定的采样率
    opts.inJustDecodeBounds = false
    opts.inSampleSize = scale
    //由于流只能被使用一次,所以需要再次打开
    inputStream = assets.open("image.jpg")
    val bitmap = BitmapFactory.decodeStream(inputStream, null, opts)
    targetImageView.setImageBitmap(bitmap)
}

The effect after running is as follows

insert image description here

Solution 2: Images are loaded by region

Sometimes it is not only required not to appear OOM, but also not to be compressed and fully displayed. In this case, BitmapRegionDecoderclasses are used.

    /**
     * 传入图片
     * BitmapRegionDecoder提供了一系列的newInstance方法来构造对象,
     * 支持传入文件路径,文件描述符,文件的inputStream等
     */
    BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream,false);
    Rect rect = Rect();
    BitmapFactory.Options opetion = BitmapFactory.Options();
    /**
     * 指定显示图片的区域
     * 参数一很明显的rect //参数二是BitmapFactory.Options,
     * 你可以告诉图片的inSampleSize,inPreferredConfig等
     */
    bitmapRegionDecoder.decodeRegion(rect, opetion);

BitmapRegionDecoderMainly used to display a certain rectangular area of ​​an image, this class is very suitable for loading partitioned areas and loading large images.

In order to display all of the big picture, along with the partial display, gestures must be added so that it can be dragged up and down to view.
In this way, you need to customize a control, and the idea of ​​customizing this control is also very simple.

  • Provide an entry to set the picture
  • Rewrite onTouchEvent, and update the parameters of the display area according to the user's certain gestures.
  • After not updating the area parameters, call it invalidate, get it onDrawinside , and draw it.regionDecoder.decodeRegionBitmapdraw

code example

First, we're going to create a custom View:BigImageView

class BigImageView(context: Context, attrs: AttributeSet) : View(context, attrs) {
    
    }

Next, declare some constants,
which rectare used to represent the drawn area, which will be assigned later.
optionsthat isBitmapFactory.Options

companion object {
    
    
	private var decoder: BitmapRegionDecoder? = null
	
	//图片的宽度和高度
	private var imageWidth: Int = 0
	private var imageHeight: Int = 0
	
	//绘制的区域
	private var rect = Rect()
	private val options = BitmapFactory.Options().apply {
    
    
	    inPreferredConfig = Bitmap.Config.RGB_565
	}
}

In onTouchEventthe event,
when the sliding event is processed ACTION_DOWN, it will be assigned downYthe current position .
When ACTION_MOVEmoving, call rect.offsetto rectmodify top, left, right,bottom

var downX = 0F
var downY = 0F

override fun onTouchEvent(event: MotionEvent): Boolean {
    
    
    super.onTouchEvent(event)

    when (event.action) {
    
    
        MotionEvent.ACTION_DOWN -> {
    
    
            downX = event.x
            downY = event.y
        }

        MotionEvent.ACTION_MOVE -> {
    
    
            val dY = (event.y - downY).toInt()
            if (imageHeight > height) {
    
    
                rect.offset(0, -dY)
                checkHeight()
                invalidate()
            }
        }

        MotionEvent.ACTION_DOWN -> {
    
    }
    }

    return true
}

Of course, it is also necessary to judge and process the boundary .
If it is rect.topless than , 0then it will rect.topbe assigned as the height of the component0rect.bottom

private fun checkHeight() {
    
    
    if (rect.bottom > imageHeight) {
    
    
        rect.bottom = imageHeight
        rect.top = imageHeight - height
    }
    if (rect.top < 0) {
    
    
        rect.top = 0
        rect.bottom = height
    }
}

Then you need to pass the image into

fun setBitmap(inputStream: InputStream) {
    
    
    var tempOptions = BitmapFactory.Options()
    tempOptions.inJustDecodeBounds = true
    BitmapFactory.decodeStream(inputStream, null, tempOptions)
    imageWidth = tempOptions.outWidth
    imageHeight = tempOptions.outHeight
    decoder = BitmapRegionDecoder.newInstance(inputStream, false)
    requestLayout()
    invalidate()
}

At onMeasurethe time, set it rectto the size of the picture

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    
    
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)

    if (rect.right == 0 && rect.bottom == 0) {
    
    
        val width = measuredWidth
        val height = measuredHeight
        rect.left = 0
        rect.top = 0
        rect.right = rect.left + width
        rect.bottom = rect.top + height
    }
}

In drawthe method, draw the specified area

override fun onDraw(canvas: Canvas?) {
    
    
    super.onDraw(canvas)

    val bitmap = decoder?.decodeRegion(rect, options)
    if (bitmap != null) {
    
    
        canvas?.drawBitmap(bitmap, 0F, 0F, null)
    }
}

Finally Activitycall in

val inputStream =  assets.open("image.jpg")
binding.bigImageView.setBitmap(inputStream)

Take a look at the complete code

class BigImageView(context: Context, attrs: AttributeSet) : View(context, attrs) {
    
    

    companion object {
    
    
        private var decoder: BitmapRegionDecoder? = null

        //图片的宽度和高度
        private var imageWidth: Int = 0
        private var imageHeight: Int = 0

        //绘制的区域
        private var rect = Rect()
        private val options = BitmapFactory.Options().apply {
    
    
            inPreferredConfig = Bitmap.Config.RGB_565
        }
    }

    var downX = 0F
    var downY = 0F

    override fun onTouchEvent(event: MotionEvent): Boolean {
    
    
        super.onTouchEvent(event)

        when (event.action) {
    
    
            MotionEvent.ACTION_DOWN -> {
    
    
                downX = event.x
                downY = event.y
            }

            MotionEvent.ACTION_MOVE -> {
    
    
                val dY = (event.y - downY).toInt()
                if (imageHeight > height) {
    
    
                    rect.offset(0, -dY)
                    checkHeight()
                    invalidate()
                }
            }

            MotionEvent.ACTION_DOWN -> {
    
    }
        }

        return true
    }

    private fun checkHeight() {
    
    
        if (rect.bottom > imageHeight) {
    
    
            rect.bottom = imageHeight
            rect.top = imageHeight - height
        }
        if (rect.top < 0) {
    
    
            rect.top = 0
            rect.bottom = height
        }
    }

    override fun onDraw(canvas: Canvas?) {
    
    
        super.onDraw(canvas)

        val bitmap = decoder?.decodeRegion(rect, options)
        if (bitmap != null) {
    
    
            canvas?.drawBitmap(bitmap, 0F, 0F, null)
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    
    
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        if (rect.right == 0 && rect.bottom == 0) {
    
    
            val width = measuredWidth
            val height = measuredHeight
            rect.left = 0
            rect.top = 0
            rect.right = rect.left + width
            rect.bottom = rect.top + height
        }
    }

    fun setBitmap(inputStream: InputStream) {
    
    
        var tempOptions = BitmapFactory.Options()
        tempOptions.inJustDecodeBounds = true
        BitmapFactory.decodeStream(inputStream, null, tempOptions)
        imageWidth = tempOptions.outWidth
        imageHeight = tempOptions.outHeight
        decoder = BitmapRegionDecoder.newInstance(inputStream, false)
        requestLayout()
        invalidate()
    }
}

The effect is as follows

insert image description here

summary

By now, we understand how to Androidload a large image in

  • Solution 1: Image sampling rate scaling: Get the width and height of the image first, and then reduce the image size BitmapFactory.Options.inJustDecodeBoundsby setting the sampling rate , so as to achieve the purpose of reducing the image sizeinSampleSize
  • Solution 2: Images are loaded by region: By bitmapRegionDecoder.decodeRegiondrawing the specified region of the image, it is avoided to load the entire image at one time, and only the images required by the screen are displayed, thereby avoidingOOM

Download the source code of this article: Demo of loading a large image in Android

Guess you like

Origin blog.csdn.net/EthanCo/article/details/131337046