Jetpack Compose detectTransformGestures consumes all gestures - android

I have taken a look at the following answer and it didn't work How to use detectTransformGestures but not consuming all pointer event
The problem: I have a LazyList that I need to scroll vertically, the children composables also are draggable horizontally and respond to tap and presses. But, I want the LazyList to also be pinched-in or pinched-out while still intercepting the drag gestures. Here's the current code of the modifier for the existing gestures:
/* LazyList child composable */
Modifier.pointerInput(myLock) {
detectDragGestures(
onDragEnd = {
if (fooBolean) {
stuffToDo()
},
onDrag = { change, dragAmount ->
draggingTheComposableLogic()
scope.launch {
/* Scrolling the lazylist using the state linked to it */
state.scroll {
scrollBy(-dragAmount.y)
}
}
}
)
}
.pointerInput(Unit) {
detectTapGesturesUnconsumed(
onTap = {
scope.launch {
val press = PressInteraction.Press(Offset.Zero)
clickInteraction.emit(press)
delay(150)
clickInteraction.emit(PressInteraction.Release(press))
}
}
)
}
My attempt: I tried modifying the source code for detectTransformGestures to remove the consume part, but it doesn't work at all. It worked for tap and drag, but not for transform gestures. If I add this piece of code to detect zoom-in and zoom-out (pinch gesture), then everything else will just stop working:
/* This surface is situated on top of the lazylist, i know it's overlaying it, but then if I add the pointerInput modifier to the lazylist, it also won't work, nothing works, it seems like this modifier keeps blocking until the pointers are lifted */
Surface(modifier = Modifier.fillMaxSize().alpha(0f)
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { c, p, z, r ->
Log.e("f", "$z")
}
)
}
) {}
Is there any way I can detect ONLY-ZOOM gesture without consuming it, so I can intercept the other gestures normally ?

Related

Jetpack compose - How to detect tap gestures without consuming them?

I want to detect tap gesture in LazyColumn and long press gesture in items:
LazyColumn(
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(onTap = { /* do something */})
}
) {
items(items) {
ListItem(
modifier=Modifier.pointerInput(Unit) {
detectTapGestures(onLongPress = { /* do something else */})
}
)
}
}
Obviously ListItem will consume all gestures.
So my question is how to detect tap gestures without consuming them?
you can use awaitRelease,i hope the problem is solved

An efficient way to check when a specific LazyColumn item comes into view?

I need to check when a certain LazyColumn item comes into view, and once it does, make a callback to onItemWithKeyViewed() only once to notify that this item has been viewed.
My attempt:
#Composable
fun SpecialList(
someItems: List<Things>,
onItemWithKeyViewed: () -> Unit
) {
val lazyListState = rememberLazyListState()
if (lazyListState.isScrollInProgress) {
val isItemWithKeyInView = lazyListState.layoutInfo
.visibleItemsInfo
.any { it.key == "specialKey" }
if (isItemWithKeyInView) {
onItemWithKeyViewed()
}
}
LazyColumn(
state = lazyListState
) {
items(items = someItems) { itemData ->
ComposableOfItem(itemData)
}
item(key = "specialKey") {
SomeOtherComposable()
}
}
}
Issue with my method is I notice the list scrolling performance degrades badly and loses frames. I realize this may be because it's checking all visible item keys on every frame?
Also, onItemWithKeyViewed() is currently being called multiple times instead of just the first time it's viewed.
Is there a more efficient way to make a single callback to onItemWithKeyViewed() only the first time "specialKey" item is viewed?
In such cases, when you have a state that is updated often, you should use derivedStateOf: this will cause recomposition only when the result of the calculation actually changes.
You should not call side effects (which is calling onItemWithKeyViewed) directly in the composable builder. You should use one of the special side-effect functions instead, usually LaunchedEffect - this ensures that the action is not repeated. You can find more information on this topic in Thinking in Compose and in side-effects documentation.
val isItemWithKeyInView by remember {
derivedStateOf {
lazyListState.isScrollInProgress &&
lazyListState.layoutInfo
.visibleItemsInfo
.any { it.key == "specialKey" }
}
}
if (isItemWithKeyInView) {
LaunchedEffect(Unit) {
onItemWithKeyViewed()
}
}

Trigger event upon hiding BottomSheetModalLayout

