Compose for Desktop LazyRow/LazyColumn not scrolling with mouse click - kotlin

For some reason LazyColumns do not scroll with a mouse click and move gesture. It only works with the mouse wheel so far. For LazyRows it is also not possible to scroll with the mouse wheel. It seems that lazy row is useless for Compose for desktop.
Are there any possiblities to enable a click and move gesture on LazyRow and LazyColum. And if not is it at least possible to enable to scroll through a LazyRow with the mouse wheel?
I used this minimal reproducible example to test the scrolling
#Composable
#Preview
fun App() {
var text by remember { mutableStateOf("Hello, World!") }
MaterialTheme {
LazyRow(modifier = Modifier.fillMaxSize()) {
repeat(100) {
item {
Text("Test Test Test Test $it ")
}
}
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}

This is the intended behavior.
All scrollable components (including LazyColumn) work (for now) only with mouse wheel scroll events on the desktop.The scrollable components should not respond to mouse drag/move events.
Here's a basic example of how you can add drag support to your components:
val scrollState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyRow(
state = scrollState,
modifier = Modifier
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
coroutineScope.launch {
scrollState.scrollBy(-delta)
}
},
)
) {
items(100) {
Text("Test Test Test Test $it")
}
}

Related

How to set Double back press Exit in Jetpack Compose?

There are some tutorials on YouTube about how to set double back press exit in XML android, but most of them are in JAVA and None of them are in Jetpack Compose.
So how can we set that Double back press in Jetpack Compose?
I mean that thing that ask us in a Toast to press back again if we are sure to Exit. Thanks for help
This sample shows Toast on first touch and waits for 2 seconds to touch again to exit app otherwise goes back to Idle state.
sealed class BackPress {
object Idle : BackPress()
object InitialTouch : BackPress()
}
#Composable
private fun BackPressSample() {
var showToast by remember { mutableStateOf(false) }
var backPressState by remember { mutableStateOf<BackPress>(BackPress.Idle) }
val context = LocalContext.current
if(showToast){
Toast.makeText(context, "Press again to exit", Toast.LENGTH_SHORT).show()
showToast= false
}
LaunchedEffect(key1 = backPressState) {
if (backPressState == BackPress.InitialTouch) {
delay(2000)
backPressState = BackPress.Idle
}
}
BackHandler(backPressState == BackPress.Idle) {
backPressState = BackPress.InitialTouch
showToast = true
}
}

Rapid clicks navigation problem with bottom sheet in Jetpack Compose

I have a button from which on click I navigate to bottom sheet, the problem come when I click this button multiple times very fast, I am using accompanist library for navigation, as you see below, here is my ModalBottomSheetLayout and Scaffold
val navController = rememberAnimatedNavController()
val bottomSheetNavigator = rememberFullScreenBottomSheetNavigator()
navController.navigatorProvider += bottomSheetNavigator
ModalBottomSheetLayout(
bottomSheetNavigator = bottomSheetNavigator
) {
Scaffold(
scaffoldState = rememberScaffoldState(snackbarHostState = snackState),
content = { paddingValues ->
AnimatedNavHost(
navController = navController,
startDestination = "bottomsheet",
) {
bottomSheet("bottomsheet") {
CancelOrderBottomSheetUi()
}
}
},
bottomBar = {
BuildBottomBar(navController)
}
)
}
And this is my bottom sheet navigator
fun rememberFullScreenBottomSheetNavigator(
animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
skipHalfExpanded: Boolean = true,
): BottomSheetNavigator {
val sheetState = rememberModalBottomSheetState(
ModalBottomSheetValue.Hidden,
animationSpec
)
if (skipHalfExpanded) {
LaunchedEffect(sheetState) {
snapshotFlow { sheetState.isAnimationRunning }
.collect {
with(sheetState) {
val isOpening =
currentValue == ModalBottomSheetValue.Hidden && targetValue == ModalBottomSheetValue.HalfExpanded
val isClosing =
currentValue == ModalBottomSheetValue.Expanded && targetValue == ModalBottomSheetValue.HalfExpanded
when {
isOpening -> animateTo(ModalBottomSheetValue.Expanded)
isClosing -> animateTo(ModalBottomSheetValue.Hidden)
}
}
}
}
}
return remember(sheetState) {
BottomSheetNavigator(sheetState = sheetState)
}
}
I have one screen where there is button, and onClick of this button I want to navigate from screen 1(where the button is clicked) to screen 2(where bottom sheet is) both of this screens have different viewModels.
This is where I click cancel button
onCancel = {
viewModel.cancelButton()
}
and in VM
fun cancelButton() {
viewModelScope.launch {
navigationDispatcher.navigateTo("bottomsheet")
}
}
So the problem is just when I spam button very fast the bottom sheet appears almost on full size and then it collapse again, if I do not spam it, it appears like normal bottom sheet and I cannot click the button on background again, this bug happens only if I spam it.
PS: I tried with button bouncer, with some delays with if cycle and so on, but noting of this worked, I think I am missing something essential for navigation itself.

