Jetpack Compose 实现图片的 Pinch Zoom 效果,代码非常简单,我们看一下:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePinchZoomRotateTheme {
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
BoxWithConstraints(
modifier = Modifier.fillMaxWidth().aspectRatio(1280f / 959f)
) {
val state = rememberTransformableState { zoomChange, panChange, rotationChange ->
scale = (scale * zoomChange).coerceIn(1f, 5f)
rotation += rotationChange
val extraWidth = (scale - 1) * constraints.maxWidth
val extraHeight = (scale - 1) * constraints.maxHeight
val maxX = extraWidth / 2
val maxY = extraHeight / 2
offset = Offset(
x = (offset.x + scale * panChange.x).coerceIn(-maxX, maxX),
y = (offset.y + scale * panChange.y).coerceIn(-maxY, maxY),
)
}
Image(
painter = painterResource(R.drawable.kermit),
contentDescription = null,
modifier = Modifier.fillMaxWidth()
.graphicsLayer {
scaleX = scale
scaleY = scale
rotationZ = rotation
translationX = offset.x
translationY = offset.y
}
.transformable(state)
)
}
}
}
}
}
同样的功能还可以使用 detectTransformGestures
API 来实现,这里就不详细介绍了,具体可以参考我的 Jetpack Compose 专栏中的另一篇文章: Jetpack Compose中的手势操作和事件处理 。
下面尝试解释一下上面代码中 rememberTransformableState
的回调函数中所做的事情,为了简化,我们可以先将上面 rememberTransformableState
的回调代码写成下面这样:
val state = rememberTransformableState { zoomChange, panChange, rotationChange ->
scale = (scale * zoomChange).coerceIn(1f, 5f)
rotation += rotationChange
offset += panChange
}
这样其实就可以实现缩放、旋转、平移效果,只不过有一点点小瑕疵:
我们发现图片平移时周围会出现空白,所以 rememberTransformableState
回调函数中相比上面简化版本多余的代码就是为了处理这一点点瑕疵所做的修正工作,也就是下面代码:
val state = rememberTransformableState { zoomChange, panChange, rotationChange ->
scale = (scale * zoomChange).coerceIn(1f, 5f)
rotation += rotationChange
val extraWidth = (scale - 1) * constraints.maxWidth
val extraHeight = (scale - 1) * constraints.maxHeight
val maxX = extraWidth / 2
val maxY = extraHeight / 2
offset = Offset(
x = (offset.x + scale * panChange.x).coerceIn(-maxX, maxX),
y = (offset.y + scale * panChange.y).coerceIn(-maxY, maxY),
)
}
我们先看下面这两行:
val extraWidth = (scale - 1) * constraints.maxWidth
val extraHeight = (scale - 1) * constraints.maxHeight
这两行是干嘛呢,请看图:
由于初始 scale
为 1f
,当我们放大之后,用 scale - 1f
去乘以 constraints.maxWidth
得到的extraWidth
其实就是放大后的矩形宽度比原始矩形宽度多出的部分,显然此时左右移动的时候,左边和右边不应该超出 extraWidth
的一半,因为再多一点的话就会看到空白区域了。对于高度也是同理。
因此剩余的代码就好理解了:
val maxX = extraWidth / 2
val maxY = extraHeight / 2
offset = Offset(
x = (offset.x + scale * panChange.x).coerceIn(-maxX, maxX),
y = (offset.y + scale * panChange.y).coerceIn(-maxY, maxY),
)
这里使用coerceIn
限制了偏移量的移动范围,在往左边拖动的时候,不能超过左边的extraWidth
的一半,往右边拖动的时候,同样不能超过右边的extraWidth
的一半,上下拖同理。
Compose 实现图片的手势操作比传统 View 简便太多了,在传统 View 体系中,如果要对 ImageView
控件做同样的事情,那可要费老劲了。。往往我们需要自定义 View,然后还要封装成一个工具库来使用,例如下面列出两个开源库,你可以查看它们的源码的数量级来对比一下Jetpack Compose:
- PhotoView:https://github.com/chrisbanes/PhotoView,该库主要通过 PhotoView.java 和 PhotoViewAttacher.java 两个核心类实现,全库总计代码超过 1000 行。
- ImageViewZoom:https://github.com/sephiroth74/ImageViewZoom,该库主要通过 ImageViewTouch.java 和 ImageViewTouchBase.java两个核心类实现,全库总计代码超过 1000 行。
可见 Jetpack Compose 短短几十行代码就实现了传统 View 体系中动辄需要上千行的代码,简直不要太简洁,不仅清晰易懂,而且还易维护。Compose 的简洁性让我们可以有时间和精力挑战更加艰巨的任务。所以还没有用 Compose 的千万不要换 Compose ,因为换了你就会发现:TM再也不想用 View 控件来开发界面了😂