如何使用自定义Drawable高效加载大图

162 阅读5分钟

一直以来,在Android中我们一般使用BitmapDrawable加载图片,但面对大图就有点力不从心了,如果把整个大图全部解码加载到内存中会非常占用内存,很容易造成OOM。

但在实际场景中,经常会有加载大图的需求,比如截屏长图,如果正常使用Glide的图片框架加载的话,图片会被压缩的很厉害,看起来非常模糊。

面对大图场景,在android中我们有一种解决方式是使用BitmapRegionDecoder,它可以分块解码图片到bitmap中,我们只用解码屏幕显示区域中的图片,许多第三方大图加载组件都是使用BitmapRegionDecoder实现的。但这些第三方组件一般都是基于自定义View实现的,典型的一个代表是subsampling-scale-image-view,当然也有基于ImageView实现的。一般大图都要配合手势缩放,手势滑动使用,基于View实现的大图加载组件,很难和普通图片的手势缩放统一,因为是两个不同类型的View,普通图片我们一般使用PhotoView。

如果能实现支持加载大图的drawable我们就可以在任意view中使用了,比如所有图片都可以使用PhotoView加载。

所以我直接上代码

/**
 * Author: dylan
 * Version: V1.0
 * Date: 2023/4/12
 * Description: none
 * Modification History: none
 */
class ImageDrawable private constructor(context: Context, private val mSource: Image, private val mConfig: Config) : Drawable() {

    companion object {
        private val mMatrixPool = MatrixPool(5)

        private val mRectFPool = RectFPool(6)

        private val mRectPool = RectPool(6)
    }

    private data class Config(
        val bitmapConfig: Bitmap.Config,
        val recycleDelayTime: Long,
        val preLoadPercentage: Float,
        val preciseMode: Boolean,
        val decoderFactory: ImageDecoderFactory,
    )

    class Builder(private val context: Context) {

        lateinit var mSource: Image
            private set

        var mBitmapConfig = Bitmap.Config.RGB_565
            private set

        var mRecycleDelayTime = 5000L
            private set

        var mPreLoadPercentage: Float = 0f
            private set

        var mPreciseMode: Boolean = false
            private set

        var mDecoderFactory: ImageDecoderFactory = DEFAULT_IMAGE_DECODER_FACTORY
            private set

        fun setBitmapConfig(bitmapConfig: Bitmap.Config): Builder {
            mBitmapConfig = bitmapConfig
            return this
        }

        fun setRecycleDelayTime(recycleDelayTime: Long): Builder {
            mRecycleDelayTime = recycleDelayTime
            return this
        }

        fun setPreLoadPercentage(preLoadPercentage: Float): Builder {
            mPreLoadPercentage = preLoadPercentage
            return this
        }

        fun setPreciseMode(preciseMode: Boolean): Builder {
            mPreciseMode = preciseMode
            return this
        }

        fun setImageDecoderFactory(factory: ImageDecoderFactory): Builder {
            mDecoderFactory = factory
            return this
        }

