I am trying to add a scrollable behavior to the topBar parameter of Scaffold. In the documentation it says, that I can add the scrollable behavior by setting the CenterAlignedTopAppBar parameter scrollBehavior. However it doesn't seem to work.
The following answer from StackOverflow didn't help.
Any advice appreciated.
Here is how my code goes.
#Composable
fun Screen() {
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
rememberTopAppBarState(),
{ true },
spring(Spring.StiffnessMediumLow),
rememberSplineBasedDecay()
)
val scrollBehavior = remember { topAppBarScrollBehavior }
Scaffold(
topBar = {
ThemedTopBar(scrollBehavior, title = "Select Guests", onBack = {/* TODO */})
}
) { innerPadding ->
//MainContent(innerPadding)
}
}
#Composable
fun ThemedTopBar(scrollBehavior: TopAppBarScrollBehavior, title: String, onBack: () -> Unit) {
CenterAlignedTopAppBar(
modifier = Modifier
.height(51.dp)
.padding(vertical = 16.dp, horizontal = 9.dp)
,
title = {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = Typography.titleMedium
)
},
navigationIcon = {
IconButton(
modifier = Modifier.size(22.dp),
onClick = onBack,
) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_arrow_back),
contentDescription = "Back Button",
tint = ThemeBlue
)
}
},
scrollBehavior = scrollBehavior
)
}
You have to attach the TopAppBarScrollBehavior.nestedScrollConnection to a Modifier.nestedScroll in order to keep track of the scroll events.
Something like:
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
CenterAlignedTopAppBar(
title = { /* .. */ },
navigationIcon = { /* .. */ },
actions = { /* .. */ },
scrollBehavior = scrollBehavior
)
},
content = { /* .. */ }
)
Related
I' ve been making a dictionary app for a while and I added that users can create own dictionaries on my app. I show users dictionary on the screen and users can delete their dictionaries whatever they want. so I am trying to make alert dialog for this because I want users not to delete their dictionaries when they press the delete icon directly. An alert dialog will appear on the screen and there should be two buttons such as cancel and accept in that alert dialog. If the user presses accept, that is, if he wants to delete, I want the dictionary to be deleted.
However, the problem is that it is difficult to implement this in compose and in the codes I wrote because I encountered many problems for some reason, whereas it should have been easy. What I did in my codes is that if user clicks delete icon onDeleteClick works and showAlertDialog becomes true in onDeleteClick. When true, it goes inside the top if block and calls the alert dialog component. When the alert dialog compo is called, CustomDialogUI opens. I send two parameters to CustomDialogUI, one is a showAlertDialog mutablestate that controls the opening and closing of the alert dialog, and the second one is deleteDicState if the user says allow in the alert dialog that opens, deleteDicState becomes true and if deleteDicState is true, the deletion must occur.
Since deleteDicState is false the first time, it does not delete, but when the alert dialog opens for the second time and I say delete, it deletes it for some reason. How can i solve this problem help.
my code
#Composable
fun CreateYourOwnDictionaryScreen(
navController: NavController,
viewModel: CreateYourOwnDictionaryViewModel = hiltViewModel()
) {
val scaffoldState = rememberScaffoldState()
val state = viewModel.state.value
val scope = rememberCoroutineScope()
val context = LocalContext.current
val showAlertDialog = remember { mutableStateOf(false) }
val deleteDicState = remember { mutableStateOf(false) }
if(showAlertDialog.value){
Dialog(onDismissRequest = { showAlertDialog.value = false }) {
CustomDialogUI(openDialogCustom = showAlertDialog,deleteDicState)
}
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
backgroundColor = bar,
title = {
androidx.compose.material3.Text(
text = "your dictionaries",
modifier = Modifier.fillMaxWidth(),
color = Color.White,
fontSize = 22.sp
)
},
navigationIcon = {
IconButton(onClick = {
navController.navigate(Screen.MainScreen.route)
}) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Go Back"
)
}
}
)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
FloatingActionButton(
onClick = { navController.navigate(Screen.CreateDicScreen.route) },
backgroundColor = bar,
) {
Icon(Icons.Filled.Add, "fab")
}
}
) {
Box(modifier = Modifier.background(MaterialTheme.colors.background)) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(state.dictionaries) { dictionary ->
CreateYourOwnDictionaryItem(
dictionary = dictionary,
modifier = Modifier
.fillMaxWidth()
.clickable {
navController.navigate(Screen.MyWordsScreen.passDicId(dictionary.uid))
},
onAddClick = {
navController.navigate(
Screen.MakeYourDictionaryScreen.passDicId(
dictionary.uid
)
)
},
onDeleteClick = {
if(deleteDicState.value){
viewModel.onEvent(
CreateYourOwnDictionaryEvents.DeleteDictionary(dictionary)
)
scope.launch {
val result = scaffoldState.snackbarHostState.showSnackbar(
message = "dictionary is deleted",
/*actionLabel = "Undo",*/
duration = SnackbarDuration.Short
)
}
}
},
onEditClick = {
navController.navigate(
Screen.UpdateOwnDictionaryScreen.passDicIdAndDicName(
dictionary.uid,
dictionary.creationTime,
)
)
}
)
}
}
}
}
}
}
#Composable
fun CustomDialogUI(
openDialogCustom: MutableState<Boolean>,
deleteDicState : MutableState<Boolean>
) {
Card(
//shape = MaterialTheme.shapes.medium,
shape = RoundedCornerShape(10.dp),
// modifier = modifier.size(280.dp, 240.dp)
modifier = Modifier.padding(10.dp, 5.dp, 10.dp, 10.dp),
elevation = 8.dp
) {
Column(
modifier = Modifier
.background(Color.White)
) {
//.......................................................................
Image(
painter = painterResource(id = R.drawable.ic_baseline_warning),
contentDescription = null, // decorative
/*contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(
color = bar
),*/
modifier = Modifier
.padding(top = 35.dp)
.height(70.dp)
.fillMaxWidth(),
)
Column(modifier = Modifier.padding(16.dp)) {
androidx.compose.material3.Text(
text = "Warning !",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth(),
style = MaterialTheme.typography.body2,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
androidx.compose.material3.Text(
text = "Are you sure that your previously created dictionary will be deleted?",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 10.dp, start = 25.dp, end = 25.dp)
.fillMaxWidth(),
)
}
//.......................................................................
Row(
Modifier
.fillMaxWidth()
.padding(top = 10.dp)
.background(bar),
horizontalArrangement = Arrangement.SpaceAround
) {
TextButton(onClick = {
openDialogCustom.value = false
}) {
Text(
"Not Now",
fontWeight = FontWeight.Bold,
color = Color.Black,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
TextButton(onClick = {
openDialogCustom.value = false
deleteDicState.value = true
}) {
Text(
"Allow",
fontWeight = FontWeight.ExtraBold,
color = Color.Black,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
}
}
}
}
I cannot call the CustomDialogUI in onDeleteClick . If I call it, it gives the following error #Composable invocations can only happen from the context of a #Composable function.
for example like this
CreateYourOwnDictionaryScreen
onDeleteClick = {
Dialog(onDismissRequest = { showAlertDialog.value = false }) {
CustomDialogUI(openDialogCustom = showAlertDialog,deleteDicState)
}
....
I cannot call like this.
So I call it outside of onDeleteClick. or directly in CustomDialogUI if the user presses the delete button, I cannot delete it there because I can't access viewmodel and dictionary there
for example like this
CustomDialogUI
TextButton(onClick = {
openDialogCustom.value = false
viewModel.onEvent(
CreateYourOwnDictionaryEvents.DeleteDictionary(dictionary)
)
scope.launch {
val result = scaffoldState.snackbarHostState.showSnackbar(
message = "dictionary is deleted",
/*actionLabel = "Undo",*/
duration = SnackbarDuration.Short
)
}
}) {
Text(
"Allow",
fontWeight = FontWeight.ExtraBold,
color = Color.Black,
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp)
)
}
}
I cannot call like this.
Passing MutableStates as composable parameters is considered a bad practice, you should pass raw values and callbacks instead. In your case, you can implement it like this:
#Composable
fun CreateYourOwnDictionaryScreen() {
val showDeleteDialogForItem = remember { mutableStateOf<DictionaryItem?>(null) }
showDeleteDialogForItem.value?.let { itemToDelete ->
DeleteDialog(
onDeleteConfirm = {
viewModel.onEvent()
showDeleteDialogForItem.value = null
},
onCancel = { showDeleteDialogForItem.value = null },
)
}
...
items.forEach { item ->
CreateYourOwnDictionaryItem(
onDeleteClick = { showDeleteDialogForItem.value = item }
)
}
}
#Composable
fun DeleteDialog(
onDeleteConfirm: () -> Unit,
onCancel: () -> Unit,
) {
...
Button(onClick = onCancel) { Text("Cancel") }
Button(onClick = onDeleteConfirm) { Text("Delete") }
}
I've wrapped a Dialog in Compose, Android. However, things don't seem to show up. Not sure what I need to do here, to fix this properly for it to work naturally speaking. Because, I plan on using inputs and other stuff e.g., buttons etc.
#SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
#OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
#Composable
fun MyDialog(
openDialog: Boolean,
closeDialog: () -> Unit,
) {
if (openDialog) {
Dialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = closeDialog,
content = {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
SmallTopAppBar(
modifier = Modifier.padding(0.dp, 0.dp, 16.dp, 10.dp),
title = {
Text(
text = "Add new item",
style = MaterialTheme.typography.titleMedium,
)
},
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.background
),
navigationIcon = {
IconButton(onClick = {
closeDialog()
}) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null
)
}
},
actions = {
Text(
"Save",
fontWeight = FontWeight.SemiBold
)
},
)
},
){
Text("Hello world!") // <-- Does not show up
}
}
)
}
}
Produces:
The reason is that you are ignoring the innerPadding values which comes fro Scaffold . You should be using it as the padding for your outer composable as Modifier.padding(it).
Text("Hello world!", modifier = Modifier.padding(it))
Above code should work . for further use Wrap the content in a container in this case Column .
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.padding(it)
.verticalScroll(state = scrollState)
.fillMaxSize()
) {
Text("Hello world!")
}
to Show a dialog you do not have to pass the immutable state to Dialog composable i.e openDialog: Boolean . Here is better example how you should handle Dialog state ..
Imagine I have a TopAppBar (1) like
In Code similar to:
TopAppBar(
backGroundColor = Colors.black
) {
Row(modifier = Modifier) {
Icon( // 2
modifier = Modifier.size(24.dp),
id = R.drawable.ic_hamburger_menu,
onClick = {
openMenu()
}
)
Text( // 3
modifier = Modifier,
text = "Page Title"
)
Icon( // 4.1
modifier = Modifier.size(24.dp),
id = R.drawable.ic_share,
onClick = {
//..
}
)
Icon( // 4.2
modifier = Modifier.size(24.dp),
id = R.drawable.ic_magnifing_glass,
onClick = {
openTopAppBarWithSearchContent()
}
)
Icon( // 5
modifier = Modifier.size(24.dp),
id = R.drawable.ic_ellipsis,
onClick = {
//..
}
)
}
}
When cliking on the magnifier glass (4.2), I would like to replace the complete (1) content (Manu icon, Text, Share icon, glass icon, points icon) of the top app bar with an individual Composable; let's say a search/input field..
With other words: openTopAppBarWithSearchContent() should replace its parent TopAppBars content.
How can this be realized in a Jetpack Compose way?
You can use a mutableState to change your layout based on state value. When you change state value it will trigger a recomposition and based on current value recomposition will pick desired layout.
#Composable
fun MyTopAppBar(
backGroundColor: Color = MaterialTheme.colors.primary
) {
var showFirstMenu by remember { mutableStateOf(true) }
if (showFirstMenu) {
Row(
modifier = Modifier
.background(MaterialTheme.colors.primary)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
// 2
IconButton(onClick = { /*TODO*/ }) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = null, tint = Color.White)
}
Text( // 3
modifier = Modifier,
text = "Page Title",
color = Color.White
)
Spacer(modifier = Modifier.weight(1f))
// 4.1
IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.Filled.Share,
contentDescription = null,
tint = Color.White
)
}
// 4.2
IconButton(onClick = { showFirstMenu = !showFirstMenu }) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = null,
tint = Color.White
)
}
// 5
IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = null,
tint = Color.White
)
}
}
} else {
TopAppBar(
title = { Text("Another Page") },
navigationIcon = {
IconButton(onClick = {}) {
Icon(Icons.Default.Menu, "Menu")
}
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Refresh, contentDescription = null)
}
IconButton(
onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Call, contentDescription = null)
}
}
)
}
}
And you can TopAppbar instead of Row to create one.
I don't understand why the FloatingButton is going up with the BottomSheet.
I tried to change the sheetElevation which is higher than the elevation of the FloatingButton, but the issue remains. That's because the code inside BottomSheetScaffoldStack says to move the FloatingButton up along with the BottomSheet. Is there any way to avoid that?
Here is the code of the BottomSheetScaffold:
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
topBar = { TopBar(
areButtonShowed = true,
title = topBarTitle,
onBackPressed = { BendRouter.navigateTo(onBackDestination) }
) },
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
ExtendedFloatingButton(
text = context.getString(R.string.start),
onClick = {}, // TODO
modifier = Modifier
.fillMaxWidth()
.height(45.dp)
.padding(
start = 24.dp,
end = 24.dp
),
backgroundColor = PureWhite
)
Spacer(modifier = Modifier.height(150.dp))
},
sheetBackgroundColor = PureWhite,
sheetPeekHeight = 0.dp,
sheetElevation = 70.dp,
sheetShape = RoundedCornerShape(50.dp),
sheetContent = {
openStretchDetails?.let { stretch ->
BottomSheetView(stretch = stretch)
}
},
content = { RoutinePageView(viewModel) }
)
And ExtendedFloatingButton:
#Composable
fun ExtendedFloatingButton(
text: String,
#DrawableRes icon: Int? = null,
onClick: () -> Unit,
modifier: Modifier = Modifier,
elevation: Dp = 12.dp,
backgroundColor: Color
) {
ExtendedFloatingActionButton(
text = {
Text(
text = text.uppercase(),
color = Gray,
fontSize = 18.sp,
maxLines = 1,
fontWeight = FontWeight.Bold,
letterSpacing = .5.sp
)
},
onClick = onClick,
icon = {
icon?.let {
Icon(
painter = painterResource(id = it),
contentDescription = ""
)
}
},
modifier = modifier,
elevation = FloatingActionButtonDefaults.elevation(elevation),
backgroundColor = backgroundColor
)
}
The BottomSheetScaffold has a default behavior that you can see by clicking on it while pressing CTRL. Looking at BottomSheetScaffoldStack inside the BottomSheetScaffold.kt class you'll see how the FloatingButton is moved along with the BottomSheet.
To solve this problem I used ModalBottomSheetLayout instead of BottomSheetScaffold.
ModalBottomSheetLayout(
sheetState = bottomState,
sheetContent = {
Box(Modifier.defaultMinSize(minHeight = 1.dp)) {
openStretchDetails?.let { stretch ->
BottomSheetView(stretch = stretch)
coroutineScope.launch {
bottomState.animateTo(ModalBottomSheetValue.Expanded)
}
}
}
},
sheetBackgroundColor = PureWhite,
sheetElevation = 16.dp,
sheetShape = RoundedCornerShape(50.dp),
) {
Scaffold(
topBar = { TopBar(
areButtonShowed = true,
title = topBarTitle,
onBackPressed = { BendRouter.navigateTo(onBackDestination) }
) },
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
ExtendedFloatingButton(
text = context.getString(R.string.start),
onClick = {}, // TODO
modifier = Modifier
.fillMaxWidth()
.height(45.dp)
.padding(
start = 24.dp,
end = 24.dp
),
backgroundColor = PureWhite
)
Spacer(modifier = Modifier.height(150.dp))
},
content = { RoutinePageView(viewModel) }
)
}
Add this on the top:
val bottomState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
confirmStateChange = { it != ModalBottomSheetValue.HalfExpanded }
)
val coroutineScope = rememberCoroutineScope()
I'm making an app bar in jetpack compose but I'm having spacing issues between the navigation icon and the title.
This is my compose function:
#Composable
fun DetailsAppBar(coin: Coin, backAction: () -> Unit) {
TopAppBar(
navigationIcon = {
IconButton(onClick = { backAction() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null
)
}
},
title = { Text(text = "${coin.rank}. ${coin.name} (${coin.symbol})") }
)
}
This is my preview function:
#Composable
#Preview
fun DetailsAppBarPreview() {
val bitcoin = Coin(
id = "",
isActive = true,
name = "Bitcoin",
rank = 1,
symbol = "BTC"
)
DetailsAppBar(coin = bitcoin, backAction = {})
}
This is the visual preview of my compose function:
This is the space I want to reduce:
Entering the code of the TopAppBar compose function I can't see any parameters that allow me to do this.
You are right. With the variant of TopAppBar you are using, this is not possible. This is because the width of the NavigationIcon is set to the default (72.dp - 4.dp). You can check the implementation of TopAppBar and see that it uses the below:
private val AppBarHorizontalPadding = 4.dp
// Start inset for the title when there is a navigation icon provided
private val TitleIconModifier = Modifier.fillMaxHeight()
.width(72.dp - AppBarHorizontalPadding)
What you could do is to use the other variant of the TopAppBar that gives you much more control in placing the title and icon. It could be something like:
#Composable
fun Toolbar(
#StringRes title: Int,
onNavigationUp: (() -> Unit)? = null,
) {
TopAppBar(backgroundColor = MaterialTheme.colors.primary) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
) {
// Title
Text(...)
// Navigation Icon
if (onNavigationUp != null) {
Icon(
painter = painterResource(id = R.drawable.ic_back),
contentDescription = stringResource(
id = R.string.back
),
tint = MaterialTheme.colors.onPrimary,
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.clickable { onNavigationUp() }
.padding(16.dp)
...... ,
)
}
}
}
}
Actually it is possible to reduce space between icon and and title but it's a little bit tricky. Just add negative offset to modifier of text like that
#Composable
fun DetailsAppBar(coin: Coin, backAction: () -> Unit) {
TopAppBar(
navigationIcon = {
IconButton(onClick = { backAction() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null
)
}
},
title = {
Text(
text = "${coin.rank}. ${coin.name} (${coin.symbol})",
modifier = Modifier.offset(x = (-16).dp)
)
}
)
}