[go: nahoru, domu]

blob: 2deb288bc1edb47a548f25ee88841f249ae08918 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.core.gesture
import androidx.compose.remember
import androidx.ui.core.CoroutineContextAmbient
import androidx.ui.core.CustomEventDispatcher
import androidx.ui.core.Modifier
import androidx.ui.core.PointerEventPass
import androidx.ui.core.PointerId
import androidx.ui.core.PointerInputChange
import androidx.ui.core.anyPositionChangeConsumed
import androidx.ui.core.changedToDown
import androidx.ui.core.changedToUp
import androidx.ui.core.composed
import androidx.ui.core.consumeDownChange
import androidx.ui.core.gesture.customevents.DelayUpEvent
import androidx.ui.core.gesture.customevents.DelayUpMessage
import androidx.ui.core.pointerinput.PointerInputFilter
import androidx.compose.ui.geometry.Offset
import androidx.ui.unit.IntSize
import androidx.ui.util.fastAny
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
// TODO(b/138605697): This bug tracks the note below: DoubleTapGestureDetector should use the
// eventual api that will allow it to temporary block tap.
// TODO(b/138754591): The behavior of this gesture detector needs to be finalized.
// TODO(b/139020678): Probably has shared functionality with other press based detectors.
/**
* Responds to pointers going down and up (tap) and then down and up again (another tap)
* with minimal gap of time between the first up and the second down.
*
* Note: This is a temporary implementation to unblock dependents. Once the underlying API that
* allows double tap to temporarily block tap from firing is complete, this gesture detector will
* not block tap when the first "up" occurs. It will however block the 2nd up from causing tap to
* fire.
*
* Also, given that this gesture detector is so temporary, opting to not write substantial tests.
*/
fun Modifier.doubleTapGestureFilter(
onDoubleTap: (Offset) -> Unit
): Modifier = composed {
@Suppress("DEPRECATION")
val coroutineContext = CoroutineContextAmbient.current
// TODO(shepshapard): coroutineContext should be a field
val filter = remember { DoubleTapGestureFilter(coroutineContext) }
filter.onDoubleTap = onDoubleTap
PointerInputModifierImpl(filter)
}
internal class DoubleTapGestureFilter(
val coroutineContext: CoroutineContext
) : PointerInputFilter() {
lateinit var onDoubleTap: (Offset) -> Unit
private enum class State {
Idle, Down, Up, SecondDown
}
var doubleTapTimeout = DoubleTapTimeout
private var state = State.Idle
private var job: Job? = null
private lateinit var delayUpDispatcher: DelayUpDispatcher
override fun onInit(customEventDispatcher: CustomEventDispatcher) {
delayUpDispatcher = DelayUpDispatcher(customEventDispatcher)
}
override fun onPointerInput(
changes: List<PointerInputChange>,
pass: PointerEventPass,
bounds: IntSize
): List<PointerInputChange> {
if (pass == PointerEventPass.PostUp) {
if (state == State.Idle && changes.all { it.changedToDown() }) {
state = State.Down
return changes
}
if (state == State.Down && changes.all { it.changedToUp() }) {
state = State.Up
delayUpDispatcher.delayUp(changes)
job = delay(doubleTapTimeout, coroutineContext) {
state = State.Idle
delayUpDispatcher.allowUp()
}
return changes
}
if (state == State.Up && changes.all { it.changedToDown() }) {
state = State.SecondDown
job?.cancel()
delayUpDispatcher.disallowUp()
return changes
}
if (state == State.SecondDown && changes.all { it.changedToUp() }) {
state = State.Idle
onDoubleTap.invoke(changes[0].previous.position!!)
return changes.map { it.consumeDownChange() }
}
}
if (pass == PointerEventPass.PostDown) {
val noPointersAreInBoundsAndNotUpState =
(state != State.Up && !changes.anyPointersInBounds(bounds))
val anyPositionChangeConsumed = changes.fastAny { it.anyPositionChangeConsumed() }
if (noPointersAreInBoundsAndNotUpState || anyPositionChangeConsumed) {
// A pointers movement was consumed or all of our pointers are out of bounds, so
// reset to idle.
fullReset()
}
}
return changes
}
override fun onCancel() {
fullReset()
}
private fun fullReset() {
delayUpDispatcher.disallowUp()
job?.cancel()
state = State.Idle
}
private class DelayUpDispatcher(val customEventDispatcher: CustomEventDispatcher) {
// Non-writeable because we send this to customEventDispatcher and we don't want to ever
// accidentally mutate what we have sent.
private var blockedUpEvents: Set<PointerId>? = null
fun delayUp(changes: List<PointerInputChange>) {
blockedUpEvents =
changes
.mapTo(mutableSetOf()) { it.id }
.also {
customEventDispatcher.retainHitPaths(it)
customEventDispatcher.dispatchCustomEvent(
DelayUpEvent(DelayUpMessage.DelayUp, it)
)
}
}
fun disallowUp() {
unBlockUpEvents(true)
}
fun allowUp() {
unBlockUpEvents(false)
}
private fun unBlockUpEvents(upIsConsumed: Boolean) {
blockedUpEvents?.let {
val message =
if (upIsConsumed) {
DelayUpMessage.DelayedUpConsumed
} else {
DelayUpMessage.DelayedUpNotConsumed
}
customEventDispatcher.dispatchCustomEvent(
DelayUpEvent(message, it)
)
customEventDispatcher.releaseHitPaths(it)
}
blockedUpEvents = null
}
}
}