        fun createFromDrawable(drawable: Drawable): ImageDrawable {
            mSource = Image.DrawableSource(drawable)
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromBitmap(bitmap: Bitmap): ImageDrawable {
            mSource = Image.BitmapSource(bitmap)
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromResource(id: Int): ImageDrawable {
            mSource = Image.UriSource(Image.resourceIdToUri(context, id))
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromAssets(assetName: String): ImageDrawable {
            mSource = Image.UriSource(Image.assetsToUri(assetName))
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromFile(file: File): ImageDrawable {
            mSource = Image.UriSource(Image.fileToUri(file))
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromUri(uri: Uri): ImageDrawable {
            mSource = Image.UriSource(uri)
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromPath(path: String): ImageDrawable {
            mSource = Image.UriSource(Image.pathToUri(path))
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromByteArray(data: ByteArray, offset: Int, length: Int): ImageDrawable {
            mSource = Image.ByteArraySource(data, offset, length)
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromStream(inputStream: InputStream): ImageDrawable {
            mSource = Image.StreamSource(inputStream)
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromFd(fd: FileDescriptor): ImageDrawable {
            mSource = Image.FdSource(fd)
            return ImageDrawable(context, mSource, createConfig())
        }

        fun createFromPfd(pfd: ParcelFileDescriptor): ImageDrawable {
            mSource = Image.PFdSource(pfd)
            return ImageDrawable(context, mSource, createConfig())
        }

        private fun createConfig(): Config {
            return Config(mBitmapConfig, mRecycleDelayTime, mPreLoadPercentage, mPreciseMode, mDecoderFactory)
        }
    }

    private lateinit var mTileLayers: Array<Array<Tile>>

    private var mDrawableScope = MainScope()

    private var mAttachView: WeakReference<View>? = null

    private var mLastImageMatrix: Matrix? = null

    private var mJob: Job? = null

    private var mCacheBitmap: Bitmap? = null

    private var mAttachWidth = 0

    private var mAttachHeight = 0

    private var mTaskId = 0

    private var mTileVersion = -1

    private var mCacheVersion = mTaskId

    private var maxSampleSize = 1

    private var mCurPower = -1

    private var mIsLargeImage = false

    @Volatile
    private var mRecycled = false

    init {
        mSource.init(mConfig.decoderFactory.create(context))
    }

    fun copy(context: Context, builder: Builder? = null): ImageDrawable {
        return ImageDrawable(context, mSource, builder?.run {
            Config(mBitmapConfig, mRecycleDelayTime, mPreLoadPercentage, mPreciseMode, mDecoderFactory)
        } ?: mConfig)
    }

    override fun draw(canvas: Canvas) {
        if (mRecycled) {
            return
        }
        val attachView = callback as? View ?: return
        attachToView(attachView)

        val matrix = mMatrixPool.take()
        val src = mRectFPool.take()
        if (mTaskId == mTileVersion) {
            if (mIsLargeImage) {
                delayRefreshDrawable(attachView)
            } else {
                refreshCurrentFrame(mTaskId, attachView)
            }
        }

        val count = canvas.save()
        canvas.clipRect(bounds)

        if (mIsLargeImage) {
            mRectPool.use { rect -> // 绘制兜底图
                rect.set(0, 0, mSource.sWidth, mSource.sHeight)
                val bitmap = mCacheBitmap?.takeIf {
                    mCacheVersion == mTaskId
                } ?: run {
                    val mOpts = BitmapFactory.Options()
                    mOpts.inSampleSize = maxSampleSize
                    mOpts.inPreferredConfig = mConfig.bitmapConfig
                    if (mConfig.bitmapConfig == Bitmap.Config.RGB_565) {
                        mOpts.inDither = true
                    }
                    mSource.decodeRegionBitmap(rect, mOpts)
                }

                bitmap?.also { it ->
                    src.set(0f, 0f, it.width.toFloat(), it.height.toFloat())
                    calculateRectTranslateMatrix(src, rect, matrix)
                    canvas.drawBitmap(it, matrix, null)
                }
                mCacheBitmap = bitmap
                mCacheVersion = mTaskId
            }
        }

        if (mTaskId == mTileVersion) {
            for (tile in mTileLayers[mCurPower]) {
                tile.drawableBitmap?.also {
                    src.set(0f, 0f, it.width.toFloat(), it.height.toFloat())
                    calculateRectTranslateMatrix(src, tile.simpleRect, matrix)
                    canvas.drawBitmap(it, matrix, null)
                }
            }
        }

        canvas.restoreToCount(count)

        mMatrixPool.given(matrix)
        mRectFPool.given(src)
    }

    override fun setAlpha(alpha: Int) {
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
    }

    override fun getOpacity(): Int {
        return PixelFormat.TRANSLUCENT
    }

    override fun getIntrinsicWidth(): Int {
        return mSource.sWidth
    }

    override fun getIntrinsicHeight(): Int {
        return mSource.sHeight
    }

    override fun onBoundsChange(bounds: Rect?) {
        val attachView = callback as? View ?: return
        attachToView(attachView)
    }

    fun recycle() {
        if (!mRecycled) {
            mRecycled = true
            if (::mTileLayers.isInitialized) {
                for (mTileLayer in mTileLayers) {
                    for (tile in mTileLayer) {
                        tile.markRelease()
                    }
                }
            }
            mSource.recycle()
            mDrawableScope.cancel()
            mCacheBitmap?.recycle()
            mCacheBitmap = null
            mAttachView?.clear()
            mMatrixPool.clear()
            mRectFPool.clear()
            mRectPool.clear()
        }
    }

    @MainThread
    private fun attachToView(attachView: View) {
        if (mRecycled) {
            return
        }
        val width = attachView.width
        val height = attachView.height
        if (width <= 0 || height <= 0) {
            return
        }
        val sameAttach = mAttachView?.get()?.let {
            it === attachView && mAttachWidth == width && mAttachHeight == height
        }
        if (sameAttach == true) {
            return
        }
        mTaskId++
        mCurPower = -1
        mLastImageMatrix = null
        mAttachWidth = width
        mAttachHeight = height
        mAttachView = WeakReference(attachView)
        mIsLargeImage = isLargeImage(mSource.sWidth, mSource.sHeight, width, height)

        val id = mTaskId
        val sampleSize = calculateMaxInSampleSize(mSource.sWidth, mSource.sHeight, width, height)

        maxSampleSize = sampleSize
        mDrawableScope.launch(start = CoroutineStart.UNDISPATCHED) {
            val result = withContext(Dispatchers.Default) {
                initTileLayers(attachView, width, height, sampleSize)
            }
            if (mTaskId == id) {
                mTileLayers = result
                mTileVersion = id
                refreshCurrentFrame(id, attachView)
            }
        }
    }

    @WorkerThread
    private fun initTileLayers(attachView: View, width: Int, height: Int, sampleSize: Int): Array<Array<Tile>> {
        val isImageViewSrcDrawable = isImageViewSrcDrawable(attachView)
        return if (!mIsLargeImage || !isImageViewSrcDrawable) {
            arrayOf(arrayOf(Tile(sampleSize, Rect(0, 0, mSource.sWidth, mSource.sHeight))))
        } else {
            val power = innerLog2(1.div(calculateMinScale(mSource.sWidth, mSource.sHeight, width, height).toDouble())).coerceAtLeast(0)
            val layerCount = power + 1
            Array(layerCount) { layer ->
                val sample = 1 shl layer
                val totalWidth = mSource.sWidth.toFloat()
                val sampleWidth = width.times(sample)
                val widthSize = totalWidth.div(sampleWidth).toInt().coerceAtLeast(1)
                val normalWidth = totalWidth.div(widthSize).roundToInt()
                val lastWidth = normalWidth.plus(totalWidth.toInt().minus(normalWidth.times(widthSize)))

                val totalHeight = mSource.sHeight.toFloat()
                val sampleHeight = height.times(sample)
                val heightSize = totalHeight.div(sampleHeight).toInt().coerceAtLeast(1)
                val normalHeight = totalHeight.div(heightSize).roundToInt()
                val lastHeight = normalHeight.plus(totalHeight.toInt().minus(normalHeight.times(heightSize)))
                Array(widthSize * heightSize) {
                    val column = it.rem(widthSize)
                    val left = normalWidth * column
                    val right = if (column == widthSize - 1) {
                        left + lastWidth
                    } else {
                        left + normalWidth
                    }
                    val row = it.div(widthSize)
                    val top = normalHeight * row
                    val bottom = if (row == heightSize - 1) {
                        top + lastHeight
                    } else {
                        top + normalHeight
                    }
                    Tile(sample, Rect(left, top, right, bottom))
                }
            }
        }
    }

    @MainThread
    private fun refreshCurrentFrame(id: Int, attachView: View) {
        val matrix = getCustomImageMatrix(attachView)
        if (mLastImageMatrix == matrix) {
            return
        }
        mLastImageMatrix?.set(matrix) ?: run {
            mLastImageMatrix = Matrix(matrix)
        }
        val scale = getScaleByMatrix(matrix)
        mCurPower = innerLog2(1.div(scale.toDouble())).coerceIn(0, mTileLayers.lastIndex)
        val isImageViewSrcDrawable = isImageViewSrcDrawable(attachView)
        val containerRect = mRectFPool.take()
        val mapTileRect = mRectFPool.take()
        val tempBounds = mRectPool.take()
        containerRect.set(0f, 0f, attachView.width.toFloat(), attachView.height.toFloat())
        var needRefresh = false
        if (mIsLargeImage) {
            mTileLayers.forEachIndexed { index, tiles ->
                if (index == mCurPower) {
                    val context = if (tiles.size == 1) Dispatchers.Main.immediate else Dispatchers.IO
                    tiles.forEachIndexed { i, tile ->
                        mapTileRect.set(tile.simpleRect)
                        matrix.mapRect(mapTileRect)
                        if (i == 0 && mConfig.preLoadPercentage > 0f) {
                            containerRect.inset(
                                mapTileRect.width().times(mConfig.preLoadPercentage).unaryMinus(),
                                mapTileRect.height().times(mConfig.preLoadPercentage).unaryMinus()
                            )
                        }
                        if (hitTest(containerRect, mapTileRect)) {
                            if (tryLoadBitmap(context, id, tile, isImageViewSrcDrawable, tempBounds)) {
                                needRefresh = true
                            }
                        } else {
                            tile.markRecycling(mDrawableScope, mConfig.recycleDelayTime)
                        }
                    }
                } else {
                    tiles.forEach { tile ->
                        tile.markRecycling(mDrawableScope, mConfig.recycleDelayTime.div(2))
                    }
                }
            }
        } else {
            val tile = mTileLayers[mTileLayers.lastIndex].first()
            tryLoadBitmap(Dispatchers.Main.immediate, id, tile, isImageViewSrcDrawable, tempBounds)
            needRefresh = true
        }

        mRectFPool.given(containerRect)
        mRectFPool.given(mapTileRect)
        mRectPool.given(tempBounds)

        if (needRefresh) {
            invalidateSelf()
        }
    }

    @MainThread
    private fun tryLoadBitmap(context: CoroutineContext, id: Int, tile: Tile, isImageViewSrcDrawable: Boolean, tempBounds: Rect): Boolean {
        if (tile.markLoading()) {
            copyBounds(tempBounds)
            val mOpts = BitmapFactory.Options()
            mOpts.inSampleSize = tile.simpleSize
            mOpts.inPreferredConfig = mConfig.bitmapConfig
            if (mConfig.bitmapConfig == Bitmap.Config.RGB_565) {
                mOpts.inDither = true
            }
            val resizeRect = Rect()
            resizeRect.apply {
                left = tile.simpleRect.left.div(mOpts.inSampleSize)
                top = tile.simpleRect.top.div(mOpts.inSampleSize)
                right = tile.simpleRect.right.div(mOpts.inSampleSize)
                bottom = tile.simpleRect.bottom.div(mOpts.inSampleSize)
            }
            mDrawableScope.launch(start = CoroutineStart.UNDISPATCHED) {
                if (id != mTaskId) {
                    return@launch
                }
                val bitmap = withContext(context) {
                    try {
                        if (mIsLargeImage) {
                            if (!isImageViewSrcDrawable && resizeRect.intersect(tempBounds)) {
                                mSource.decodeRegionBitmap(resizeRect, mOpts)
                            } else {
                                mSource.decodeRegionBitmap(tile.simpleRect, mOpts)
                            }
                        } else {
                            if (!isImageViewSrcDrawable && resizeRect.intersect(tempBounds)) {
                                mRectPool
                                mSource.decodeBitmap(mOpts)?.resize(resizeRect)
                            } else {
                                mSource.decodeBitmap(mOpts)
                            }
                        }
                    } catch (e: Exception) {
                        null
                    }
                }
                if (id == mTaskId) {
                    tile.markCompleted(bitmap)
                    invalidateSelf()
                }
            }
        } else if (tile.isCompleted()) {
            return true
        }
        return false
    }

    private fun delayRefreshDrawable(attachView: View) {
//        mJob?.cancel()
//        mJob = null
        mJob = mDrawableScope.launch {
            delay(100L)
            if (mTaskId == mTileVersion) {
                refreshCurrentFrame(mTaskId, attachView)
            }
        }
    }

    private fun hitTest(src: RectF, dst: RectF): Boolean {
        return RectF.intersects(src, dst)
    }

    private fun isImageViewSrcDrawable(attachView: View): Boolean {
        return attachView is ImageView && attachView.drawable === this
    }

    private fun getCustomImageMatrix(attachView: View): Matrix {
        return if (attachView is ImageView && attachView.drawable === this) {
            attachView.imageMatrix
        } else {
            Matrix()
        }
    }

    private fun innerLog2(value: Double): Int {
        return if (mConfig.preciseMode) {
            log2(value).toInt()
        } else {
            log2(value).roundToInt()
        }
    }
}

------------

class Adapter(private val recyclerView: RecyclerView) : RecyclerView.Adapter<ViewHolder>() {    private val maxCount = 20    private var count = 0    private var sources: Array<String?> = arrayOfNulls(maxCount)    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {        return ViewHolder(TextView(parent.context).also {            it.layoutParams = ViewGroup.LayoutParams(200, 500)        })    }    override fun onBindViewHolder(holder: ViewHolder, position: Int) {        (holder.itemView as TextView).text = getItem(position)    }    override fun getItemCount(): Int {        return count.coerceAtMost(maxCount)    }    fun getItem(position: Int): String {        val index = if (count > maxCount) {            count - maxCount + position        } else {            position        }        return requireNotNull(sources[index.rem(maxCount)])    }    fun addItem(item: String) {        count++        sources[count.minus(1).rem(maxCount)] = item        if (count > maxCount) {            val position = maxCount - 2            (recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(                position,                recyclerView.height - 500            )            notifyDataSetChanged()        } else {            notifyItemInserted(count - 1)            notifyItemChanged(count - 1)        }    }}