前语

加载高清大图时,往往会有不能缩放分段加载的需求出现。本文将就BitmapRegionDecoder和subsampling-scale-image-view的运用总结一下Bitmap的分区域解码

定义

Android大图预览

假定现在有一张这样的图片,尺度为3040  1280。假如按需求彻底显现在ImageView上的话就必须进行压缩处理。当需求要求是不能缩放的时候,就需求进行分段检查了。因为像这种尺度巨细的图片在加载到内存后容易造成OOM,所以需求进行区域解码

图中红框的部分就是需求区域解码的部分,即每次只有进行红框区域巨细的解码,在需求看其余部分时能够经过如拖动等手势来移动红框区域,到达检查全图的目的。

BitmapRegionDecoder

Android原生供给了BitmapRegionDecoder用于完成Bitmap的区域解码,简单运用的api如下:

// 依据图片文件的InputStream创立BitmapRegionDecoder
val decoder = BitmapRegionDecoder.newInstance(inputStream, false)
val option: BitmapFactory.Options = BitmapFactory.Options()
val rect: Rect = Rect(0, 0, 100, 100)
// rect制定的区域即为需求区域解码的区域
decoder.decodeRegion(rect, option)
  • 经过BitmapRegionDecoder.newInstance能够依据图片文件的InputStream目标创立一个BitmapRegionDecoder目标。
  • decodeRegion办法传入一个Rect和一个BitmapFactory.Options,前者用于规则解码区域,后者用于配置Bitmap,如inSampleSize、inPreferredConfig等。ps:解码区域必须在图片宽高范围内,不然会出现溃散。

区域解码与全图解码

经过区域解码得到的Bitmap,宽高和占用内存仅仅指定区域的图像所需求的巨细

譬如按1080 1037区域巨细加载,能够检查Bitmap的allocationByteCount为4479840,即1080 * 1037 * 4

Android大图预览

若直接按全图解码,allocationByteCount为15564800,即3040 * 1280 * 4

Android大图预览

能够看到,区域解码的优点是图像不会完好的被加载到内存中,而是按需加载了。

自定义一个图片检查的View

因为BitmapRegionDecoder仅仅完成区域解码,假如改动这个区域仍是需求开发者经过具体交互完成。这儿用接触事情简单完成了一个自定义View。因为仅仅简单依靠接触事情,滑动的灵敏度仍是偏高,实践开发能够完成一些自定义的拖拽工具来进行辅助。代码比较简单,可参考注释。

class RegionImageView @JvmOverloads constructor(
    context: Context,
    attr: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attr, defStyleAttr) {
    private var decoder: BitmapRegionDecoder? = null
    private val option: BitmapFactory.Options = BitmapFactory.Options()
    private val rect: Rect = Rect()
    private var lastX: Float = -1f
    private var lastY: Float = -1f
    fun setImage(fileName: String) {
        val inputStream = context.assets.open(fileName)
        try {
            this.decoder = BitmapRegionDecoder.newInstance(inputStream, false)
            // 触发onMeasure,用于更新Rect的初始值
            requestLayout()
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            inputStream.close()
        }
    }
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                this.decoder ?: return false
                this.lastX = event.x
                this.lastY = event.y
                return true
            }
            MotionEvent.ACTION_MOVE -> {
                val decoder = this.decoder ?: return false
                val dx = event.x - this.lastX
                val dy = event.y - this.lastY
                // 每次MOVE事情依据前后差值对Rect进行更新,需求留意不能超过图片的实践宽高
                if (decoder.width > width) {
                    this.rect.offset(-dx.toInt(), 0)
                    if (this.rect.right > decoder.width) {
                        this.rect.right = decoder.width
                        this.rect.left = decoder.width - width
                    } else if (this.rect.left < 0) {
                        this.rect.right = width
                        this.rect.left = 0
                    }
                    invalidate()
                }
                if (decoder.height > height) {
                    this.rect.offset(0, -dy.toInt())
                    if (this.rect.bottom > decoder.height) {
                        this.rect.bottom = decoder.height
                        this.rect.top = decoder.height - height
                    } else if (this.rect.top < 0) {
                        this.rect.bottom = height
                        this.rect.top = 0
                    }
                    invalidate()
                }
            }
            MotionEvent.ACTION_UP -> {
                this.lastX = -1f
                this.lastY = -1f
            }
            else -> {
            }
        }
        return super.onTouchEvent(event)
    }
    // 丈量后默许第一次加载的区域是从0开端到控件的宽高巨细
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val w = MeasureSpec.getSize(widthMeasureSpec)
        val h = MeasureSpec.getSize(heightMeasureSpec)
        this.rect.left = 0
        this.rect.top = 0
        this.rect.right = w
        this.rect.bottom = h
    }
    // 每次制作时,经过BitmapRegionDecoder解码出当前区域的Bitmap
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.let {
            val bitmap = this.decoder?.decodeRegion(rect, option) ?: return
            it.drawBitmap(bitmap, 0f, 0f, null)
        }
    }
}

