合成文档中所述的默认缩放行为会干扰拖动手势,并围绕可缩放对象的中心(而不是手指)旋转和缩放
有更好的方法吗?
这是一个非常简单的可缩放图像。
@Composable
fun ZoomableImage() {
var scale by remember { mutableStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
Box(
Modifier
.size(600.dp)
) {
Image(
painter = rememberImagePainter(data = "https://picsum.photos/600/600"),
contentDescription = "A Content description",
modifier = Modifier
.align(Alignment.Center)
.graphicsLayer(
scaleX = scale,
scaleY = scale,
translationX = if (scale > 1f) offset.x else 0f,
translationY = if (scale > 1f) offset.y else 0f
)
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { _, pan: Offset, zoom: Float, _ ->
offset += pan
scale = (scale * zoom).coerceIn(0.5f, 4f)
}
)
}
)
}
}
仅支持缩放和平移。不支持旋转和双击。要获得稍微平滑的平移,您可以将小乘数应用于pan
,例如:
offset += pan * 1.5f
我还添加了强制输入
,以避免在边界看起来很奇怪之前放大/缩小。如果需要,可以随意删除强制输入
。也可以删除包含的框
和对齐
。仅当我们之前缩放过时,才会应用平移。这看起来更自然。
欢迎反馈和改进
我将此解决方案中的代码制作成一个库:de.mr-pine.utils:zoomables
您必须将指针输入范围与检测变形几何图形一起使用,并将此功能用作您的 onGesture:
fun onTransformGesture(
centroid: Offset,
pan: Offset,
zoom: Float,
transformRotation: Float
) {
offset += pan
scale *= zoom
rotation += transformRotation
val x0 = centroid.x - imageCenter.x
val y0 = centroid.y - imageCenter.y
val hyp0 = sqrt(x0 * x0 + y0 * y0)
val hyp1 = zoom * hyp0 * (if (x0 > 0) {
1f
} else {
-1f
})
val alpha0 = atan(y0 / x0)
val alpha1 = alpha0 + (transformRotation * ((2 * PI) / 360))
val x1 = cos(alpha1) * hyp1
val y1 = sin(alpha1) * hyp1
transformOffset =
centroid - (imageCenter - offset) - Offset(x1.toFloat(), y1.toFloat())
offset = transformOffset
}
以下是如何围绕触摸输入旋转/缩放的示例,该示例还支持滑动和双击以重置缩放:
val scope = rememberCoroutineScope()
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
var dragOffset by remember { mutableStateOf(Offset.Zero) }
var imageCenter by remember { mutableStateOf(Offset.Zero) }
var transformOffset by remember { mutableStateOf(Offset.Zero) }
Box(
Modifier
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
if (scale != 1f) {
scope.launch {
state.animateZoomBy(1 / scale)
}
offset = Offset.Zero
rotation = 0f
} else {
scope.launch {
state.animateZoomBy(2f)
}
}
}
)
}
.pointerInput(Unit) {
val panZoomLock = true
forEachGesture {
awaitPointerEventScope {
var transformRotation = 0f
var zoom = 1f
var pan = Offset.Zero
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
var lockedToPanZoom = false
var drag: PointerInputChange?
var overSlop = Offset.Zero
val down = awaitFirstDown(requireUnconsumed = false)
var transformEventCounter = 0
do {
val event = awaitPointerEvent()
val canceled = event.changes.fastAny { it.positionChangeConsumed() }
var relevant = true
if (event.changes.size > 1) {
if (!canceled) {
val zoomChange = event.calculateZoom()
val rotationChange = event.calculateRotation()
val panChange = event.calculatePan()
if (!pastTouchSlop) {
zoom *= zoomChange
transformRotation += rotationChange
pan += panChange
val centroidSize =
event.calculateCentroidSize(useCurrent = false)
val zoomMotion = abs(1 - zoom) * centroidSize
val rotationMotion =
abs(transformRotation * PI.toFloat() * centroidSize / 180f)
val panMotion = pan.getDistance()
if (zoomMotion > touchSlop ||
rotationMotion > touchSlop ||
panMotion > touchSlop
) {
pastTouchSlop = true
lockedToPanZoom =
panZoomLock && rotationMotion < touchSlop
}
}
if (pastTouchSlop) {
val eventCentroid = event.calculateCentroid(useCurrent = false)
val effectiveRotation =
if (lockedToPanZoom) 0f else rotationChange
if (effectiveRotation != 0f ||
zoomChange != 1f ||
panChange != Offset.Zero
) {
onTransformGesture(
eventCentroid,
panChange,
zoomChange,
effectiveRotation
)
}
event.changes.fastForEach {
if (it.positionChanged()) {
it.consumeAllChanges()
}
}
}
}
} else if (transformEventCounter > 3) relevant = false
transformEventCounter++
} while (!canceled && event.changes.fastAny { it.pressed } && relevant)
do {
val event = awaitPointerEvent()
drag = awaitTouchSlopOrCancellation(down.id) { change, over ->
change.consumePositionChange()
overSlop = over
}
} while (drag != null && !drag.positionChangeConsumed())
if (drag != null) {
dragOffset = Offset.Zero
if (scale !in 0.92f..1.08f) {
offset += overSlop
} else {
dragOffset += overSlop
}
if (drag(drag.id) {
if (scale !in 0.92f..1.08f) {
offset += it.positionChange()
} else {
dragOffset += it.positionChange()
}
it.consumePositionChange()
}
) {
if (scale in 0.92f..1.08f) {
val offsetX = dragOffset.x
if (offsetX > 300) {
onSwipeRight()
} else if (offsetX < -300) {
onSwipeLeft()
}
}
}
}
}
}
}
) {
ZoomComposable(
modifier = Modifier
.offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
.graphicsLayer(
scaleX = scale - 0.02f,
scaleY = scale - 0.02f,
rotationZ = rotation
)
.onGloballyPositioned { coordinates ->
val localOffset =
Offset(
coordinates.size.width.toFloat() / 2,
coordinates.size.height.toFloat() / 2
)
val windowOffset = coordinates.localToWindow(localOffset)
imageCenter = coordinates.parentLayoutCoordinates?.windowToLocal(windowOffset)
?: Offset.Zero
}
)
}
问题内容: 考虑以下两行代码 和 在性能上,以上两个语句有什么区别吗?我见过很多人使用后者,当被问及他们说这是最佳实践时,没有充分的理由。 问题答案: 没有不同。 第二个原因仅仅是因为C / C ++程序员总是执行分配而不是比较。 例如 而java编译器会生成编译错误。 因此,由于可读性强,我个人更喜欢第一个,人们倾向于从左到右阅读,而不是。
问题内容: String s = “”; for(i=0;i<....){ s = some Assignment; } 要么 我不需要在循环外再次使用“ s”。第一个选项可能更好,因为不会每次都初始化一个新的String。但是,第二个结果将导致变量的范围仅限于循环本身。 编辑:回应米尔豪斯的回答。在循环中将String分配给常量是没有意义的吗?不,这里的“某些分配”是指从要迭代的列表中获得的变化
问题内容: 有时我们可以同时使用派生表和临时表编写查询。我的问题是哪个更好?为什么? 问题答案: 派生表是一种逻辑构造。 可以将其存储在中,在运行时通过在每次访问时重新评估基础语句来构建,甚至可以对其进行优化。 临时表是一种物理构造。它是在其中创建的表,并在其中填充了值。 哪种更好取决于查询所使用的查询,用于派生表的语句以及许多其他因素。 例如,每次使用时都可以(并且很可能会)重新评估in中的(公
问题内容: 如何使用https://www.amcharts.com/demos/line-chart-with-scroll-and- zoom/ 这样的图表 我对这些功能特别感兴趣 为了能够使用这两个选择控件选择域轴窗口。 为了能够通过选择部分域轴进行缩放。 为了能够缩小并查看大图。 为了能够映射到范围轴(Y)并在任意点(无标记)像工具提示一样查看该气球上的值 我在这里尝试了可滚动的JFree
如何拥有像https://www.amcharts.com/demos/line-chart-with-scroll-and-zoom/这样的图表 null 我在这里做了初步尝试,可滚动JFree域轴和自定义标记标签,在垃圾上帝的帮助下,我使域可滚动。 我仍然缺少这些功能,以便能够有效地使用graph。
问题内容: 我目前正在使用JFreeChart显示框线图的基于Java的项目。 我的问题是如何显示包含带有大约20个类别和5个以上系列的CategoryDataset的箱形图的图表。 当前,如果未设置ChartPanel的首选大小,则图例,标签和注释可读取,但Boxplots太小。或设置ChartPanel的大小,以使Boxplots具有可接受的大小,然后将图例,标签和注释水平拉伸。 我的问题是,