LazyColumn that respects MotionEvent.ACTION_SCROLL

How to force compose' LazyColumn to act like traditional scrollable elements like RecyclerView or ListView?
Useful when want to scroll with mouse, e.g. with vysor.
The solution is to add filter to modifier
const val VERTICAL_SCROLL_MULTIPLIER = -4
// Helps with scrolling with mouse wheel (just like recycler view/list view without compose)
#ExperimentalComposeUiApi
#Composable
fun MyLazyColumn(
modifier: Modifier = Modifier,
state: LazyListState, // rememberLazyListState()
content: LazyListScope.() -> Unit
) {
LazyColumn(
state = state,
modifier = modifier
.pointerInteropFilter {
if (it.action == MotionEvent.ACTION_SCROLL) {
state.dispatchRawDelta(it.getAxisValue(MotionEvent.AXIS_VSCROLL) * VERTICAL_SCROLL_MULTIPLIER)
}
false
},
content = content
)
}

Preview a "screen" in android jetpack compose navigation with a PreviewParameter NavController

I discovering android Cetpack Compose (and Navigation) and try to display a preview of a view with a navController as parameter.
To achieve, I use the PreviewParameter and I have no error, but nothing displayed in the Preview window.
Anyone know how pass a fake NavController instance to a Composable?
class FakeNavController : PreviewParameterProvider<NavController> {
override val values: Sequence<NavController>
get() {}
}
#Preview
#Composable
fun Preview(
#PreviewParameter(FakeNavController::class) fakeNavController: NavController
) {
HomeView(fakeNavController)
}
You don't have to make it nullable and pass null to it.
You just need to pass this: rememberNavController()
#Preview
#Composable
fun Preview() {
HomeView(rememberNavController())
}
PreviewParameter is used to create multiple previews of the same view with different data. You're expected to return the needed values from values. In your example you return nothing that's why it doesn't work(it doesn't even build in my case).
That won't help you to mock the navigation controller, as you still need to create it somehow to return from values. I think it's impossible.
Instead you can pass a handler, in this case you don't need to mock it:
#Composable
fun HomeView(openOtherScreen: () -> Unit) {
}
// your Navigation view
composable(Destinations.Home) { from ->
HomeView(
openOtherScreen = {
navController.navigate(Destinations.Other)
},
)
}
Finally, I declare a nullable NavController and it works.
#Composable
fun HomeView(navController: NavController?) {
Surface {
Column(
modifier = Modifier
.padding(all = 4.dp)
) {
Text(
text = "Home View",
style = MaterialTheme.typography.body2
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { navController?.navigate("lineRoute") }) {
Text(text = "nav to Line view")
}
}
}
}
#Preview
#Composable
fun Preview (){
HomeView(null)
}

How to show the bottom sheet with transparent background in jetpack compose?

