如何在 Jetpack Compose 中获取 onTouchEvent?
How can I get onTouchEvent in Jetpack Compose?
在普通视图中,我们可以有onTouchEvent
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {}
MotionEvent.ACTION_MOVE -> {}
MotionEvent.ACTION_UP -> {}
else -> return false
}
invalidate()
return true
}
在 Jetpack Compose 中,我只能发现我们在修饰符中有 tapGestureFilter
,它只接受 ACTION_UP
的动作。
Modifier
.tapGestureFilter { Log.d("Track", "Tap ${it.x} | ${it.y}") }
.doubleTapGestureFilter { Log.d("Track", "DoubleTap ${it.x} | ${it.y}") }
Jetpack Compose 有等效的 onTouchEvent
吗?
经过一番研究,看起来可以使用 dragGestureFilter
,与 tapGestureFilter
混合使用
Modifier
.dragGestureFilter(object: DragObserver {
override fun onDrag(dragDistance: Offset): Offset {
Log.d("Track", "onActionMove ${dragDistance.x} | ${dragDistance.y}")
return super.onDrag(dragDistance)
}
override fun onStart(downPosition: Offset) {
Log.d("Track", "onActionDown ${downPosition.x} | ${downPosition.y}")
super.onStart(downPosition)
}
override fun onStop(velocity: Offset) {
Log.d("Track", "onStop ${velocity.x} | ${velocity.y}")
super.onStop(velocity)
}
}, { true })
.tapGestureFilter {
Log.d("NGVL", "onActionUp ${it.x} | ${it.y}")
}
仍然使用 tagGestureFilter
的原因是因为 onStop
不提供位置,而只提供速度,因此 tapGestureFilter
确实有助于提供最后的位置(如果需要)
我们有一个单独的 package,这非常有用。
有两个主要的扩展功能适合您:
如果你想处理事件,我建议使用 pointerInteropFilter
,它类似于 View.onTouchEvent
。它与 modifier
:
一起使用
Column(modifier = Modifier.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {}
MotionEvent.ACTION_MOVE -> {}
MotionEvent.ACTION_UP -> {}
else -> false
}
true
})
这将根据您指定的 View.onTouchEvent
示例编写调整后的代码。
P.S。不要忘记 @ExperimentalPointerInput
注释。
可能有点晚了,但由于 compose 不断更新,所以我今天是这样做的:
Modifier
.pointerInput(Unit) {
detectTapGestures {...}
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount -> ...}
})
我们还有 detectHorizontalDragGestures
和 detectVerticalDragGestures
等人来帮助我们。
ps: 1.0.0-beta03
如果您不使用触摸 api 与现有视图代码进行互操作,pointerInteropFilter
未被描述为首选使用方式。
A special PointerInputModifier that provides access to the underlying
MotionEvents originally dispatched to Compose. Prefer pointerInput and
use this only for interoperation with existing code that consumes
MotionEvents. While the main intent of this Modifier is to allow
arbitrary code to access the original MotionEvent dispatched to
Compose, for completeness, analogs are provided to allow arbitrary
code to interact with the system as if it were an Android View.
您可以将 pointerInput
、awaitTouchDown
用于 MotionEvent.ACTION_DOWN
,将 awaitPointerEvent
用于 MotionEvent.ACTION_MOVE
和 MotionEvent.ACTION_UP
val pointerModifier = Modifier
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
// ACTION_DOWN here
do {
//This PointerEvent contains details including
// event, id, position and more
val event: PointerEvent = awaitPointerEvent()
// ACTION_MOVE loop
// Consuming event prevents other gestures or scroll to intercept
event.changes.forEach { pointerInputChange: PointerInputChange ->
pointerInputChange.consumePositionChange()
}
} while (event.changes.any { it.pressed })
// ACTION_UP is here
}
}
}
关于手势的一些关键注意事项
- pointerInput 传播是当你有多个来自
从下到上。
- 如果 children 和 parent 正在侦听它传播的输入更改
从内部 children 到外部 parent。不同于触摸事件
parent 到 children
- 如果您不使用事件,则可以使用滚动拖动等其他事件
干扰或消耗事件,大多数事件检查它是否
在传播给他们之前消耗
detectDragGestures实例源代码
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
do {
drag = awaitPointerSlopOrCancellation(
down.id,
down.type
) { change, over ->
change.consumePositionChange()
overSlop = over
}
} while (drag != null && !drag.positionChangeConsumed())
所以当你需要阻止其他事件拦截时
在awaitFirstDown
之后调用pointerInputChange.consumeDown()
,调用
pointerInputChange.consumePositionChange() 在 awaitPointerEvent
之后
和 awaitFirstDown()
有 requireUnconsumed
参数是
默认情况下为真。如果将其设置为 false,即使 pointerInput 消耗
在你的手势之前放下你仍然明白。这也是诸如拖动之类的事件无论如何都会使用它首先下降的方式。
您看到的每个可用事件 detectDragGestures
,
detectTapGestures
甚至 awaitFirstDown
使用 awaitPointerEvent
为了实现,所以使用 awaitFirstDown
、awaitPointerEvent
使用更改您可以配置自己的手势。
例如,这是我从原始 detectTransformGestures
自定义的函数,仅在特定数量的指针向下时调用。
suspend fun PointerInputScope.detectMultiplePointerTransformGestures(
panZoomLock: Boolean = false,
numberOfPointersRequired: Int = 2,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit,
) {
forEachGesture {
awaitPointerEventScope {
var rotation = 0f
var zoom = 1f
var pan = Offset.Zero
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
var lockedToPanZoom = false
awaitFirstDown(requireUnconsumed = false)
do {
val event = awaitPointerEvent()
val downPointerCount = event.changes.size
// If any position change is consumed from another pointer or pointer
// count that is pressed is not equal to pointerCount cancel this gesture
val canceled = event.changes.any { it.positionChangeConsumed() } || (
downPointerCount != numberOfPointersRequired)
if (!canceled) {
val zoomChange = event.calculateZoom()
val rotationChange = event.calculateRotation()
val panChange = event.calculatePan()
if (!pastTouchSlop) {
zoom *= zoomChange
rotation += rotationChange
pan += panChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val zoomMotion = abs(1 - zoom) * centroidSize
val rotationMotion =
abs(rotation * PI.toFloat() * centroidSize / 180f)
val panMotion = pan.getDistance()
if (zoomMotion > touchSlop ||
rotationMotion > touchSlop ||
panMotion > touchSlop
) {
pastTouchSlop = true
lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
}
}
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.forEach {
if (it.positionChanged()) {
it.consumeAllChanges()
}
}
}
}
} while (!canceled && event.changes.any { it.pressed })
}
}
}
编辑
从 1.2.0-beta01 开始,部分消耗类似
PointerInputChange.consemePositionChange()
,
PointerInputChange.consumeDownChange()
,以及一个用于消耗所有更改的 PointerInputChange.consumeAllChanges()
已弃用
PointerInputChange.consume()
是唯一一个用来防止其他 gestures/event.
我还有一个 tutorial here 详细介绍了手势
在普通视图中,我们可以有onTouchEvent
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {}
MotionEvent.ACTION_MOVE -> {}
MotionEvent.ACTION_UP -> {}
else -> return false
}
invalidate()
return true
}
在 Jetpack Compose 中,我只能发现我们在修饰符中有 tapGestureFilter
,它只接受 ACTION_UP
的动作。
Modifier
.tapGestureFilter { Log.d("Track", "Tap ${it.x} | ${it.y}") }
.doubleTapGestureFilter { Log.d("Track", "DoubleTap ${it.x} | ${it.y}") }
Jetpack Compose 有等效的 onTouchEvent
吗?
经过一番研究,看起来可以使用 dragGestureFilter
,与 tapGestureFilter
Modifier
.dragGestureFilter(object: DragObserver {
override fun onDrag(dragDistance: Offset): Offset {
Log.d("Track", "onActionMove ${dragDistance.x} | ${dragDistance.y}")
return super.onDrag(dragDistance)
}
override fun onStart(downPosition: Offset) {
Log.d("Track", "onActionDown ${downPosition.x} | ${downPosition.y}")
super.onStart(downPosition)
}
override fun onStop(velocity: Offset) {
Log.d("Track", "onStop ${velocity.x} | ${velocity.y}")
super.onStop(velocity)
}
}, { true })
.tapGestureFilter {
Log.d("NGVL", "onActionUp ${it.x} | ${it.y}")
}
仍然使用 tagGestureFilter
的原因是因为 onStop
不提供位置,而只提供速度,因此 tapGestureFilter
确实有助于提供最后的位置(如果需要)
我们有一个单独的 package,这非常有用。 有两个主要的扩展功能适合您:
如果你想处理事件,我建议使用 pointerInteropFilter
,它类似于 View.onTouchEvent
。它与 modifier
:
Column(modifier = Modifier.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {}
MotionEvent.ACTION_MOVE -> {}
MotionEvent.ACTION_UP -> {}
else -> false
}
true
})
这将根据您指定的 View.onTouchEvent
示例编写调整后的代码。
P.S。不要忘记 @ExperimentalPointerInput
注释。
可能有点晚了,但由于 compose 不断更新,所以我今天是这样做的:
Modifier
.pointerInput(Unit) {
detectTapGestures {...}
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount -> ...}
})
我们还有 detectHorizontalDragGestures
和 detectVerticalDragGestures
等人来帮助我们。
ps: 1.0.0-beta03
pointerInteropFilter
未被描述为首选使用方式。
A special PointerInputModifier that provides access to the underlying MotionEvents originally dispatched to Compose. Prefer pointerInput and use this only for interoperation with existing code that consumes MotionEvents. While the main intent of this Modifier is to allow arbitrary code to access the original MotionEvent dispatched to Compose, for completeness, analogs are provided to allow arbitrary code to interact with the system as if it were an Android View.
您可以将 pointerInput
、awaitTouchDown
用于 MotionEvent.ACTION_DOWN
,将 awaitPointerEvent
用于 MotionEvent.ACTION_MOVE
和 MotionEvent.ACTION_UP
val pointerModifier = Modifier
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
// ACTION_DOWN here
do {
//This PointerEvent contains details including
// event, id, position and more
val event: PointerEvent = awaitPointerEvent()
// ACTION_MOVE loop
// Consuming event prevents other gestures or scroll to intercept
event.changes.forEach { pointerInputChange: PointerInputChange ->
pointerInputChange.consumePositionChange()
}
} while (event.changes.any { it.pressed })
// ACTION_UP is here
}
}
}
关于手势的一些关键注意事项
- pointerInput 传播是当你有多个来自 从下到上。
- 如果 children 和 parent 正在侦听它传播的输入更改 从内部 children 到外部 parent。不同于触摸事件 parent 到 children
- 如果您不使用事件,则可以使用滚动拖动等其他事件 干扰或消耗事件,大多数事件检查它是否 在传播给他们之前消耗
detectDragGestures实例源代码
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
do {
drag = awaitPointerSlopOrCancellation(
down.id,
down.type
) { change, over ->
change.consumePositionChange()
overSlop = over
}
} while (drag != null && !drag.positionChangeConsumed())
所以当你需要阻止其他事件拦截时
在
之后awaitFirstDown
之后调用pointerInputChange.consumeDown()
,调用 pointerInputChange.consumePositionChange() 在awaitPointerEvent
和
awaitFirstDown()
有requireUnconsumed
参数是 默认情况下为真。如果将其设置为 false,即使 pointerInput 消耗 在你的手势之前放下你仍然明白。这也是诸如拖动之类的事件无论如何都会使用它首先下降的方式。您看到的每个可用事件
detectDragGestures
,detectTapGestures
甚至awaitFirstDown
使用awaitPointerEvent
为了实现,所以使用awaitFirstDown
、awaitPointerEvent
使用更改您可以配置自己的手势。
例如,这是我从原始 detectTransformGestures
自定义的函数,仅在特定数量的指针向下时调用。
suspend fun PointerInputScope.detectMultiplePointerTransformGestures(
panZoomLock: Boolean = false,
numberOfPointersRequired: Int = 2,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit,
) {
forEachGesture {
awaitPointerEventScope {
var rotation = 0f
var zoom = 1f
var pan = Offset.Zero
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
var lockedToPanZoom = false
awaitFirstDown(requireUnconsumed = false)
do {
val event = awaitPointerEvent()
val downPointerCount = event.changes.size
// If any position change is consumed from another pointer or pointer
// count that is pressed is not equal to pointerCount cancel this gesture
val canceled = event.changes.any { it.positionChangeConsumed() } || (
downPointerCount != numberOfPointersRequired)
if (!canceled) {
val zoomChange = event.calculateZoom()
val rotationChange = event.calculateRotation()
val panChange = event.calculatePan()
if (!pastTouchSlop) {
zoom *= zoomChange
rotation += rotationChange
pan += panChange
val centroidSize = event.calculateCentroidSize(useCurrent = false)
val zoomMotion = abs(1 - zoom) * centroidSize
val rotationMotion =
abs(rotation * PI.toFloat() * centroidSize / 180f)
val panMotion = pan.getDistance()
if (zoomMotion > touchSlop ||
rotationMotion > touchSlop ||
panMotion > touchSlop
) {
pastTouchSlop = true
lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
}
}
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.forEach {
if (it.positionChanged()) {
it.consumeAllChanges()
}
}
}
}
} while (!canceled && event.changes.any { it.pressed })
}
}
}
编辑
从 1.2.0-beta01 开始,部分消耗类似
PointerInputChange.consemePositionChange()
,
PointerInputChange.consumeDownChange()
,以及一个用于消耗所有更改的 PointerInputChange.consumeAllChanges()
已弃用
PointerInputChange.consume()
是唯一一个用来防止其他 gestures/event.
我还有一个 tutorial here 详细介绍了手势