Android Jetpack compose 下的简版 PhotoView
可双击放大、还原,可缩放、平移
/**
* 放大倍数 a: 2倍, 放大倍数 b: 容器宽/图片宽、容器长/图片长的较大值.
* 双击: 放大到倍数 min(a,b), 缩小到原始尺寸
* 双指: 放大到倍数 max(a,b), 缩小到原始尺寸
*/
@Composable
fun PhotoView(click: (() -> Unit)? = null) {
var imageInitialSize by remember {
mutableStateOf(IntSize.Zero)
}
var boxSize by remember {
mutableStateOf(IntSize.Zero)
}
var maxScale by remember {
mutableStateOf(1f)
}
var scale by remember {
mutableStateOf(1f)
}
var offset by remember {
mutableStateOf(Offset.Zero)
}
// 使用 transformable 进行缩放、平移和旋转, 但是都需要双指, 不太适用图片放大后的平移(通常是单指)
val transformState = rememberTransformableState { zoomChange, panChange, rotationChange ->
scale = (scale * zoomChange).coerceIn(1f, maxScale)
// 1. 直接使用平移距离
// offset += panChange
// 2. 考虑图片超出容器边界问题
/*var x = offset.x
if (scale * imageInitialSize.width > boxSize.width) {
val delta = scale * imageInitialSize.width - boxSize.width
x = (offset.x + panChange.x).coerceIn(-delta / 2, delta / 2)
}
var y = offset.y
if (scale * imageInitialSize.height > boxSize.height) {
val delta = scale * imageInitialSize.height - boxSize.height
y = (offset.y + panChange.y).coerceIn(-delta / 2, delta / 2)
}
offset = Offset(x, y)*/
}
Box(modifier = Modifier
.fillMaxSize()
.graphicsLayer {
scaleX = scale
scaleY = scale
translationX = offset.x
translationY = offset.y
}
// 1. 使用 transformable 处理缩放和平移
//.transformable(transformState)
// 2. 使用 awaitPointerEventScope 自行处理缩放和平移
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
do {
val event = awaitPointerEvent()
val zoomChange = event.calculateZoom()
// LogUtils.d("zoomChange: $zoomChange")
scale *= zoomChange
var panChange = event.calculatePan()
// LogUtils.d("panChange: $panChange")
// 乘以 scale 防止在图片放大状态下移动得特别慢
panChange *= scale
// 为了让放大倍数超过 maxScale 时还可以随手势继续放大,
// 但是要手松开后回到 maxScale 这个状态, 在下面 awaitPointerEventScope 结束后
// 执行了 scale.coerceIn(1f, maxScale).
// 因此这个地方也要临时使用最后的真实值来计算 offset, 否则会出现整个图片上下 offset
// 不一致的情况(放大到超过 maxScale 后再松手)
val tempScale = scale.coerceIn(1f, maxScale)
var x = offset.x
if (tempScale * imageInitialSize.width > boxSize.width) {
val delta = tempScale * imageInitialSize.width - boxSize.width
x = (offset.x + panChange.x).coerceIn(-delta / 2, delta / 2)
}
var y = offset.y
if (tempScale * imageInitialSize.height > boxSize.height) {
val delta = tempScale * imageInitialSize.height - boxSize.height
y = (offset.y + panChange.y).coerceIn(-delta / 2, delta / 2)
}
if (x != offset.x || y != offset.y) {
offset = Offset(x, y)
}
event.changes.forEach {
if (it.positionChanged()) {
it.consume()
}
}
} while (event.changes.any { it.pressed })
}
scale = scale.coerceIn(1f, maxScale)
if (scale == 1f) {
offset = Offset.Zero
}
}
}
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
offset = Offset.Zero
if (scale != 1.0f) {
scale = 1.0f
} else {
scale = min(2f, maxScale)
}
}, onTap = {
click?.invoke()
}
)
}
.onSizeChanged {
// LogUtils.d("Box onSizeChanged")
boxSize = it
val xRatio = it.width.toFloat() / imageInitialSize.width
val yRatio = it.height.toFloat() / imageInitialSize.height
maxScale = max(2f, max(xRatio, yRatio))
},
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.onSizeChanged {
// LogUtils.d("Image onSizeChanged")
imageInitialSize = it
}, painter = painterResource(id = R.drawable.screenshot), contentDescription = ""
)
}
}
@Preview(showBackground = true)
@Composable
fun PhotoViewPreview() {
PhotoView()
}