SubsamplingScaleImageView

davemorrissey/subsampling-scale-image-view能够用于加载超大尺度的图片,避免大内存导致的OOM,内部依靠的也是BitmapRegionDecoder。优点是SubsamplingScaleImageView现已帮咱们完成了相关的手势如拖动、缩放,内部还完成了二次采样和区块显现的逻辑。

假如需求加载assets目录下的图片,能够这样调用

subsamplingScaleImageView.setImage(ImageSource.asset("sample1.jpeg"))
public final class ImageSource {
    static final String FILE_SCHEME = "file:///";
    static final String ASSET_SCHEME = "file:///android_asset/";
    private final Uri uri;
    private final Bitmap bitmap;
    private final Integer resource;
    private boolean tile;
    private int sWidth;
    private int sHeight;
    private Rect sRegion;
    private boolean cached;

ImageSource是对图片资源信息的抽象

  • uri、bitmap、resource别离指代图像来历是文件、解析好的Bitmap目标仍是resourceId。
  • tile:是否需求分片加载,一般以uri、resource方式加载的都会为true。
  • sWidth、sHeight、sRegion:加载图片的宽高和区域,一般能够指定加载图片的特定区域,而不是全图加载
  • cached:控制重置时,是否需求recycle掉Bitmap
public final void setImage(@NonNull ImageSource imageSource, ImageSource previewSource, ImageViewState state) {
    ...
    if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) {
        ...
    } else if (imageSource.getBitmap() != null) {
        ...
    } else {
        sRegion = imageSource.getSRegion();
        uri = imageSource.getUri();
        if (uri == null && imageSource.getResource() != null) {
            uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + imageSource.getResource());
        }
        if (imageSource.getTile() || sRegion != null) {
            // Load the bitmap using tile decoding.
            TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri);
            execute(task);
        } else {
            // Load the bitmap as a single image.
            BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
            execute(task);
        }
    }
}

因为在咱们的调用下,tile为true,setImage办法最终会走到一个TilesInitTask当中。是一个AsyncTask。ps:该库中的多线程异步操作都是经过AsyncTask封装的。

// TilesInitTask
@Override
protected int[] doInBackground(Void... params) {
    try {
        String sourceUri = source.toString();
        Context context = contextRef.get();
        DecoderFactory<? extends ImageRegionDecoder> decoderFactory = decoderFactoryRef.get();
        SubsamplingScaleImageView view = viewRef.get();
        if (context != null && decoderFactory != null && view != null) {
            view.debug("TilesInitTask.doInBackground");
            decoder = decoderFactory.make();
            Point dimensions = decoder.init(context, source);
            int sWidth = dimensions.x;
            int sHeight = dimensions.y;
            int exifOrientation = view.getExifOrientation(context, sourceUri);
            if (view.sRegion != null) {
                view.sRegion.left = Math.max(0, view.sRegion.left);
                view.sRegion.top = Math.max(0, view.sRegion.top);
                view.sRegion.right = Math.min(sWidth, view.sRegion.right);
                view.sRegion.bottom = Math.min(sHeight, view.sRegion.bottom);
                sWidth = view.sRegion.width();
                sHeight = view.sRegion.height();
            }
            return new int[] { sWidth, sHeight, exifOrientation };
        }
    } catch (Exception e) {
        Log.e(TAG, "Failed to initialise bitmap decoder", e);
        this.exception = e;
    }
    return null;
}
@Override
protected void onPostExecute(int[] xyo) {
    final SubsamplingScaleImageView view = viewRef.get();
    if (view != null) {
        if (decoder != null && xyo != null && xyo.length == 3) {
            view.onTilesInited(decoder, xyo[0], xyo[1], xyo[2]);
        } else if (exception != null && view.onImageEventListener != null) {
            view.onImageEventListener.onImageLoadError(exception);
        }
    }
}