My app consists of the home screen and on this screen, there is a button when users click on it they navigate to the login bottom sheet.
I am going to use this login bottom sheet elsewhere in the app so I prefer to make it a separate screen and navigate from home to login.
It is desirable to show the home screen as the background for the login screen. I mean the login bottom sheet's main content should be empty and transparent in order to see the home screen as the background. But instead of the home screen for background, the white background shows up.
Here are my codes:
LoginScreen:
#Composable
fun LoginScreen(
loginViewModel: LoginViewModel = hiltViewModel()
) {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
LoginContent()
},
sheetPeekHeight = 400.dp,
sheetShape = RoundedCornerShape(topEnd = 52.dp, topStart = 52.dp),
backgroundColor = Color.Transparent
) {
Box(modifier = Modifier.fillMaxSize().background(color = Color.Transparent)) {
}
}
}
HomeScreen:
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(),
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.White)
) {
somecontent
...
...
...
Button(onClick = {
viewModel.navigate(
LoginDestination.route()
)
}) {
Text("Go to the login screen")
}
}
}
I use navigation like this:
fun interface NavigationDestination {
fun route(): String
val arguments: List<NamedNavArgument>
get() = emptyList()
val deepLinks: List<NavDeepLink>
get() = emptyList()
}
and then Login destination overrides it:
object LoginDestination : NavigationDestination {
override fun route(): String = "login"
}
and here is the implementation of the navigator:
#Singleton
internal class ClientNavigatorImpl #Inject constructor() : ClientNavigator {
private val navigationEvents = Channel<NavigatorEvent>()
override val destinations = navigationEvents.receiveAsFlow()
override fun navigateUp(): Boolean =
navigationEvents.trySend(NavigatorEvent.NavigateUp).isSuccess
override fun popBackStack(): Boolean =
navigationEvents.trySend(NavigatorEvent.PopBackStack).isSuccess
override fun navigate(route: String, builder: NavOptionsBuilder.() -> Unit): Boolean =
navigationEvents.trySend(NavigatorEvent.Directions(route, builder)).isSuccess
}
and the navigator event is:
sealed class NavigatorEvent {
object NavigateUp : NavigatorEvent()
object PopBackStack : NavigatorEvent()
class Directions(
val destination: String,
val builder: NavOptionsBuilder.() -> Unit
) : NavigatorEvent()
}
the way you are trying to show the LoginScreen won't work as you expected because when you navigate to LoginScreen it's like opening a new Screen, HomeScreen is then added to the backstack and not shown behind your LoginScreen. To make it work, try like this:
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(),
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.White)
) {
Button(onClick = {
//TODO: Add functionality
}) {
Text("Go to the login screen")
}
}
}
And change the LoginScreen parameters that you can give it a Composable:
#Composable
fun LoginScreen(
loginViewModel: LoginViewModel = hiltViewModel(),
screen: #Composable (() -> Unit)
) {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
//The Login Content needs to be here
*EDIT*
BackHandler(enabled = true) {
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
*EDIT*
},
sheetPeekHeight = 400.dp,
sheetShape = RoundedCornerShape(topEnd = 52.dp, topStart = 52.dp),
backgroundColor = Color.Transparent
) {
screen() //Adds the content which is shown on the Screen behind bottomsheet
}
}
And then use it somehow like this:
LoginScreen( /*YourLoginViewModel*/) {
HomeScreen(Modifier, /*YourHomeScreenModel*/){
}
}
Now your bottom sheet is shown all the time, to hide it you need to work with the BottomSheetState collapsed/expanded and the sheetPeekHeight = 400.dp, which you need to set to 0 that the sheet is hidden completely at first
In the end you need to implement that the BottomSheetState changes on the ButtonClick where you navigated to the Screen in your first attempt
Edit:
Also don't use backgroundColor. To change the bottomSheets Background you need to use sheetBackgroundColor = Color.Transparent
helpinghand You are right about handling the device back button. But now when the user presses the back button only the Login screen's sheet content collapse and the main content of the login screen which is the main content of the home screen remains. In order to it works I don't need the composable callback screen as a parameter for the login screen function and instead, I replace it with another callback like (callback: () -> Unit) and whenever want to get rid of login screen just invoke it in the login screen (for example when clicking outside the bottom sheet or collapsing it) and then in the home screen create a boolean mutable state for detecting when it needs to show the login screen and so in the trailing lambda make the state false.