I have a list of chips in Jetpack Compose, and I was wondering how it would be possible to detect long press events on them?
Code:
SuggestionChip(
onClick = {
onQuickPresetTapped()
},
label = {
Text(text = "${quickPreset.width}x${quickPreset.height}")
}
)
Currently I have only successfully detected simple click events, although I want the app to be able to detect long click event as well.
I have tried many Stack Overflow solutions, such as the following:
SuggestionChip(
label = {
Text(text = "${quickPreset.width}x${quickPreset.height}")
},
onClick = { },
modifier = Modifier.combinedClickable(
onClick = { onQuickPresetTapped() },
onLongClick = { onQuickPresetLongPressed() }
)
)
... which did not fix the problem.
Another thing I tried is wrapping the chip in a box with a long click and click event like so:
Box(
modifier = Modifier.combinedClickable(
onClick = { onQuickPresetTapped() },
onLongClick = { onQuickPresetLongPressed() }
)
) {
SuggestionChip(
onClick = {
onQuickPresetTapped()
},
label = {
Text(text = "${quickPreset.width}x${quickPreset.height}")
}
)
}
... this also did not solve the problem.
How can I get long press events to work on chips? I have tried many things without success.
Related
I'm currently getting an index out of bound which I can't figure out why...
Following situation:
Sometimes I update my compose bottom navigation to add an item or to change tint color etc. When the app is running and the update is happening it works. But when i close the app and it starts to initialize the navigation with the new state I get an index out of bound exception on the painterResource...
Would be nice if anyone has a hint for me. Another thing to add is i wire the compose currently to xml but i can't see a problem with that.
Thanks in advance
BottomNavigation(
backgroundColor = backgroundColor,
) {
navigation.navigationItems.forEach { navigationItem ->
val isSelected = selectedItem == navigationItem.destination
val iconResId =
if (isSelected) navigationItem.selectedIconRes else navigationItem.iconRes
val hasBadge = badges.contains(navigationItem.destination)
BottomNavigationItem(
icon = {
if (hasBadge) {
BadgedBox(
modifier = Modifier.background(badgeBackgroundColor),
badge = { Badge(backgroundColor = badgeColor) }
) {
Icon(
painterResource(iconResId),
contentDescription = null
)
}
} else {
Icon(
painterResource(iconResId),
contentDescription = null
)
}
},
selected = isSelected,
onClick = { onItemSelected(navigationItem.destination) },
alwaysShowLabel = true,
selectedContentColor = selectedTintColor,
unselectedContentColor = tintColor
)
}
}
Update:
I found now out why it happens but it’s still strange. The drawable itself produced somehow that error. After putting a different one there it worked all the time. The question is why did it not work after restarting the app but when running on the dynamic change… I have no idea…
Update 2:
After the dynamic change there are artifacts happening within the icons… For example 3rd icon has artifacts of 4th etc. What could cause that? Any idea?
As solved here, I disable the tap flashing by setting the indication to null.
However, this is not working for Button or Icons?!
In the Button you can't use the indication=null in the clickable modifier since it is defined internally by the component which uses indication = rememberRipple(). This creates and remembers a Ripple using values provided by RippleTheme.
You can provide a custom LocalRippleTheme to override the default behaviour.
Something like:
CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {
Button(
onClick = { /*...*/ },
) {
//...
}
}
with:
private object NoRippleTheme : RippleTheme {
#Composable
override fun defaultColor() = Color.Unspecified
#Composable
override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f,0.0f,0.0f,0.0f)
}
You can use
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* Called when the gesture starts */ },
onDoubleTap = { /* Called on Double Tap */ },
onLongPress = { /* Called on Long Press */ },
onTap = { /* Called on Tap */ }
)
}
instead of onClick().It' will not show the wave effect when click the button.
I'm trying to implement Grid Layout with 2 columns using Compose, but LazyVertical Grid does not work for me. I searched for some workarounds to fulfill the task, but nothing was rendered on a screen. Any ideas?
val state = rememberLazyListState()
LazyVerticalGrid(
cells = GridCells.Fixed(2),
state = state,
content = {
items(bookList.books){
bookList.books.map {
BookUI(book = it, onClick = {})
}
}
}
)
I tried using LazyVerticalGrid this way, but it does not render a list, while LazyColumn renders it
You don't need a map when using items.
Change
items(bookList.books){
bookList.books.map {
BookUI(book = it, onClick = {})
}
}
to
items(bookList.books){ book ->
BookUI(book = it, onClick = {})
}
Don't forget to import,
import androidx.compose.foundation.lazy.items
try to use the following code:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun MyGrid(items: List<String>) {
LazyVerticalGrid(
cells = GridCells.Fixed(count = 2)
) {
items(items) { text ->
Text(text = text)
}
}
}
Few things you should pay attention to:
the items(*) {} function need to be imported from androidx.compose.foundation.lazy.items
You added #OptIn(ExperimentalFoundationApi::class)
rememberLazyListState() is actually a default param so no need to add it.
For the above example you can use something like this:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun Content() {
MyGrid(
items = listOf(
"Item A",
"Item B",
"Item C",
"Item D",
"Item E",
"Item F"
)
)
}
And you will get this:
Implementation has changed since the time your question was created, so the following code snipped should work in your case:
val state = rememberLazyGridState()
LazyVerticalGrid(
state = state,
columns = GridCells.Fixed(2)
) {
items(bookList.books) { books ->
BookUI(book = it, onClick = {})
}
}
I have implemented a column of buttons in jetpack compose. We realized it is possible to click multiple items at once (with multiple fingers for example), and we would like to disable this feature.
Is there an out of the box way to disable multiple simultaneous clicks on children composables by using a parent column modifier?
Here is an example of the current state of my ui, notice there are two selected items and two unselected items.
Here is some code of how it is implemented (stripped down)
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(nestedScrollParams.childScrollState),
) {
viewDataList.forEachIndexed { index, viewData ->
Row(modifier = modifier.fillMaxWidth()
.height(dimensionResource(id = 48.dp)
.background(colorResource(id = R.color.large_button_background))
.clickable { onClick(viewData) },
verticalAlignment = Alignment.CenterVertically
) {
//Internal composables, etc
}
}
Check this solution. It has similar behavior to splitMotionEvents="false" flag. Use this extension with your Column modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import kotlinx.coroutines.coroutineScope
fun Modifier.disableSplitMotionEvents() =
pointerInput(Unit) {
coroutineScope {
var currentId: Long = -1L
awaitPointerEventScope {
while (true) {
awaitPointerEvent(PointerEventPass.Initial).changes.forEach { pointerInfo ->
when {
pointerInfo.pressed && currentId == -1L -> currentId = pointerInfo.id.value
pointerInfo.pressed.not() && currentId == pointerInfo.id.value -> currentId = -1
pointerInfo.id.value != currentId && currentId != -1L -> pointerInfo.consume()
else -> Unit
}
}
}
}
}
}
Here are four solutions:
Click Debounce (ViewModel)r
For this, you need to use a viewmodel. The viewmodel handles the click event. You should pass in some id (or data) that identifies the item being clicked. In your example, you could pass an id that you assign to each item (such as a button id):
// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect
class MyViewModel : ViewModel() {
val debounceState = MutableStateFlow<String?>(null)
init {
viewModelScope.launch {
debounceState
.debounce(300)
.collect { buttonId ->
if (buttonId != null) {
when (buttonId) {
ButtonIds.Support -> displaySupport()
ButtonIds.About -> displayAbout()
ButtonIds.TermsAndService -> displayTermsAndService()
ButtonIds.Privacy -> displayPrivacy()
}
}
}
}
}
fun onItemClick(buttonId: String) {
debounceState.value = buttonId
}
}
object ButtonIds {
const val Support = "support"
const val About = "about"
const val TermsAndService = "termsAndService"
const val Privacy = "privacy"
}
The debouncer ignores any clicks that come in within 500 milliseconds of the last one received. I've tested this and it works. You'll never be able to click more than one item at a time. Although you can touch two at a time and both will be highlighted, only the first one you touch will generate the click handler.
Click Debouncer (Modifier)
This is another take on the click debouncer but is designed to be used as a Modifier. This is probably the one you will want to use the most. Most apps will make the use of scrolling lists that let you tap on a list item. If you quickly tap on an item multiple times, the code in the clickable modifier will execute multiple times. This can be a nuisance. While users normally won't tap multiple times, I've seen even accidental double clicks trigger the clickable twice. Since you want to avoid this throughout your app on not just lists but buttons as well, you probably should use a custom modifier that lets you fix this issue without having to resort to the viewmodel approach shown above.
Create a custom modifier. I've named it onClick:
fun Modifier.onClick(
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 = {
App.debounceClicks {
onClick.invoke()
}
},
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
You'll notice that in the code above, I'm using App.debounceClicks. This of course doesn't exist in your app. You need to create this function somewhere in your app where it is globally accessible. This could be a singleton object. In my code, I use a class that inherits from Application, as this is what gets instantiated when the app starts:
class App : Application() {
override fun onCreate() {
super.onCreate()
}
companion object {
private val debounceState = MutableStateFlow { }
init {
GlobalScope.launch(Dispatchers.Main) {
// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect
debounceState
.debounce(300)
.collect { onClick ->
onClick.invoke()
}
}
}
fun debounceClicks(onClick: () -> Unit) {
debounceState.value = onClick
}
}
}
Don't forget to include the name of your class in your AndroidManifest:
<application
android:name=".App"
Now instead of using clickable, use onClick instead:
Text("Do Something", modifier = Modifier.onClick { })
Globally disable multi-touch
In your main activity, override dispatchTouchEvent:
class MainActivity : AppCompatActivity() {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return ev?.getPointerCount() == 1 && super.dispatchTouchEvent(ev)
}
}
This disables multi-touch globally. If your app has a Google Maps, you will want to add some code to to dispatchTouchEvent to make sure it remains enabled when the screen showing the map is visible. Users will use two fingers to zoom on a map and that requires multi-touch enabled.
State Managed Click Handler
Use a single click event handler that stores the state of which item is clicked. When the first item calls the click, it sets the state to indicate that the click handler is "in-use". If a second item attempts to call the click handler and "in-use" is set to true, it just returns without performing the handler's code. This is essentially the equivalent of a synchronous handler but instead of blocking, any further calls just get ignored.
The most simple approach that I found for this issue is to save the click state for each Item on the list, and update the state to 'true' if an item is clicked.
NOTE: Using this approach works properly only in a use-case where the list will be re-composed after the click handling; for example navigating to another Screen when the item click is performed.
Otherwise if you stay in the same Composable and try to click another item, the second click will be ignored and so on.
for example:
#Composable
fun MyList() {
// Save the click state in a MutableState
val isClicked = remember {
mutableStateOf(false)
}
LazyColumn {
items(10) {
ListItem(index = "$it", state = isClicked) {
// Handle the click
}
}
}
}
ListItem Composable:
#Composable
fun ListItem(
index: String,
state: MutableState<Boolean>,
onClick: () -> Unit
) {
Text(
text = "Item $index",
modifier = Modifier
.clickable {
// If the state is true, escape the function
if (state.value)
return#clickable
// else, call onClick block
onClick()
state.value = true
}
)
}
Trying to turn off multi-touch, or adding single click to the modifier, is not flexible enough. I borrowed the idea from #Johann‘s code. Instead of disabling at the app level, I can call it only when I need to disable it.
Here is an Alternative solution:
class ClickHelper private constructor() {
private val now: Long
get() = System.currentTimeMillis()
private var lastEventTimeMs: Long = 0
fun clickOnce(event: () -> Unit) {
if (now - lastEventTimeMs >= 300L) {
event.invoke()
}
lastEventTimeMs = now
}
companion object {
#Volatile
private var instance: ClickHelper? = null
fun getInstance() =
instance ?: synchronized(this) {
instance ?: ClickHelper().also { instance = it }
}
}
}
then you can use it anywhere you want:
Button(onClick = { ClickHelper.getInstance().clickOnce {
// Handle the click
} } ) { }
or:
Text(modifier = Modifier.clickable { ClickHelper.getInstance().clickOnce {
// Handle the click
} } ) { }
fun singleClick(onClick: () -> Unit): () -> Unit {
var latest: Long = 0
return {
val now = System.currentTimeMillis()
if (now - latest >= 300) {
onClick()
latest = now
}
}
}
Then you can use
Button(onClick = singleClick {
// TODO
})
Here is my solution.
It's based on https://stackoverflow.com/a/69914674/7011814
by I don't use GlobalScope (here is an explanation why) and I don't use MutableStateFlow as well (because its combination with GlobalScope may cause a potential memory leak).
Here is a head stone of the solution:
#OptIn(FlowPreview::class)
#Composable
fun <T>multipleEventsCutter(
content: #Composable (MultipleEventsCutterManager) -> T
) : T {
val debounceState = remember {
MutableSharedFlow<() -> Unit>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
}
val result = content(
object : MultipleEventsCutterManager {
override fun processEvent(event: () -> Unit) {
debounceState.tryEmit(event)
}
}
)
LaunchedEffect(true) {
debounceState
.debounce(CLICK_COLLAPSING_INTERVAL)
.collect { onClick ->
onClick.invoke()
}
}
return result
}
#OptIn(FlowPreview::class)
#Composable
fun MultipleEventsCutter(
content: #Composable (MultipleEventsCutterManager) -> Unit
) {
multipleEventsCutter(content)
}
The first function can be used as a wrapper around your code like this:
MultipleEventsCutter { multipleEventsCutterManager ->
Button(
onClick = { multipleClicksCutter.processEvent(onClick) },
...
) {
...
}
}
And you can use the second one to create your own modifier, like next one:
fun Modifier.clickableSingle(
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
}
) {
multipleEventsCutter { manager ->
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = { manager.processEvent { onClick() } },
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
}
Just add two lines in your styles. This will disable multitouch in whole application:
<style name="AppTheme" parent="...">
...
<item name="android:windowEnableSplitTouch">false</item>
<item name="android:splitMotionEvents">false</item>
</style>
Say that I have a button that expands when tapped the first time to reveal text, and performs an action on the second tap--thus acting as a sort of confirmation before performing the action.
#Compose
fun ExpandingConfirmButton(onConfirm: () -> Unit) {
// expanded state of the button
val (expanded, setExpanded) = remember { mutableStateOf(false) }
// animating weight. make the button larger when it is tapped once
val weight = animate(if (expanded) 10F else 2F)
Button(onClick = {
// only fire the action after two taps
if(expanded) {
onConfirm()
}
setExpanded(!expanded)
}) {
Icon(Icons.Dfault.Delete)
// only show the text if the button has already been clicked once,
// otherwise just show the icon
if(expanded) {
Text(text = "Delete", softWrap = false)
}
}
}
And I would use that button like so:
#Composable
fun PersonList(people: List<Person>) {
// some database service exposed via an ambient
val dataService = DataService.current
LazyColumnFor(items = people) {
Row() {
Text(text = it.firstName, modifier = Modifier.weight(10F))
// on the first tap, the button should take up half of the row
ExpandingConfirmButton(onConfirm = { dataService.deletePerson(it) })
}
}
}
This all seems pretty straight-forward. Indeed, before I had split the ExpandingConfirmButton into it's own component and instead had the Button() it wraps directly in my PersonList component, it worked flawlessly.
However, it seems that the Row doesn't quite know what to do with the button when the weight changes when it is inside it's own component. The text inside the button displays, but the size does not change. Does this have to do with the Row's RowScope not getting utilized by the Modifier on the Button component inside ExpandingConfirmButton? If so, how do I go about using it?
What I ended up doing is basically just using the .animateContentSize() modifier.
#Compose
fun ExpandingConfirmButton(onConfirm: () -> Unit) {
// expanded state of the button
val (expanded, setExpanded) = remember { mutableStateOf(false) }
Button(
modifier = Modifier.animateContentSize(), // do this
onClick = {
// only fire the action after two taps
if(expanded) {
onConfirm()
}
setExpanded(!expanded)
}) {
Icon(Icons.Default.Delete)
// only show the text if the button has already been clicked once,
// otherwise just show the icon
if(expanded) {
Text(text = "Delete", softWrap = false)
}
}
}
It really is just that easy. No messing with manual widths, weights, or anything like that.