Blog Infos
Author
Published
Topics
,
Published
Preamble
A Repeating Button
@Composable
fun RepeatingButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
) {
Button(
modifier = modifier,
onClick = {},
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
border = border,
colors = colors,
contentPadding = contentPadding,
content = content
)
}
@Composable
fun RepeatingButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
) {
var pressed by remember { mutableStateOf(false) }
Button(
modifier = modifier.pointerInteropFilter {
pressed = when (it.action) {
MotionEvent.ACTION_DOWN -> true
else -> false
}
true
},
onClick = {},
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
border = border,
colors = colors,
contentPadding = contentPadding,
content = content
)
}

The Repeating Behavior

Parameters:
Behavior:
@Composable
fun RepeatingButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
maxDelayMillis: Long = 1000,
minDelayMillis: Long = 5,
delayDecayFactor: Float = .15f,
content: @Composable RowScope.() -> Unit
) {
var pressed by remember { mutableStateOf(false) }
Button(
modifier = modifier.pointerInteropFilter {
pressed = when (it.action) {
MotionEvent.ACTION_DOWN -> true
else -> false
}
true
},
onClick = {},
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
border = border,
colors = colors,
contentPadding = contentPadding,
content = content
)
LaunchedEffect(pressed, enabled) {
var currentDelayMillis = maxDelayMillis
while (enabled && pressed) {
onClick()
delay(currentDelayMillis)
currentDelayMillis =
(currentDelayMillis - (currentDelayMillis * delayDecayFactor))
.toLong().coerceAtLeast(minDelayMillis)
}
}
}

Fast from start to finish

To get our delay-and-decay behavior to work, we can make use of rememberUpdatedState (took me a while to discover that one). Every time recomposition happens, we can take the new onClick and hand it to the compose framework. Our LaunchedEvent can then use the updated value in its while loop, effectively querying the compose framework for the value each time. Something like so:

@Composable
fun RepeatingButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
maxDelayMillis: Long = 1000,
minDelayMillis: Long = 5,
delayDecayFactor: Float = .15f,
content: @Composable RowScope.() -> Unit
) {
val currentClickListener by rememberUpdatedState(onClick)
var pressed by remember { mutableStateOf(false) }
Button(
modifier = modifier.pointerInteropFilter {
pressed = when (it.action) {
MotionEvent.ACTION_DOWN -> true
else -> false
}
true
},
onClick = {},
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
border = border,
colors = colors,
contentPadding = contentPadding,
content = content
)
LaunchedEffect(pressed, enabled) {
var currentDelayMillis = maxDelayMillis
while (enabled && pressed) {
currentClickListener()
delay(currentDelayMillis)
currentDelayMillis =
(currentDelayMillis - (currentDelayMillis * delayDecayFactor))
.toLong().coerceAtLeast(minDelayMillis)
}
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

At long last we have Kotlin first at Meta!

Android started to support Kotlin 5 years ago and became the first-choice language three years ago. But Meta just announced Kotlin as the preferred and default language for our Android code base only 3 months…
Watch Video

At long last we have Kotlin first at Meta!

Peng Jiang & Sergei Rybalkin
Software Engineer & Kotlin
Meta

At long last we have Kotlin first at Meta!

Peng Jiang & Serge ...
Software Engineer & ...
Meta

At long last we have Kotlin first at Meta!

Peng Jiang & Ser ...
Software Engineer & Kotli ...
Meta

Jobs

Notice that in our LaunchedEffect, we’re calling currentClickListener, which is simply val currentClickListener by rememberUpdatedState(onClick). With this, we should be good to go, right?

Slow start, fast finish

What about the Ripple?
@Composable
fun RepeatingButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
maxDelayMillis: Long = 1000,
minDelayMillis: Long = 5,
delayDecayFactor: Float = .15f,
content: @Composable RowScope.() -> Unit
) {
val currentClickListener by rememberUpdatedState(onClick)
Button(
modifier = modifier.pointerInput(interactionSource, enabled) {
forEachGesture {
coroutineScope {
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
val heldButtonJob = launch {
var currentDelayMillis = maxDelayMillis
while (enabled && down.pressed) {
currentClickListener()
delay(currentDelayMillis)
val nextDelayMillis =
currentDelayMillis - (currentDelayMillis * delayDecayFactor)
currentDelayMillis =
nextDelayMillis.toLong().coerceAtLeast(minDelayMillis)
}
}
waitForUpOrCancellation()
heldButtonJob.cancel()
}
}
}
},
onClick = {},
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
border = border,
colors = colors,
contentPadding = contentPadding,
content = content
)
}
@Composable
fun RepeatingButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
maxDelayMillis: Long = 1000,
minDelayMillis: Long = 5,
delayDecayFactor: Float = .20f,
content: @Composable RowScope.() -> Unit
) {
Button(
modifier = modifier.repeatingClickable(
interactionSource = interactionSource,
enabled = enabled,
maxDelayMillis = maxDelayMillis,
minDelayMillis = minDelayMillis,
decayFactor = delayDecayFactor
) { onClick() },
onClick = {},
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
border = border,
colors = colors,
contentPadding = contentPadding,
content = content
)
}
fun Modifier.repeatingClickable(
interactionSource: InteractionSource,
enabled: Boolean,
maxDelayMillis: Long = 1000,
minDelayMillis: Long = 5,
delayDecayFactor: Float = .20f,
onClick: () -> Unit
): Modifier = composed {
val currentClickListener by rememberUpdatedState(onClick)
pointerInput(interactionSource, enabled) {
forEachGesture {
coroutineScope {
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
val heldButtonJob = launch {
var currentDelayMillis = maxDelayMillis
while (enabled && down.pressed) {
currentClickListener()
delay(currentDelayMillis)
val nextMillis = currentDelayMillis - (currentDelayMillis * delayDecayFactor)
currentDelayMillis = nextMillis.toLong().coerceAtLeast(minDelayMillis)
}
}
waitForUpOrCancellation()
heldButtonJob.cancel()
}
}
}
}
}
Final Thoughts

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu