2

I have a composable component like so:

Card(
    modifier = Modifier
        .fillMaxWidth()
        .then(modifier ?: Modifier),
    backgroundColor = colorResource(id = R.color.Red),
    shape = RoundedCornerShape(percent = 50),
) {
    Row (
        modifier = Modifier
            .size(160.dp)
    ) {
    }
}

When the user clicks and holds the Card I want to check if the user has held the Card for a second. If they held it longer than a second then I want to log "CLICKED" but if they let go before a second then don't log "CLICKED"

How can I achieve this?

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
KTOV
  • 559
  • 3
  • 14
  • 39

3 Answers3

4

This can be done by writing a custom gesture Modifier such as

fun Modifier.timedClick(
    timeInMillis: Long,
    interactionSource: MutableInteractionSource = remember {MutableInteractionSource()},
    onClick: (Boolean) -> Unit
) = composed {

    var timeOfTouch = -1L
    LaunchedEffect(key1 = timeInMillis, key2 = interactionSource) {
        interactionSource.interactions
            .onEach { interaction: Interaction ->
                when (interaction) {
                    is PressInteraction.Press -> {
                        timeOfTouch = System.currentTimeMillis()
                    }
                    is PressInteraction.Release -> {
                        val currentTime = System.currentTimeMillis()
                        onClick(currentTime - timeOfTouch > timeInMillis)
                    }
                    is PressInteraction.Cancel -> {
                        onClick(false)
                    }
                }

            }
            .launchIn(this)
    }

    Modifier.clickable(
        interactionSource = interactionSource,
        indication = rememberRipple(),
        onClick = {}
    )
}

Usage

val context = LocalContext.current

Card(
    shape = RoundedCornerShape(percent = 50),
) {
    Box(
        modifier = Modifier
            .timedClick(
                timeInMillis = 1000,
            ) { passed: Boolean ->
                Toast
                    .makeText(
                        context,
                        "Pressed longer than 1000 $passed",
                        Toast.LENGTH_SHORT
                    )
                    .show()
            }
            .fillMaxWidth()
            .height(100.dp),
        contentAlignment = Alignment.Center
    ) {

        Text("Hello World")
    }
}

Result

enter image description here

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Instead of using the InteractionSource, which only informs you of changes in the interaction state and not of when those changes happened, please write a custom PointerInputModifier Instead. That will give you access to the time when the pointer events happened. The problem with this solution is that it is impossible to write reliable tests, as it's querying the system time. – Jelle Fresen Aug 31 '22 at 14:13
  • @JelleFresen Thanks for suggestion and warning. I will add a pointerInput version. Which time do you suggest to use? – Thracian Aug 31 '22 at 14:28
  • That would be the uptimeMillis property in PointerInputChange – Jelle Fresen Sep 02 '22 at 15:48
  • I meant for click version. I thought you mean `Calendar` or `GregorianCalendar `. My initial answer was actually `Modifier.pointerInput()` as i answered here https://stackoverflow.com/a/70847531/5457853. But it doesn't come with ripple. You either should implement your own ripple or need to have a Modiifer.clickable{} to mimic ripple. – Thracian Sep 02 '22 at 16:05
0

You could look at using the long press from

https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/package-summary#(androidx.compose.ui.input.pointer.PointerInputScope).detectTapGestures(kotlin.Function1,kotlin.Function1,kotlin.coroutines.SuspendFunction2,kotlin.Function1)

Not sure if the long press triggers at the time you want. But you can look at how it is implemented and make your own.

Michael Krussel
  • 2,586
  • 1
  • 14
  • 16
0

You can use the InteractionSource.collectIsPressedAsState to know if the Card is pressed and a side effect to know when the Card is released.

Something like:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
var diffTime by remember { mutableStateOf(0L) }

if (isPressed){
    //Pressed
    val dateNow = Calendar.getInstance().timeInMillis
   
    DisposableEffect(Unit) {
        onDispose {
            //released
            val dateReleased = Calendar.getInstance().timeInMillis
            diffTime = dateReleased - dateNow
           
            //add your logic...
        }
    }
}

Card(
    onClick={},
    interactionSource = interactionSource,
){
    //..
}
Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841