/*
* 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.compose.foundation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.foundation.legacygestures.pressIndicatorGestureFilter
import androidx.compose.foundation.legacygestures.tapGestureFilter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import kotlinx.coroutines.launch
/**
* Configure component to receive clicks via input or accessibility "click" event.
*
* Add this modifier to the element to make it clickable within its bounds and show a default
* indication when it's pressed.
*
* This version has no [MutableInteractionSource] or [Indication] parameters, default indication from
* [LocalIndication] will be used. To specify [MutableInteractionSource] or [Indication], use another
* overload.
*
* If you need to support double click or long click alongside the single click, consider
* using [combinedClickable].
*
* @sample androidx.compose.foundation.samples.ClickableSample
*
* @param enabled Controls the enabled state. When `false`, [onClick], and this modifier will
* appear disabled for accessibility services
* @param onClickLabel semantic / accessibility label for the [onClick] action
* @param role the type of user interface element. Accessibility services might use this
* to describe the element or do customizations
* @param onClick will be called when user clicks on the element
*/
fun Modifier.clickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
) = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
}
) {
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = onClick,
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
/**
* Configure component to receive clicks via input or accessibility "click" event.
*
* Add this modifier to the element to make it clickable within its bounds and show an indication
* as specified in [indication] parameter.
*
* If you need to support double click or long click alongside the single click, consider
* using [combinedClickable].
*
* @sample androidx.compose.foundation.samples.ClickableSample
*
* @param interactionSource [MutableInteractionSource] that will be used to dispatch
* [PressInteraction.Press] when this clickable is pressed. Only the initial (first) press will be
* recorded and dispatched with [MutableInteractionSource].
* @param indication indication to be shown when modified element is pressed. Be default,
* indication from [LocalIndication] will be used. Pass `null` to show no indication, or
* current value from [LocalIndication] to show theme default
* @param enabled Controls the enabled state. When `false`, [onClick], and this modifier will
* appear disabled for accessibility services
* @param onClickLabel semantic / accessibility label for the [onClick] action
* @param role the type of user interface element. Accessibility services might use this
* to describe the element or do customizations
* @param onClick will be called when user clicks on the element
*/
@Suppress("DEPRECATION")
fun Modifier.clickable(
interactionSource: MutableInteractionSource,
indication: Indication?,
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
) = composed(
factory = {
val scope = rememberCoroutineScope()
val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
val interactionUpdate =
if (enabled) {
Modifier.pressIndicatorGestureFilter(
onStart = {
scope.launch {
// Remove any old interactions if we didn't fire stop / cancel properly
pressedInteraction.value?.let { oldValue ->
val interaction = PressInteraction.Cancel(oldValue)
interactionSource.emit(interaction)
pressedInteraction.value = null
}
val interaction = PressInteraction.Press(it)
interactionSource.emit(interaction)
pressedInteraction.value = interaction
}
},
onStop = {
scope.launch {
pressedInteraction.value?.let {
val interaction = PressInteraction.Release(it)
interactionSource.emit(interaction)
pressedInteraction.value = null
}
}
},
onCancel = {
scope.launch {
pressedInteraction.value?.let {
val interaction = PressInteraction.Cancel(it)
interactionSource.emit(interaction)
pressedInteraction.value = null
}
}
}
)
} else {
Modifier
}
val tap = if (enabled) tapGestureFilter(onTap = { onClick() }) else Modifier
DisposableEffect(interactionSource) {
onDispose {
pressedInteraction.value?.let { oldValue ->
val interaction = PressInteraction.Cancel(oldValue)
interactionSource.tryEmit(interaction)
pressedInteraction.value = null
}
}
}
Modifier
.genericClickableWithoutGesture(
gestureModifiers = Modifier.then(interactionUpdate).then(tap),
interactionSource = interactionSource,
indication = indication,
enabled = enabled,
onClickLabel = onClickLabel,
role = role,
onLongClickLabel = null,
onLongClick = null,
onClick = onClick
)
},
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
properties["indication"] = indication
properties["interactionSource"] = interactionSource
}
)
/**
* Configure component to receive clicks, double clicks and long clicks via input or accessibility
* "click" event.
*
* Add this modifier to the element to make it clickable within its bounds.
*
* If you need only click handling, and no double or long clicks, consider using [clickable]
*
* This version has no [MutableInteractionSource] or [Indication] parameters, default indication
* from [LocalIndication] will be used. To specify [MutableInteractionSource] or [Indication],
* use another overload.
*
* @sample androidx.compose.foundation.samples.ClickableSample
*
* @param enabled Controls the enabled state. When `false`, [onClick], [onLongClick] or
* [onDoubleClick] won't be invoked
* @param onClickLabel semantic / accessibility label for the [onClick] action
* @param role the type of user interface element. Accessibility services might use this
* to describe the element or do customizations
* @param onLongClickLabel semantic / accessibility label for the [onLongClick] action
* @param onLongClick will be called when user long presses on the element
* @param onDoubleClick will be called when user double clicks on the element
* @param onClick will be called when user clicks on the element
*/
@ExperimentalFoundationApi
fun Modifier.combinedClickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onLongClickLabel: String? = null,
onLongClick: (() -> Unit)? = null,
onDoubleClick: (() -> Unit)? = null,
onClick: () -> Unit
) = composed(
inspectorInfo = debugInspectorInfo {
name = "combinedClickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
properties["onDoubleClick"] = onDoubleClick
properties["onLongClick"] = onLongClick
properties["onLongClickLabel"] = onLongClickLabel
}
) {
Modifier.combinedClickable(
enabled = enabled,
onClickLabel = onClickLabel,
onLongClickLabel = onLongClickLabel,
onLongClick = onLongClick,
onDoubleClick = onDoubleClick,
onClick = onClick,
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
/**
* Configure component to receive clicks, double clicks and long clicks via input or accessibility
* "click" event.
*
* Add this modifier to the element to make it clickable within its bounds.
*
* If you need only click handling, and no double or long clicks, consider using [clickable].
*
* Add this modifier to the element to make it clickable within its bounds.
*
* @sample androidx.compose.foundation.samples.ClickableSample
*
* @param interactionSource [MutableInteractionSource] that will be used to emit
* [PressInteraction.Press] when this clickable is pressed. Only the initial (first) press will be
* recorded and emitted with [MutableInteractionSource].
* @param indication indication to be shown when modified element is pressed. Be default,
* indication from [LocalIndication] will be used. Pass `null` to show no indication, or
* current value from [LocalIndication] to show theme default
* @param enabled Controls the enabled state. When `false`, [onClick], [onLongClick] or
* [onDoubleClick] won't be invoked
* @param onClickLabel semantic / accessibility label for the [onClick] action
* @param role the type of user interface element. Accessibility services might use this
* to describe the element or do customizations
* @param onLongClickLabel semantic / accessibility label for the [onLongClick] action
* @param onLongClick will be called when user long presses on the element
* @param onDoubleClick will be called when user double clicks on the element
* @param onClick will be called when user clicks on the element
*/
@ExperimentalFoundationApi
fun Modifier.combinedClickable(
interactionSource: MutableInteractionSource,
indication: Indication?,
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onLongClickLabel: String? = null,
onLongClick: (() -> Unit)? = null,
onDoubleClick: (() -> Unit)? = null,
onClick: () -> Unit
) = composed(
factory = {
val scope = rememberCoroutineScope()
val onClickState = rememberUpdatedState(onClick)
val interactionSourceState = rememberUpdatedState(interactionSource)
val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
val gesture = if (enabled) {
Modifier.pointerInput(onDoubleClick, onLongClick) {
detectTapGestures(
onDoubleTap = if (onDoubleClick != null) {
{ onDoubleClick() }
} else {
null
},
onLongPress = if (onLongClick != null) {
{ onLongClick() }
} else {
null
},
onPress = {
scope.launch {
// Remove any old interactions if we didn't fire stop / cancel properly
pressedInteraction.value?.let { oldValue ->
val interaction = PressInteraction.Cancel(oldValue)
interactionSourceState.value.emit(interaction)
pressedInteraction.value = null
}
val interaction = PressInteraction.Press(it)
interactionSourceState.value.emit(interaction)
pressedInteraction.value = interaction
}
tryAwaitRelease()
scope.launch {
pressedInteraction.value?.let { oldValue ->
val interaction = PressInteraction.Release(oldValue)
interactionSourceState.value.emit(interaction)
pressedInteraction.value = null
}
}
},
onTap = { onClickState.value.invoke() }
)
}
} else {
Modifier
}
DisposableEffect(interactionSource) {
onDispose {
scope.launch {
pressedInteraction.value?.let { oldValue ->
val interaction = PressInteraction.Cancel(oldValue)
interactionSourceState.value.emit(interaction)
pressedInteraction.value = null
}
}
}
}
Modifier
.genericClickableWithoutGesture(
gestureModifiers = gesture,
interactionSource = interactionSource,
indication = indication,
enabled = enabled,
onClickLabel = onClickLabel,
role = role,
onLongClickLabel = onLongClickLabel,
onLongClick = onLongClick,
onClick = onClick
)
},
inspectorInfo = debugInspectorInfo {
name = "combinedClickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
properties["onDoubleClick"] = onDoubleClick
properties["onLongClick"] = onLongClick
properties["onLongClickLabel"] = onLongClickLabel
properties["indication"] = indication
properties["interactionSource"] = interactionSource
}
)
@Composable
@Suppress("ComposableModifierFactory")
internal fun Modifier.genericClickableWithoutGesture(
gestureModifiers: Modifier,
interactionSource: MutableInteractionSource,
indication: Indication?,
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onLongClickLabel: String? = null,
onLongClick: (() -> Unit)? = null,
onClick: () -> Unit
): Modifier {
val semanticModifier = Modifier.semantics(mergeDescendants = true) {
if (role != null) {
this.role = role
}
// b/156468846: add long click semantics and double click if needed
onClick(action = { onClick(); true }, label = onClickLabel)
if (onLongClick != null) {
onLongClick(action = { onLongClick(); true }, label = onLongClickLabel)
}
if (!enabled) {
disabled()
}
}
return this
.then(semanticModifier)
.indication(interactionSource, indication)
.then(gestureModifiers)
}