[go: nahoru, domu]

blob: 80e0fa60d313facebf897293af8079a5b8817dae [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.Composable
import androidx.compose.remember
import androidx.ui.core.Modifier
import androidx.ui.core.PointerEventPass
import androidx.ui.core.PointerInputChange
import androidx.ui.core.anyPositionChangeConsumed
import androidx.ui.core.changedToDown
import androidx.ui.core.changedToUpIgnoreConsumed
import androidx.ui.core.consumeDownChange
import androidx.ui.core.pointerinput.PointerInputFilter
import androidx.ui.geometry.Offset
import androidx.ui.unit.IntPxSize
import androidx.ui.unit.PxPosition
/**
* This gesture detector has callbacks for when a press gesture starts and ends for the purposes of
* displaying visual feedback for those two states.
*
* More specifically:
* - It will call [onStart] if the first pointer down it receives during the
* [PointerEventPass.PostUp] pass is not consumed.
* - It will call [onStop] if [onStart] has been called and the last [PointerInputChange] it
* receives during the [PointerEventPass.PostUp] pass has an up change, consumed or not, indicating
* the press gesture indication should end.
* - It will call [onCancel] if movement has been consumed by the time of the
* [PointerEventPass.PostDown] pass, indicating that the press gesture indication should end because
* something moved.
*
* This gesture detector always consumes the down change during the [PointerEventPass.PostUp] pass.
*/
// TODO(b/139020678): Probably has shared functionality with other press based detectors.
@Composable
fun PressIndicatorGestureDetector(
onStart: ((PxPosition) -> Unit)? = null,
onStop: (() -> Unit)? = null,
onCancel: (() -> Unit)? = null,
enabled: Boolean = true
): Modifier {
val recognizer = remember { PressIndicatorGestureRecognizer() }
recognizer.onStart = onStart
recognizer.onStop = onStop
recognizer.onCancel = onCancel
recognizer.setEnabled(enabled)
return PointerInputModifierImpl(recognizer)
}
internal class PressIndicatorGestureRecognizer : PointerInputFilter() {
/**
* Called if the first pointer's down change was not consumed by the time this gesture
* recognizer receives it in the [PointerEventPass.PostUp] pass.
*
* This callback should be used to indicate that the press state should be shown. An [Offset]
* is provided to indicate where the first pointer made contact with this gesrure detector.
*/
var onStart: ((PxPosition) -> Unit)? = null
/**
* Called if onStart was attempted to be called (it may have been null), no pointer movement
* was consumed, and the last pointer went up (consumed or not).
*
* This should be used for removing visual feedback that indicates that the press has ended with
* a completed press released gesture.
*/
var onStop: (() -> Unit)? = null
/**
* Called if onStart was attempted to be called (it may have been null), and either:
* 1. Pointer movement was consumed by the time [PointerEventPass.PostDown] reaches this
* gesture recognizer.
* 2. [setEnabled] is called with false.
* 3. This gesture recognizer is removed from the hierarchy, or it has no descendants
* to define it's position or size.
* 4. The Compose root is notified that it will no longer receive input, and thus onStop
* will never be reached (For example, the Android View that hosts compose receives
* MotionEvent.ACTION_CANCEL).
*
* This should be used for removing visual feedback that indicates that the press gesture was
* cancelled.
*/
var onCancel: (() -> Unit)? = null
private var state = State.Idle
/**
* Sets whether this gesture recognizer is enabled. True by default.
*
* When enabled, this gesture recognizer will act normally.
*
* When disabled, this gesture recognizer will not process any input. No aspects
* of any [PointerInputChange]s will be consumed and no callbacks will be called.
*
* If the last callback that was attempted to be called was [onStart] ([onStart] may have
* been false) and [enabled] is false, [onCancel] will be called.
*/
fun setEnabled(enabled: Boolean) {
if (state == State.Started) {
// If the state is Started and we were passed true, we don't want to change it to
// Enabled.
// If the state is Started and we were passed false, we can set to Disabled and
// call the cancel callback.
if (!enabled) {
state = State.Disabled
onCancel?.invoke()
}
} else {
// If the state is anything but Started, just set the state according to the value
// we were passed.
state =
if (enabled) {
State.Idle
} else {
State.Disabled
}
}
}
override val pointerInputHandler =
{ changes: List<PointerInputChange>, pass: PointerEventPass, bounds: IntPxSize ->
var internalChanges = changes
if (pass == PointerEventPass.InitialDown && state == State.Started) {
internalChanges = internalChanges.map {
if (it.changedToDown()) {
it.consumeDownChange()
} else {
it
}
}
}
if (pass == PointerEventPass.PostUp) {
if (state == State.Idle && internalChanges.all { it.changedToDown() }) {
// If we have not yet started and all of the changes changed to down, we are
// starting.
state = State.Started
onStart?.invoke(internalChanges.first().current.position!!)
} else if (state == State.Started) {
if (internalChanges.all { it.changedToUpIgnoreConsumed() }) {
// If we have started and all of the changes changed to up, we are stopping.
state = State.Idle
onStop?.invoke()
} else if (!internalChanges.anyPointersInBounds(bounds)) {
// If all of the down pointers are currently out of bounds, we should cancel
// as this indicates that the user does not which to trigger a press based
// event.
state = State.Idle
onCancel?.invoke()
}
}
if (state == State.Started) {
internalChanges = internalChanges.map { it.consumeDownChange() }
}
}
if (
pass == PointerEventPass.PostDown &&
state == State.Started &&
internalChanges.any { it.anyPositionChangeConsumed() }
) {
// On the final pass, if we have started and any of the changes had consumed
// position changes, we cancel.
state = State.Idle
onCancel?.invoke()
}
internalChanges
}
override val cancelHandler = {
if (state == State.Started) {
state = State.Idle
onCancel?.invoke()
}
}
private enum class State {
Disabled, Idle, Started
}
}