TilesInitTask首要的操作是创立一个SkiaImageRegionDecoder,它首要的作用是封装BitmapRegionDecoder。经过BitmapRegionDecoder获取图片的具体宽高和在Exif中获取图片的方向,便于显现调整。

后续会在onDraw时调用initialiseBaseLayer办法进行图片的加载操作,这儿会依据份额计算出采样率来决议是否需求区域解码仍是全图解码。值得一提的是,当采样率为1,图片宽高小于Canvas的getMaximumBitmapWidth()getMaximumBitmapHeight()时,也是会直接进行全图解码的。这儿调用的TileLoadTask就是运用BitmapRegionDecoder进行解码的操作。

ps:Tile目标为区域的抽象类型,内部会包含指定区域的Bitmap,在onDraw时会依据区域经过Matrix制作到Canvas上。

private synchronized void initialiseBaseLayer(@NonNull Point maxTileDimensions) {
    debug("initialiseBaseLayer maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y);
    satTemp = new ScaleAndTranslate(0f, new PointF(0, 0));
    fitToBounds(true, satTemp);
    // Load double resolution - next level will be split into four tiles and at the center all four are required,
    // so don't bother with tiling until the next level 16 tiles are needed.
    fullImageSampleSize = calculateInSampleSize(satTemp.scale);
    if (fullImageSampleSize > 1) {
        fullImageSampleSize /= 2;
    }
    if (fullImageSampleSize == 1 && sRegion == null && sWidth() < maxTileDimensions.x && sHeight() < maxTileDimensions.y) {
        // Whole image is required at native resolution, and is smaller than the canvas max bitmap size.
        // Use BitmapDecoder for better image support.
        decoder.recycle();
        decoder = null;
        BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
        execute(task);
    } else {
        initialiseTileMap(maxTileDimensions);
        List<Tile> baseGrid = tileMap.get(fullImageSampleSize);
        for (Tile baseTile : baseGrid) {
            TileLoadTask task = new TileLoadTask(this, decoder, baseTile);
            execute(task);
        }
        refreshRequiredTiles(true);
    }
}

加载网络图片

BitmapRegionDecoder只能加载本地图片,而假如需求加载网络图片,能够结合Glide运用,以SubsamplingScaleImageView为例

Glide.with(this)
    .asFile()
    .load("")
    .into(object : CustomTarget<File?>() {
        override fun onResourceReady(resource: File, transition: Transition<in File?>?) {
            subsamplingScaleImageView.setImage(ImageSource.uri(Uri.fromFile(resource)))
        }
        override fun onLoadCleared(placeholder: Drawable?) {}
})

能够经过CustomTarget获取到图片的File文件,然后再调用SubsamplingScaleImageView#setImage

最终

本文首要总结Bitmap的分区域解码,利用原生的BitmapRegionDecoder可完成区域解码,经过SubsamplingScaleImageView能够对BitmapRegionDecoder进行进一步的交互扩展和优化。假如需求是TV端开发能够参考这篇文章,里面有结合具体的TV端操作适配:Android完成TV端大图阅读。

参考文章:

Android完成TV端大图阅读

tddrv.cn/a/233555

Android 超大图长图阅读库 SubsamplingScaleImageView 源码解析