I was testing Compose and navigation and noticed strange behavior (nav ver: androidx.navigation:navigation-compose:2.4.0-alpha10)
In this example, is a screen to check if the app is up to date or not (boolean var in the code for simple test), and if yes, navigate to other screen.
The big issue I found here was that when automating the navigation, the second screen switches to the first one very quickly. But if the navigation is done with the click of a button, for example, nothing strange happens...
MainActivity file code:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Surface(color = MaterialTheme.colors.background) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "first_screen"
) {
composable(route = "first_screen") {
FirstScreen(navController)
}
composable(route = "second_screen") {
SecondScreen(navController)
}
}
}
}
}
}
}
FirstScreen file code:
#Composable
fun FirstScreen(navController: NavController) {
val isAppUpdated = true // switch case
Scaffold(
topBar = {
TopAppBar {
Text(text = "First Screen Bar")
}
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "First Screen",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h4
)
if (!isAppUpdated) OutdatedApp()
else {
// Two options to navigate: with compose button or automatically
// UpdatedApp {
// navigateToSecondScreen(navController)
// }
navigateToSecondScreen(navController)
}
}
}
}
#Composable
fun OutdatedApp() {
Text(
text = "Your app is out of date, consider updating it!",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body1
)
}
#Composable
fun UpdatedApp(onClick: () -> Unit) {
Button(
onClick = { onClick() }
) {
Text(
text = "Navigate to Second Screen",
style = MaterialTheme.typography.button
)
}
}
private fun navigateToSecondScreen(navController: NavController) {
navController.navigate(route = "second_screen") {
launchSingleTop
popUpTo(route = "first_screen") { inclusive = true }
}
}
SecondScreen file code:
#Composable
fun SecondScreen(navController: NavController) {
Scaffold(
topBar = {
TopAppBar {
Text(text = "Second Screen Bar")
}
}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Second Screen",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h4
)
}
}
}
Can anyone explain what happens in these cases?
And any solution to implement this logic?
EDIT
I did another test without Scaffold on FirstScreen and it looks like this problem didn't happen...
SOLUTION
Philip's answer
LaunchedEffect's documentation
Related
I want to do something like this:
Where there is a view that pops on top of the open keyboard.
I've tried to do this, and I have this so far:
However, when I put this view in a Box, as the second view, the first view is also pushed up, here's the code:
#OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
#ExperimentalComposeUiApi
fun Modifier.bringIntoViewAfterImeAnimation(): Modifier = composed {
var focusState by remember { mutableStateOf<FocusState?>(null) }
val relocationRequester = remember { BringIntoViewRequester() }
val isImeVisible = WindowInsets.isImeVisible
LaunchedEffect(
isImeVisible,
focusState,
relocationRequester
) {
if (isImeVisible && focusState?.isFocused == false) {
relocationRequester.bringIntoView()
}
relocationRequester.bringIntoView()
}
bringIntoViewRequester(relocationRequester).onFocusChanged { focusState = it }
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun SpaceCreator(navController: NavController) {
Column(
modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(10.dp)),
verticalArrangement = Arrangement.Bottom
) {
SpaceCreatorContainer()
}
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun SpaceCreatorContainer() {
Card(
modifier = Modifier
.bringIntoViewAfterImeAnimation()
.shadow(elevation = 10.dp, shape = RoundedCornerShape(10.dp), clip = true)
.background(color = colors.background)
) {
SpaceCreatorWrapper()
}
}
#Composable
fun SpaceCreatorWrapper() {
val localFocusManager = LocalFocusManager.current
Column(
modifier = Modifier.padding(15.dp).clip(RoundedCornerShape(10.dp))
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Top Text"
)
Text(text = "Content")
OutlinedTextField(
value = "ss",
onValueChange = { },
label = { Text("Email Address") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = { localFocusManager.clearFocus() }
)
)
OutlinedTextField(
value = "ss",
onValueChange = { },
label = { Text("Email Address") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { localFocusManager.clearFocus() }
)
)
Text(text = "Content")
}
}
That's the code for SpaceCreator(), and then I add it to a Box where it's supposed to float over another view, like so:
BoxWithConstraints(
propagateMinConstraints = true
) {
Box(
modifier = Modifier
.fillMaxSize()
) {
MainView()
SpaceCreator(navController = navController)
}
}
How do I only push up the second view in the Box when the keyboard is opened, and not the entire box?
Also,
Currently, I have a AnimationNavigation screen which is a Box(fillMaxSize), and the content of the keyboard modal stuck to the bottom. I also have a auto focus on the input, which is making the navigation a bit slow, unlike other apps I've seen.
Is there a smooth way to do this on low-mid-range devices like Samsung A23?
Thank you.
I'am new to jetpack compose and i really liked it. But ran into a problem : I want know if my view is swiped up or down so i created a LazyColumn with some item in it to be able to scroll something. It work fine but i would like to access the Gesture property to know if the view is scrolled down or up, here is my code :
LazyColumn{
items (100){
Text(
text = "Item $it",
fontSize = 24.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxSize()
.padding(vertical = 24.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
//change.consumeAllChanges()// i don't know if this does something, i tried to remove it
println("detectDragGestures")
val (x, y) = dragAmount
if(abs(x) < abs(y)){
if (y > 0)
println("drag down")
else
println("drag Up")
}
}
})
}
}
This work, i can detect if the view is scrolled down or up, the problem is when i tap on the item and scroll, i get the right print but the view isn't scrolled, i have to click between item to be able to scroll.
I don't really know how gesture work in jetpack compose but i would like to get the direction of the swipe without preventing my view to be scrolled.
I managed to detect scroll direction with using Column instead of LazyColumn.
I hope it can lead you.
#Composable
fun ScrollDetect() {
val scrollState = rememberScrollState()
var dragPosition by remember {
mutableStateOf(0)
}
LaunchedEffect(key1 = scrollState.value, block = {
if (scrollState.value > dragPosition)
Log.e("dragging", "up")
else
Log.e("dragging", "down")
dragPosition = scrollState.value
})
Column(
modifier = Modifier
.verticalScroll(scrollState)
)
{
repeat(100) {
Text(
text = "Item $it",
fontSize = 24.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxSize()
.padding(vertical = 24.dp)
)
}
}
}
Explanation of how gesture system in Jetpack Compose works in detail here and here
change.consumeAllChanges() is deprecated now, partial consumes are deprecated. change.consume() is the only consume function to be used. What consume does is it returns change.isConsumed true and because of that any drag, scroll, transform gesture stops progressing or receiving events. detectDragGestures and detectTransformGestures consume events by default so next
Modifier.pointerInput() doesn't get these events. What you commented doesn't mean anything since drag already consumes events.
Here it's source code.
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
do {
drag = awaitPointerSlopOrCancellation(
down.id,
down.type
) { change, over ->
change.consume()
overSlop = over
}
// ! EVERY Default movable GESTURE HAS THIS CHECK
} while (drag != null && !drag.isConsumed)
if (drag != null) {
onDragStart.invoke(drag.position)
onDrag(drag, overSlop)
if (
!drag(drag.id) {
onDrag(it, it.positionChange())
it.consume()
}
) {
onDragCancel()
} else {
onDragEnd()
}
}
}
}
}
What you can do is using nestedScroll on parent of LazyColumn as here
#Composable
private fun NestedScrollExample() {
var text by remember { mutableStateOf("") }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
text = "onPreScroll()\n" +
"available: $available\n" +
"source: $source\n\n"
return super.onPreScroll(available, source)
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
text += "onPostScroll()\n" +
"consumed: $consumed\n" +
"available: $available\n" +
"source: $source\n\n"
return super.onPostScroll(consumed, available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
text += "onPreFling()\n" +
" available: $available\n\n"
return super.onPreFling(available)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
text += "onPostFling()\n" +
"consumed: $consumed\n" +
"available: $available\n\n"
return super.onPostFling(consumed, available)
}
}
}
Column() {
Box(
Modifier
.weight(1f)
.nestedScroll(nestedScrollConnection)
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(100) {
Text(
text = "I'm item $it",
modifier = Modifier
.shadow(1.dp, RoundedCornerShape(5.dp))
.fillMaxWidth()
.background(Color.LightGray)
.padding(12.dp),
fontSize = 16.sp,
color = Color.White
)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = text,
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.height(250.dp)
.padding(10.dp)
.background(BlueGrey400),
fontSize = 16.sp,
color = Color.White
)
}
}
It will return if you scroll up or down on your LazyColumn, if you want to do this only when your items are being scrolled you can do it as
Column(modifier = Modifier.fillMaxSize()) {
var text by remember { mutableStateOf("Drag to see effects") }
Text(text)
LazyColumn {
items(100) {
Text(
text = "Item $it",
fontSize = 24.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.border(3.dp, Color.Green)
.fillMaxSize()
.padding(vertical = 24.dp)
.pointerMotionEvents(Unit,
onDown = {
it.consume()
},
onMove = { change ->
val position = change.positionChange()
val dragText = if (position.y < 0) {
"drag up"
} else if (position.y > 0) {
"drag down"
} else {
"idle"
}
text = "position: ${change.position}\n" +
"positionChange: ${change.positionChange()}\n" +
"dragText: $dragText"
}
)
)
}
}
}
Modifier.pointerMotionEvents() is a gesture Modifier i wrote, it's available here.
I am creating custom Snackbar using Jetpack Compose. But I have also implemented BottomNavBar using Scaffold. Everything works fine but only one problem is snackbar goes under the BottomNavBar. So how to move above to the snackbar over BottomNavBar?
Snackbar code:
#Composable
fun DefaultSnackbar(
snackbarHostState: SnackbarHostState,
onDismiss: () -> Unit?,
modifier: Modifier = Modifier
) {
SnackbarHost(
hostState = snackbarHostState,
modifier = modifier,
snackbar = {data ->
Snackbar(
modifier = Modifier.padding(8.dp),
content = {
Text(
text = data.message,
style = MaterialTheme.typography.body2,
color = White
)
},
action = {
data.actionLabel?.let { actionLabel ->
TextButton(
onClick = {
onDismiss()
}
) {
Text(
text = actionLabel,
style = MaterialTheme.typography.body2,
color = White
)
}
}
}
)
}
)
}
Bottom Navigation Code :-
#Composable
fun BottomNav()
{
BottomNavigation(
elevation = 15.dp,
modifier = Modifier.zIndex(5f)
) {
BottomNavigationItem(
icon = {Icon(imageVector = Icons.Filled.Search, contentDescription = "search icon", tint = Color.White)},
selected = false,
onClick = { /*TODO*/ }
)
BottomNavigationItem(
icon = {Icon(imageVector = Icons.Filled.Home, contentDescription = "search icon", tint = Color.White)},
selected = true,
onClick = { /*TODO*/ }
)
BottomNavigationItem(
icon = {Icon(imageVector = Icons.Filled.MoreVert, contentDescription = "search icon", tint = Color.White)},
selected = false,
onClick = { /*TODO*/ }
)
}
}
Calling snackbar :
#ExperimentalCoroutinesApi
#AndroidEntryPoint
class RecipeListFragment : Fragment() {
private val viewModel: RecipeListViewModel by viewModels()
private val snackbarController = SnackbarController(lifecycleScope)
#ExperimentalComposeUiApi
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
AppTheme(darkTheme = viewModel.isDarkTheme.value) {
val recipes = viewModel.recipes.value
val query = viewModel.query.value
val selectedCategory = viewModel.selectedCategory.value
val scaffoldState = rememberScaffoldState()
Scaffold(
topBar = {
SearchBar(
query = query,
onQueryChange = viewModel::onQueryChange,
selectedCategory = selectedCategory,
onSelectedCategoryChanged = viewModel::onSelectedCategoryChanged,
onToggleTheme = viewModel::onToggleTheme,
searchRecipe = {
if (viewModel.selectedCategory.value?.value == "Milk") {
snackbarController.getScope().launch {
snackbarController.showSnackbar(
scaffoldSate= scaffoldState,
message = "Milk is in Snack",
actionLabel = "Hide",
)
}
} else {
viewModel.searchRecipe()
}
}
)
},
bottomBar = { BottomNav() },
scaffoldState = scaffoldState,
snackbarHost = {scaffoldState.snackbarHostState},
) {
Box(modifier = Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize())
{
...
others items
...
CircularIndeterminateProgressBar(display = viewModel.isLoading.value)
DefaultSnackbar(
snackbarHostState = scaffoldState.snackbarHostState,
onDismiss = { scaffoldState.snackbarHostState.currentSnackbarData?.dismiss()},
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}
}
}
}
}
Is it possible to center DropdownMenu in my example? or show it wherever I click or tap?
I tried alignment and arrangements and none of them work. I prefer showing the DropdownMenu wherever I tab but I couldn't find a way to do it.
fun main() = Window {
var helloText by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Item("Darian", "Russ") {
helloText = it
}
Item("Maynerd", "Andre") {
helloText = it
}
Item("Sandra", "Victoria") {
helloText = it
}
Spacer(modifier = Modifier.height(2.dp))
Text(text = helloText)
}
}
#Composable
fun Item(text: String, text2: String, onMenuTab: (String) -> Unit) {
var expanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val modifier = Modifier.clickable {
expanded = true
}
DropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
DropdownMenuItem(onClick = {
onMenuTab("hello $text $text2")
expanded = false
}, modifier = Modifier.align(Alignment.CenterHorizontally)) {
Text("Hello")
}
DropdownMenuItem(onClick = { /* Handle settings! */ }) {
Text("Settings")
}
Divider()
DropdownMenuItem(onClick = { /* Handle send feedback! */ }) {
Text("Send Feedback")
}
}
Text(text = text, modifier = modifier)
Text(text = text2, modifier = modifier)
Divider(modifier = Modifier.height(2.dp))
Spacer(modifier = Modifier.height(2.dp))
}
}
For me fixing the alignment of the popup did the trick-
Popup(alignment = Alignment.TopStart)
Earlier it was Popup(alignment = Alignment.CenterStart)
and that was taking my popup view to the top of the screen. But now comes below the clicked item.
I am working on this shopping app which uses radio buttons. The customer should be able to select a radio button and go it by clicking the 'select button'.
Can anyone help me get get started.
the code is below.
I hope this makes sense to everyone. I have made a page for each of the radio buttons. pages are name the same as the buttons.
#Composable
fun MyRadioGroup() {
val navController = rememberNavController()
val radioOptions = listOf(
"Dollar Store", "Fred Myers",
"Wall Mart", "SafeWay", "Sherm's", "CostCo", "Other"
)
val (selectedOption, onOptionSelected) = remember {
mutableStateOf(radioOptions[1])
}
Scaffold() {
TopAppBar(
title = { Text(stringResource(id = R.string.app_name)) },
)
}
Column(
modifier = Modifier
.padding(start = 16.dp, top = 160.dp)
) {
Text(
text = "Select Store from List Below")
Divider()
Text(
text = "Then push the 'Select'")
}
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
radioOptions.forEach { text ->
Row(
Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.selectable(
selected = (text == selectedOption),
onClick = { onOptionSelected(text) }
)
.padding(horizontal = 16.dp)
) {
RadioButton(
selected = (text == selectedOption),
modifier = Modifier
.padding(start = 16.dp, top = 12.dp),
onClick = {
onOptionSelected(text)
}
)
Text(
text = text,
modifier = Modifier.padding(start = 16.dp, top = 16.dp)
)
}
}
}
Column(modifier = Modifier
.padding(start = 24.dp, top = 640.dp)
) {
Button(onClick = {
TODO()
}) {
Text(text = "Select")
}
}
}
The 'select' button is near the bottom of the code.
First create a sealed class Screen to link radio buttons text to composable as recommended by Google like this:
sealed class Screen(val route: String, val label: String){
object MyRadioGroup: Screen(route = "myRadioGroup", label = "APP NAME")
object DollarStore: Screen(route = "Dollar Store", label = "Dollar Store")
object FreedMyers: Screen(route = "Fred Myers", label = "FreedMyers")}
then modify NavHost or AnimatedNavHost(if you are using accompanist's Navigatio Animatation library as:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
Surface(color = MaterialTheme.colors.background) {
NavHost(navController = navController, startDestination = Screen.MyRadioGroup.route){
composable(Screen.MyRadioGroup.route){
MyRadioGroup(navController = navController)
}
composable(Screen.DollarStore.route){
//Call here the associated composable function
DollarStore()
}
composable(Screen.FreedMyers.route){
//Call here the associated composable function
FreedMyers()
}
}
}
}
}}
Finally add some code to MyRadioGroup Composable
fun MyRadioGroup(navController: NavController) {
val radioOptions = listOf(
"Dollar Store", "Fred Myers",
"Wall Mart", "SafeWay", "Sherm's", "CostCo", "Other"
)
val (selectedOption, onOptionSelected) = remember {
mutableStateOf(radioOptions[1])
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.app_name)) },
)
}
) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Column(
modifier = Modifier.padding(start = 16.dp, top = 16.dp)
) {
Text(text = "Select Store from List Below")
Divider()
Text(text = "Then push the 'Select'")
}
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
radioOptions.forEach { text ->
Row(
Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.selectable(
selected = (text == selectedOption),
onClick = { onOptionSelected(text) }
)
.padding(horizontal = 16.dp)
) {
RadioButton(
selected = (text == selectedOption),
modifier = Modifier
.padding(start = 16.dp, top = 12.dp),
onClick = { onOptionSelected(text) }
)
Text(
text = text,
modifier = Modifier.padding(start = 16.dp, top = 16.dp)
)
}
}
}
Column(
modifier = Modifier
.padding(start = 24.dp, top = 24.dp)
) {
Button(
onClick = {
navController.navigate(selectedOption) //navigate to selected route
}
) {
Text(text = "Select")
}
}
}
}}