Android Compose大图加载碰壁之路

背景

近期新增了发布长图,核心诉求:支持双指缩放,下载原图。

整个app 以Compose为主,先前已经引入相关的三方库:

  1. github.com/coil-kt/coi...: 图片加载
  2. github.com/usuiat/Zoom...: 缩放

理所当然,选择基于上述三方库快速构建,但执行过程中发现有很多细节、坑需要处理。

初步接入

kotlin 复制代码
@Composable fun PhotoItem(
    uri: String,
    modifier: Modifier = Modifier,
) {
    AsyncImage(
        model = "https://example.com/image.jpg",
        contentDescription = null,
        modifier = Modifier.zoomable(rememberZoomState()),
    )
}

对于加载长图而言,该版本存在几个问题:

  1. 缺少loading效果
  2. maxScale无法动态适配网络图片宽高
  3. 图片放大后太糊

逐个击破

loading效果

使用SubcomposeAsyncImage

kotlin 复制代码
@Composable fun PhotoItem(
    uri: String,
    modifier: Modifier = Modifier,
) {
    SubcomposeAsyncImage(
        model = uri,
        loading = {
            CircularProgressIndicator()
        },
        modifier = Modifier.zoomable(rememberZoomState()),
    )
}

maxScale无法适配网络图片宽高

目前Zoomable最新版本1.6.1, maxScale作为ZoomState的构造参数,没法动态修改,如果在PhotoItem内部通过remember变量监听coil image result生成ZoomState,会导致图片无法交互的bug。

只能外围包裹一个ZoomablePhotoItem,代码如下:

kotlin 复制代码
@Composable
private fun ZoomablePhotoItem(uri: String) {
    var intrinsicSize by remember {
        mutableStateOf(Size.Zero)
    }
    var layoutSize by remember {
        mutableStateOf(IntSize.Zero)
    }
    key(intrinsicSize, layoutSize) {
        val zoomState: ZoomState = remember(intrinsicSize, layoutSize) {
            if (intrinsicSize == Size.Zero || layoutSize == IntSize.Zero) {
                ZoomState()
            } else {
                val maxScale = ZoomScaleUtil.getMaxZoomScale(
                    layoutWidth = layoutSize.width.toFloat(),
                    layoutHeight = layoutSize.height.toFloat(),
                    imageIntrinsicWidth = intrinsicSize.width,
                    imageIntrinsicHeight = intrinsicSize.height,
                )
                ZoomState(maxScale, intrinsicSize)
            }
        }
        PhotoItem(
            uri = uri,
            zoomState = zoomState,
            onIntrinsicSizeResult = { size ->
                intrinsicSize = size
            },
            onLayoutSizeResult = { intSize ->
                layoutSize = intSize
            },
        )
    }
}

@Composable
private fun PhotoItem(
    uri: String,
    zoomState: ZoomState,
    onIntrinsicSizeResult: (intrinsicSize: Size) -> Unit,
    onLayoutSizeResult: (IntSize) -> Unit,
    modifier: Modifier = Modifier,
) {
    SubcomposeAsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(uri)
            .listener { _, result ->
                val size = Size(
                    result.drawable.intrinsicWidth.toFloat(),
                    result.drawable.intrinsicHeight.toFloat(),
                )
                onIntrinsicSizeResult(size)
            }
            .build(),
        loading = {
            CircularProgressIndicator()
        },
        contentDescription = null,
        contentScale = ContentScale.Fit,
        modifier = modifier
            .fillMaxSize()
            .onSizeChanged(onLayoutSizeResult)
            .zoomable(zoomState),
    )
}

对于计算宽高比,提供了一个util方法:保证图片能放大到没有黑边

  1. scale=1 表示初始状态,同ImageView#centerInside
  2. maxScale=max(图片放大到没有黑边, 5)
kotlin 复制代码
internal object ZoomScaleUtil {
    private const val DEFAULT_MAX_SCALE = 5.0f

