如果只是实现单张图片的缩放、位移、旋转可以参考官方文档直接使用transformable即可。
在实际使用时会考虑拖动的边界,当图片拖到靠近屏幕边缘需要阻止,计算边界方法:
private fun calOffset(
imgSize: Size,
scale: Float,
offsetChanged: Offset,
): Offset {
if (imgSize == Size.Unspecified) return Offset.Zero
val px = imgSize.width * (scale - 1f) / 2f
val py = imgSize.height * (scale - 1f) / 2f
var np = offsetChanged
val xDiff = np.x.absoluteValue - px
val yDiff = np.y.absoluteValue - py
if (xDiff > 0)
np = np.copy(x = px * np.x.absoluteValue / np.x)
if (yDiff > 0)
np = np.copy(y = py * np.y.absoluteValue / np.y)
return np
}
在图片的Modifier中实现
.graphicsLayer {
imgSize = size // 获取图片大小
scaleX = scale
scaleY = scale
translationX = offset.x
translationY = offset.y
}
双击放大则通过
.pointerInput() {
detectTapGestures(
onDoubleTap = { point ->
scale = ...
offset = ...
}
})
}
但是当嵌入到HorizontalPager中后,pager就无法滑动切换页面了,在这种情况下就必须要了解手势的消费情况,想办法传递到pager中。
要想处理手势就只能通过pointerInput里面的event来处理消费情况,里面有detectTransformGestures可以用来替换Modifier中的transformable,但是detectTransformGestures没有暴露event出来,我们通过源码可以看到event都被消费了:
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
) {
awaitEachGesture {
...
if (pastTouchSlop) {
val centroid = event.calculateCentroid(useCurrent = false)
val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
if (effectiveRotation != 0f ||
zoomChange != 1f ||
panChange != Offset.Zero
) {
onGesture(centroid, panChange, zoomChange, effectiveRotation)
}
event.changes.fastForEach {
if (it.positionChanged()) {
it.consume() //此处被消费了
}
}
}
...
}
}
所以此处最简单的的处理方式就是重写detectTransformGestures方法,将onGesture增加返回值,然后在消费之前判断是否需要被消费:
...
// 返回是否允许消费
allowConsume = onGesture(centroid, panChange, zoomChange, effectiveRotation)
...
event.changes.fastForEach {
// 只有允许消费的才消费
if (allowConsume && it.positionChanged()) {
it.consume()
}
}
在计算图片offset时判断是否允许消费:
private fun calOffset(
imgSize: Size,
scale: Float,
offsetChanged: Offset,
isInvalid: (Boolean) -> Unit = {} // true:将手势交还父组件(pager)
): Offset {
...
// 当横向移动超出边界,且横向移动大于纵向移动时,对子组件属于无效手势,此时将手势给父组件
isInvalid(xDiff > 0 && xDiff > yDiff)
...
}
完整预览的代码:
// 如果是多张图片,page对应外层pager页码
// 如果是单张图片不用传pagerScope
@OptIn(ExperimentalPagerApi::class)
@Composable
fun PhotoPreview(data: Any?, page: Int = 0, pagerScope: PagerScope? = null) {
var scale by remember { mutableStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
var imgSize: Size = Size.Unspecified
Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) {
AsyncImage(model = data, contentDescription = null, contentScale = ContentScale.Fit,
modifier = Modifier
.graphicsLayer {
imgSize = size
scaleX = scale
scaleY = scale
translationX = offset.x
translationY = offset.y
// 页面切换时需要恢复scale、offset
if (pagerScope != null) {
val pageOffset =
pagerScope.calculateCurrentOffsetForPage(page = page).absoluteValue
if (pageOffset == 1f) {
scale = 1f
offset = Offset.Zero
}
}
}
.pointerInput("transform") {
// 此处为自定义手势检测,主要是增加返回值判断是否需要消费
detectCustomTransformGestures { _, pan, zoom, _ ->
var requestIgnoreConsume = false
scale = (zoom * scale).coerceAtLeast(1f)
scale = if (scale > 5f) 5f else scale
offset = calOffset(imgSize, scale, offset + pan) {
requestIgnoreConsume = it
}
return@detectCustomTransformGestures requestIgnoreConsume
}
}
.pointerInput("tap") {
detectTapGestures(
onDoubleTap = { point ->
val center = Offset(imgSize.width / 2, imgSize.height / 2)
val realPoint = offset + center - point
scale = if (scale <= 1f) 2f else 1f
offset = if (scale <= 1f) Offset.Zero else {
calOffset(imgSize, scale, realPoint * 2f)
}
}
)
})
}
}
private fun calOffset(
imgSize: Size,
scale: Float,
offsetChanged: Offset,
isInvalid: (Boolean) -> Unit = {}
): Offset {
if (imgSize == Size.Unspecified) return Offset.Zero
val px = imgSize.width * (scale - 1f) / 2f
val py = imgSize.height * (scale - 1f) / 2f
var np = offsetChanged
val xDiff = np.x.absoluteValue - px
val yDiff = np.y.absoluteValue - py
if (xDiff > 0)
np = np.copy(x = px * np.x.absoluteValue / np.x)
if (yDiff > 0)
np = np.copy(y = py * np.y.absoluteValue / np.y)
isInvalid(xDiff > 0 && xDiff > yDiff)
return np
}