一直以来,在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) } }}