    fun getMaxZoomScale(
        layoutWidth: Float,
        layoutHeight: Float,
        imageIntrinsicWidth: Float,
        imageIntrinsicHeight: Float,
    ): Float {
        val layoutRatio = layoutWidth / layoutHeight
        val imageRatio = imageIntrinsicWidth / imageIntrinsicHeight

        val maxScale = if (layoutRatio > imageRatio) {
            // layoutHeight as image default displayHeight
            // displayWidth/displayHeight = imageIntrinsicWidth/imageHeight,  displayHeight = layoutHeight
            val displayWidth = layoutHeight * imageIntrinsicWidth / imageIntrinsicHeight
            layoutWidth / displayWidth
        } else {
            // layoutWidth as image default displayWidth
            // displayWidth/displayHeight = imageIntrinsicWidth/imageHeight,  displayWidth = layoutWidth
            val displayHeight = layoutWidth / (imageIntrinsicWidth / imageIntrinsicHeight)
            layoutHeight / displayHeight
        }

        return maxScale.toBigDecimal().setScale(2, RoundingMode.FLOOR).toFloat().coerceAtLeast(DEFAULT_MAX_SCALE)
    }
}

图片太糊

这是因为coil默认根据视图大小对原图进行了处理,zoomable只对布局进行放大。 所以只需要强制coil加载原图即可。

kotlin 复制代码
...
    SubcomposeAsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(uri)
            .size(coil.size.Size.ORIGINAL), // ORIGINAL表示原图
        ...
    )
...

本以为一切就好了,一运行直接crash了。 原图:

原图 crash日志

173185920 = 1440 x 30067 x 4,好家伙这张图直接干到165M了! 根据日志 可以追踪到RecordingCanvas中上限是100M。

大图处理

可以使用经典的三方库:github.com/davemorriss... 出于以下原因,没有引入该库:

  1. 项目中对三方包的审核较为严格,引入流程很繁琐
  2. 大部分图片<10M(上传限制了10M)

新写了一个 coil Transformation,保证展示的Bitmap内存占用小于100M

kotlin 复制代码
...
    SubcomposeAsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(uri)
            .size(coil.size.Size.ORIGINAL), // ORIGINAL表示原图
            .transformations(LargeImageTransformation()) // 图片压缩至80M以内
            .allowHardware(false)  // 部分图片hardware失败
        ...
    )
...

class LargeImageTransformation : Transformation {
    override val cacheKey: String
        get() = LargeImageTransformation::class.java.name

    override suspend fun transform(input: Bitmap, size: coil.size.Size): Bitmap {
        return input.extractThumbnail(input.byteCount, BITMAP_BYTE_COUNT_MAX_LIMIT)
    }

    companion object {
        // App will crash once Bitmap#getByteCount > 100M when displaying
        // @see android.graphics.RecordingCanvas.throwIfCannotDraw
        private const val BITMAP_BYTE_COUNT_MAX_LIMIT = 80 * 1024 * 1024
    }
}

private fun Bitmap.extractThumbnail(currentSize: Int, limitSize: Int): Bitmap {
    if (currentSize < limitSize) return this
    val ratio = sqrt(currentSize.toFloat() / limitSize) + 0.01
    val width = (this.width / ratio).toInt()
    val height = (this.height / ratio).toInt()
    return Bitmap.createScaledBitmap(this, width, height, true)
}

最终效果:

超宽图 超长图

总结

本文通过Coil和Zoomable实现了Compose大图预览,遇到了几个问题,并逐一给出解决方案。

相关推荐
怣疯knight37 分钟前
Windows不安装 Android Studio如何打包安卓软件
android·windows·android studio
ke_csdn43 分钟前
从Java演变到Kotlin下的jet pack
android
wenzhangli71 小时前
在低代码设计中践行 Harness Engineering
android·低代码·rxjava
xingpanvip2 小时前
星盘接口开发文档:组合三限盘接口指南
android·开发语言·前端·python·php·lua
TechMix3 小时前
【fkw学习笔记】Android 13 AOSP 源码添加系统预置应用实战指南
android·笔记·学习
云起SAAS3 小时前
私域直播系统UniApp源码 多商户商城+直播带货 微信小程序+H5+安卓iOS
android·微信小程序·uni-app·私域直播系统
空中海3 小时前
01. 安卓逆向基础、环境搭建与授权
android
星河耀银海3 小时前
JAVA 泛型与通配符:从原理到实战应用
android·java·服务器
Ada大侦探3 小时前
新手小白学习数据分析01----数据分析师???& 数据分析思维学习
android·学习·数据分析
空中海3 小时前
安卓逆向5. 安卓风险防护、加固复测与综合
android