背景
近期新增了发布长图,核心诉求:支持双指缩放,下载原图。
整个app 以Compose为主,先前已经引入相关的三方库:
理所当然,选择基于上述三方库快速构建,但执行过程中发现有很多细节、坑需要处理。
初步接入
kotlin
@Composable fun PhotoItem(
uri: String,
modifier: Modifier = Modifier,
) {
AsyncImage(
model = "https://example.com/image.jpg",
contentDescription = null,
modifier = Modifier.zoomable(rememberZoomState()),
)
}
对于加载长图而言,该版本存在几个问题:
- 缺少loading效果
- maxScale无法动态适配网络图片宽高
- 图片放大后太糊
逐个击破
loading效果
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方法:保证图片能放大到没有黑边
- scale=1 表示初始状态,同
ImageView#centerInside
- 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... 出于以下原因,没有引入该库:
- 项目中对三方包的审核较为严格,引入流程很繁琐
- 大部分图片<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大图预览,遇到了几个问题,并逐一给出解决方案。