How can I trigger calling a function when the user clicks outside the ModalBottomSheetLayout, effectively hiding it?
One solution I've found is to implement a LaunchedEffect with the state, but this only changes after it has been hidden, whereas I would like it to happen immediately as the Modal is hiding.
val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
LaunchedEffect(bottomSheetState.isVisible) {
if (!bottomSheetState.isVisible) {
doWork()
}
}
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetContent = {
Text("Lorem Ipsum")
}
) {
Text("hello worled")
}
You can still use LaunchedEffect but listen to the value of the ModalBottomSheetState.currentValue
LaunchedEffect(modalBottomSheetState.currentValue) {
when (modalBottomSheetState.currentValue) {
ModalBottomSheetValue.Hidden -> {
// Do what you need with a hidden state
}
ModalBottomSheetValue.Expanded -> {
// Do what you need with Expanded state
}
ModalBottomSheetValue.HalfExpanded -> {
// Do what you need with a HalfExpanded state
}
}
You can trigger an action at half expanded so it doesn't wait until all the bottom sheet is hidden.
Note that his state is only enabled if the height of the bottom sheet is more than 50% of the screen height.

Android Jetpack Compose and Login screen

I'm facing an issue and I can't understand what is happening here.
I'm using FirebaseUI for authentication and I have this logic on my composable:
if (isAnonymousUser && authResultCode != AuthResultCode.CANCELLED) {
LaunchedEffect(true) {
loginLauncher.launch(viewModel.buildLoginIntent()) //This trigger the UI Sign in flow
}
}
if (!isAnonymousUser) {
LaunchedEffect(true) {
navController.popBackStack()
navController.navigate(Screen.HomeScreen.route)
}
}
The problem are these lines
if (!isAnonymousUser) {
LaunchedEffect(true) {
navController.popBackStack()
navController.navigate(Screen.HomeScreen.route)
}
}
If I remove the LaunchedEffect, I can move to HomeScreen but the navController.navigate is called in a loop and the application becomes unusable. This comes from the view model:
val isAnonymousUser = viewModel.isAnonymousUser.collectAsState().value
Instead using LaunchedEffect it doesn't happen. What is happening? Help me to understand, I think the problem is on the MutableState of isAnonymousUser

Listen ModalBottomSheetLayout state change in Jetpack Compose

Currently I am using ModalBottomSheetLayout to display the bottom sheet.
I don't know is there a way to listen to the bottom page closing event?
In Compose to listen for changes you need to check current values of your mutable states.
In case with ModalBottomSheetLayout you have ModalBottomSheetState, and you can check on currentValue of this state. If you need you can modify your views state depending on this value.
If you wanna perform some action on state changes, you need to use side effects. The most basic one is LaunchedEffect, you can use it in combination with snapshotFlow to watch any state value:
LaunchedEffect(Unit) {
snapshotFlow { modalBottomSheetState.currentValue }
.collect {
println(it.toString())
}
}
Also I'll be called first time when you launch a screen, and you'll have Hidden there too, so depending on your needs it may not be an ideal solution.
To get the most close to listening on becoming hidden, you can use DisposableEffect.
if (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
DisposableEffect(Unit) {
onDispose {
println("hidden")
}
}
}
Here I'm launching DisposableEffect when your sheet appears, and when it disappears - I'm removing it from the view hierarchy, which will cause the onDispose to be called.
Full example of basically everything you can do with the state:
val modalBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
LaunchedEffect(modalBottomSheetState.currentValue) {
println(modalBottomSheetState.currentValue)
}
if (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
DisposableEffect(Unit) {
onDispose {
println("hidden")
}
}
}
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
Text(
"sheetContent",
modifier = Modifier.fillMaxHeight()
)
}
) {
Column {
Text(modalBottomSheetState.currentValue.toString())
Button(onClick = {
scope.launch {
modalBottomSheetState.show()
}
}) {
Text("Show bottom sheet")
}
}
}
You can just use confirmStateChange from rememberModalBottomSheetState:
val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden, confirmStateChange = {
if (it == ModalBottomSheetValue.Hidden) {
println(it)
}
true
})
Using confirmStateChange won't be invoked however if you show/hide the modal yourself with a clickable for example - it seems to only be invoked on clicking the scrim or swiping.
A way around this is to just observe the modalBottomSheetState.targetValue which will change anytime the modal is animating between two different states:
LaunchedEffect(modalBottomSheetState.targetValue) {
if (modalBottomSheetState.targetValue == ModalBottomSheetValue.Hidden) {
// do something when animating to Hidden state
} else {
// expanding
}
}
This is closer to the timing of confirmStateChange which is invoked before the call to show/hide. Observing modalBottomSheetState.currentValue will change at the end of the animation, while modalBottomSheetState.targetValue will change before the animation begins.

Categories

Resources