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 ..
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 want my code to remove elements from list of text fields properly.
Each element has an X button to remove it's text field.
If I start removing elements from the bottom it works but it doesn't work for removing random elements
I want to use forEachIndexed for displaing the list
Please help me with solving this problem. I've been trying to do this for some time but every trial is unsuccessful.
This is a piece of code that I've managed to write but removing elements doesn't work properly
val listOfWords = mutableStateListOf<String>()
#Composable
fun Main() {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Words",
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 4.dp),
style = MaterialTheme.typography.h6
)
listOfWords.forEachIndexed { index, word ->
Input(word, 30, "Word", 1,
{newWord ->
listOfWords[index] = newWord
Log.d("text ",word)
},
{
listOfWords.removeAt(index)
}
)
}
IconButton(
onClick = {
listOfWords.add("")
}
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Add"
)
}
}
}
#Composable
fun Input(
word: String,
maxChar: Int,
label: String,
maxLines: Int,
onEdit: (word: String) -> (Unit),
onRemove: () -> (Unit)
) {
var text by remember { mutableStateOf(word) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp, 0.dp, 8.dp, 0.dp)
) {
OutlinedTextField(
value = text,
onValueChange = {
if (it.length <= maxChar) text = it
onEdit(text)
},
modifier = Modifier.fillMaxWidth(),
label = { Text(label) },
leadingIcon = {
Icon(Icons.Default.Edit, null)
},
trailingIcon = {
IconButton(onClick = {
onRemove()
}) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "Back"
)
}
},
maxLines = maxLines
)
Text(
text = "${text.length} / $maxChar",
textAlign = TextAlign.End,
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(end = 16.dp)
)
}
}
The problem is here.
var text by remember { mutableStateOf(word) }
Without supplying a key to Input's remember, compose will not be able refresh/update your remaining Input's states during Main's re-composition every time an Input is removed.
You can use the word parameter as key for remember to re-calculate every composition pass (i.e when you add/remove or typed a value in the TextField), and your code should probably work as you expected.
var text by remember(word) { mutableStateOf(word) }
Have you tried doing the following instead?
listOfWords.forEachIndexed { index, word ->
... // rest of code
{
listOfWords.removeAt(index)
}
With Jetpack Compose, I'm trying to implement a function to switch between items with flick actions, like the ViewPager in Android JetPack.
At first I tried to get it to work using a dialog as shown in the code below. However, I found that the dialog cannot be moved horizontally with flick actions.
#OptIn(ExperimentalPagerApi::class)
#Composable
fun ContentDisplayWindow(
openDialog: MutableState<Boolean>,
contentList: List<Content>,
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
IconButton(
modifier = Modifier.align(Alignment.End),
onClick = { openDialog.value = false }
) {
Icon(
painter = painterResource(R.drawable.ic_arrow_back),
contentDescription = ""
)
}
HorizontalPager(
count = contentList.size
) { index ->
Dialog(
onDismissRequest = { openDialog.value = false },
properties = DialogProperties(dismissOnClickOutside = false)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.border(2.dp, Color.Red)
.background(color = MaterialTheme.colors.surface)
) {
Text(
text = contentList[index].title,
modifier = Modifier.padding(
top = 10.dp,
bottom = 10.dp,
start = 20.dp
)
)
Spacer(modifier = Modifier.padding(vertical = 10.dp))
Text(
text = contentList[index].description,
modifier = Modifier.padding(
top = 10.dp,
bottom = 10.dp,
start = 20.dp
)
)
}
}
}
}
}
implementaton:
implementation "com.google.accompanist:accompanist-pager:0.25.1"
So I tried to find a way to perform the process of dimming the screen and disabling other buttons or input fields such as a dialog, but was not able to find it. Is there any other way to make it work like a dialog?
Using compose, I want to create something like this :
Problem is using compose AlertDialog I only achieve to get this :
There is a padding between the AlertDialog border and the title that can not be removed. How to remove it using Compose ?
Using Modifier.padding(all = 0.dp) doesn't work. I tried it at every level.
Properties "title" and "text" are wrapped with AlertDialogBaselineLayout that is adding padding. You can't remove it.
I have made a custom Composable that is acting as an AlertDialog and doesn't use AlertDialogBaselineLayout.
You can copy/paste it and edit it according to your needs :
NoPaddingAlertDialog :
#Composable
fun NoPaddingAlertDialog(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: #Composable (() -> Unit)? = null,
text: #Composable (() -> Unit)? = null,
confirmButton: #Composable () -> Unit,
dismissButton: #Composable (() -> Unit)? = null,
shape: Shape = MaterialTheme.shapes.medium,
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(backgroundColor),
properties: DialogProperties = DialogProperties()
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = properties
) {
Surface(
modifier = modifier,
shape = shape,
color = backgroundColor,
contentColor = contentColor
) {
Column(
modifier = Modifier
.fillMaxWidth()
) {
title?.let {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
val textStyle = MaterialTheme.typography.subtitle1
ProvideTextStyle(textStyle, it)
}
}
text?.let {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
val textStyle = MaterialTheme.typography.subtitle1
ProvideTextStyle(textStyle, it)
}
}
Box(
Modifier
.fillMaxWidth()
.padding(all = 8.dp)
) {
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
dismissButton?.invoke()
confirmButton()
}
}
}
}
}
}
You can then easily use it this way :
#Composable
fun MyCustomAlertDialog(openDialog: MutableState<Boolean> = mutableStateOf(true)) {
if (openDialog.value) {
NoPaddingAlertDialog(
title = {
Surface(
color = Color.Blue,
contentColor = Color.White,
modifier = Modifier
.fillMaxWidth()
) {
Text(
text = " Popup Titleď¸",
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 16.dp),
)
}
},
text = {
Column(Modifier.fillMaxWidth()) {
OutlinedTextField(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = 8.dp, vertical = 16.dp)
.fillMaxWidth(),
value = "Alert Dialog content ...",
onValueChange = { },
label = { Text(text = "Content") },
)
}
},
onDismissRequest = { /*TODO*/ },
confirmButton = {
PopupButton(title = "Ok", setShow = {
openDialog.value = false
})
},
dismissButton = {
PopupButton(
title = "Cancel",
setShow = {
openDialog.value = false
}
)
}
)
}
}
And get this :
You can use a Dialog() instead of AlertDialog(). It allows you to pass in a composable as the content parameter, so you can set whatever padding you want. Just don't forget to set the background color in the root composable of the content, since it is transparent by default.
TopAppBar(
backgroundColor = Color.Transparent,
elevation = 0.dp,
modifier= Modifier.fillMaxWidth(),
navigationIcon = {
IconButton(
onClick = { TODO },
enabled = true,
) {
Icon(
painter = painterResource(id = R.drawable.icon_back_arrow),
contentDescription = "Back",
)
}
}
},
title = {
Text(
modifier = if (action == null) Modifier.fillMaxWidth() else Modifier,
textAlign = if (action == null) TextAlign.Center else TextAlign.Start,
maxLines = 1,
text = "Hello"
)
},
actions = {
action?.run {
Text(
modifier = Modifier
.padding(horizontal = 16.dp)
.clickable(onClick = TODO),
color = Color.Green,
text ="Cancel",
)
}
}
I'm new in jetpack and want to align title of TopAppBar at center if action is null. Title is not align center of layout. when there is no navigationIcon it work but adding navigationIcon it show slightly right. How can I do it to make title text at center of layout.
With Material2 you have to use the other constructor of TopAppBar that has no pre-defined slots for content, allowing you to customize the layout of content inside.
You can do something like:
val appBarHorizontalPadding = 4.dp
val titleIconModifier = Modifier.fillMaxHeight()
.width(72.dp - appBarHorizontalPadding)
TopAppBar(
backgroundColor = Color.Transparent,
elevation = 0.dp,
modifier= Modifier.fillMaxWidth()) {
//TopAppBar Content
Box(Modifier.height(32.dp)) {
//Navigation Icon
Row(titleIconModifier, verticalAlignment = Alignment.CenterVertically) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high,
) {
IconButton(
onClick = { },
enabled = true,
) {
Icon(
painter = painterResource(id = R.drawable.ic_add_24px),
contentDescription = "Back",
)
}
}
}
//Title
Row(Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically) {
ProvideTextStyle(value = MaterialTheme.typography.h6) {
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.high,
){
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
maxLines = 1,
text = "Hello"
)
}
}
}
//actions
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Row(
Modifier.fillMaxHeight(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
content = actions
)
}
}
}
With Material3 you can simply use the CenterAlignedTopAppBar:
CenterAlignedTopAppBar(
title = { Text("Centered TopAppBar") },
navigationIcon = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "Localized description"
)
}
}
)
If you're using Material3, you can use the CenterAlignedTopAppBar as well.
fun CenterAlignedTopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable () -> Unit = {},
actions: #Composable RowScope.() -> Unit = {},
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null
) {
SingleRowTopAppBar(
modifier = modifier,
title = title,
titleTextStyle =
MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont),
centeredTitle = true,
navigationIcon = navigationIcon,
actions = actions,
colors = colors,
scrollBehavior = scrollBehavior
)
}
EDIT: the old answer is out of date, please use CenterAlignedTopAppBar
CenterAlignedTopAppBar(
title = { Text(text = stringResource(id = titleRes)) },
actions = {
IconButton(onClick = onActionClick) {
Icon(
imageVector = actionIcon,
contentDescription = actionIconContentDescription,
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = colors,
modifier = modifier,
)
OLD ANSWER:
I redid the native implementation a bit.
Need to do just two things:
1.Add this file to your project. This is a slightly modified implementation of the TopAppBar class.
https://gist.github.com/evansgelist/aadcd633e9b160f9f634c16e99ffe163
2.Replace in your code TopAppBar to CenterTopAppBar. And it's all!
Scaffold(
topBar = {
CenterTopAppBar(
title = {
Text(
textAlign = TextAlign.Center,
text = text,
)
},
EDIT
extension's code
val Number.toPx
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
Resources.getSystem().displayMetrics
)
Result
The core of the title centering is that the space of the left and right occupying slots is the same. You only need to adjust the default size of the slots. We give the left and right slots the default space occupying slots, which can solve this problem well, and the code is simple.
#Composable
fun TopBar(title: Int, actions: #Composable (() -> Unit)? = null, popOnClick: () -> Unit) {
val modifier = Modifier.size(width = 70.dp, height = 50.dp).background(Color.Red)
TopAppBar(
title = {
Text(text = stringResource(id = title), fontSize = 16.sp,
textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) },
navigationIcon = {
Box(modifier = modifier, contentAlignment = Alignment.Center) {
IconButton(onClick = { popOnClick() }) {
Icon(Icons.Filled.ArrowBack, contentDescription = "", tint = MaterialTheme.colors.primary)
}
}
},
actions = {
Box(modifier = modifier, contentAlignment = Alignment.Center) {
if (actions != null) {
actions()
}
}
},
backgroundColor = MaterialTheme.colors.surface,
elevation = 1.dp
)
}
It depends what's in the appbar.
If you only have the title, then you could do the following:
topBar = {
TopAppBar(content = {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Title Text",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h6,
)
})
},
If you have an icon either side you should be able to do the same, may have to adjust things if have two icons one side and one the other, then may want to use add a same sized icon to even things out and put enabled to false to it's not clickable and color to transparent so it's not seen, otherwise you could try and figure out the size and add padding end to the text, it seems you also need to add 16.dp padding to the transparent icon, as the navigation icon is given extra padding before the title but not between title and actions
Here's what I did for the arrowIcon and title
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
navController.popBackStack()
}
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "back arrow icon"
)
}
},
title = {
Text(
modifier = Modifier
.fillMaxWidth(),
text = "Basic Navigation",
textAlign = TextAlign.Center,
)
},
actions = {
IconButton(
modifier = Modifier
.padding(start = 16.dp),
enabled = false,
onClick = {}
) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "back arrow icon",
tint = Color.Transparent
)
}
}
)
}
I'm just a begginer with Jetpack Compose and before I searched for a solution of that problem I tried to figure my own, maybe it will be enough for someone. I needed centered title for TopAppBar with navigation icon on the left side only or with two icons on the left and right side, this solution was OK for me. Later I configurated that for including passed icon on the right side or no.
TopAppBar(
backgroundColor = Color.Green,
elevation = 5.dp,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// size of an icon and placeholder box on the right side
val iconSize = 32.dp
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Arrow Back",
modifier = Modifier
.clickable {
navController.popBackStack()
}
.size(iconSize)
)
Text(text = "Centered Title", fontSize = 32.sp)
// placeholder on the right side in order to have centered title - might be replaced with icon
Box(
modifier = Modifier
.size(iconSize)
) { }
}
I can't attach screenshot, preview is here: https://i.stack.imgur.com/UNQTF.png
I found a good workaround if you're using Material 2.
Create a Row, that contains:
Column with weight 1, containing an Icon
Column with weight 1, containing Header
Spacer with weight 1
The device's screen will automatically divide into 3 equal sections.
Here is a code sample:
Scaffold(
topBar = {
TopAppBar(
backgroundColor = Transparent,
elevation = 0.dp,
modifier = Modifier.fillMaxWidth()
) {
/** TopAppBar Content */
Row(
modifier = Modifier.height(51.dp).fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
/** Icon*/
Column(modifier = Modifier.weight(1f).padding(start = 9.dp)) {
IconButton(
onClick = { /*TODO*/ },
modifier = Modifier
.width(22.dp)
.height(22.dp),
enabled = true,
) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_arrow_back),
contentDescription = "Back",
tint = ThemeBlue
)
}
}
/** Header */
Column(modifier = Modifier.weight(1f)) {
Text(
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.h1,
textAlign = TextAlign.Center,
maxLines = 1,
text = "Header",
)
}
/*TODO
* When the stable version of Material 3 comes out
* we should replace the whole TopAppBar with CenterAlignedTopAppBar.
* For now, this Spacer keeps the Header component in the center of the topBar
*/
Spacer(modifier = Modifier.weight(1f))
}
}
},
content = {
Divider(color = ThemeBlue, thickness = 0.5.dp)
}
)
Here is a screenshot of the result this code produces:
Use fillMaxWidth() to let the Text View occupy AppBar width
Use wrapContentWidth(align = Alignment.CenterHorizontally) to align the Text Title.
TopAppBar(
title = {
Text(
text = screenname,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(align= Alignment.CenterHorizontally)
)
},
modifier = modifier
)
title = {
Row(
modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Text(
stringResource(R.string.app_title).uppercase(),
style = MaterialTheme.typography.h1
)
Spacer(modifier.width(1.dp))
}
},
#Composable
fun TopAppBarCompose(){
TopAppBar(
title = {
Box(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Hello",
fontSize = 30.sp,
modifier = Modifier.align(Alignment.Center)
)
}
},
)
}
The previous solutions are too complex. It is actually quite simple:
title = {
Text(
text = "title",
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}