I am struggling to understand how both Sync and Gmail have achieved this with there dialogs. I have tried different options such as Position Alert dialog in Android Compose, to move the alert dialog position however, I am still unable to get a similar appearance and lose the ability to tap outside to dismiss, which these two versions retain.
I am unsure how they can achieve the same result, both looking better than what I am able to achieve, while I am using a hacky solution. I am unsure if I'm missing something, maybe a different thing all together from Dialog/AlertDialog?
My current code implementation
if (openDialog) {
AlertDialog(
onDismissRequest = {
menuStatus = !menuStatus
openDialog = !openDialog
expanded = !expanded
},
shape = RoundedCornerShape(10.dp),
confirmButton = { null },
text = {
Column() {
Row() {
Text(text = "Account Management")
}
Spacer(modifier = Modifier.padding(16.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(id = R.drawable.baseline_logout_24),
contentDescription = null,
modifier = Modifier
.padding(end = 8.dp)
)
Text(text = "Sign out", )
}
}
},
modifier = Modifier
.customDialogModifier(CustomDialogPosition.TOP)
)
}
enum class CustomDialogPosition {
BOTTOM, TOP
}
fun Modifier.customDialogModifier(pos: CustomDialogPosition) = layout { measurable, constraints ->
val placeable = measurable.measure(constraints);
layout(constraints.maxWidth, constraints.maxHeight) {
when (pos) {
CustomDialogPosition.BOTTOM -> {
placeable.place(0, constraints.maxHeight - placeable.height, 10f)
}
CustomDialogPosition.TOP -> {
placeable.place(0, 150, 10f)
}
}
}
}
Related
I'm facing this weird issue with fillMaxWidth(fraction = ...) with an AlertDialog and Button, where the Button shows up initially at one size, and on click it shrinks to wrapping its content. Here is the most basic example I can create. I've tried with multiple versions of Compose but they all do the same thing. Any ideas?
AlertDialog(
modifier = modifier,
onDismissRequest = {},
text = { },
buttons = {
Button(
onClick = { },
modifier = Modifier
.fillMaxWidth(0.75f)
.padding(start = 12.dp, end = 12.dp, bottom = 8.dp)
) {
Text(text = "Done")
}
}
)
Before click:
After click:
I think you have to define a specific width for the AlertDialog, any child it has may not be able to calculate a 75% of unknown width .
either you fill the max width of the dialog
AlertDialog(
modifier = Modifier.fillMaxWidth(),
...
or specify an actual dp width
AlertDialog(
modifier = Modifier.width(150.dp),
...
I don't wanna throw jargons I can't explain, but I suspect when AlertDialog doesn't have any specific width, it cannot provide any incoming measurements for its children, which is in this case the Button cannot compute a 75% of unknown width on initial display, it looks like all size computation is happening only after an action has been made to the button, maybe a recomposition is happening under-the-hood having correct measurements.
I've used the sample composable from here and added your modifier to it for each button so it looks like
#Composable
fun AlertDialogSample() {
MaterialTheme {
Column {
val openDialog = remember { mutableStateOf(false) }
Button(onClick = {
openDialog.value = true
}) {
Text("Click me")
}
if (openDialog.value) {
AlertDialog(
onDismissRequest = {
// Dismiss the dialog when the user clicks outside the dialog or on the back
// button. If you want to disable that functionality, simply use an empty
// onCloseRequest.
openDialog.value = false
},
title = {
Text(text = "Dialog Title")
},
text = {
Text("Here is a text ")
},
confirmButton = {
Button(
modifier = Modifier
.fillMaxWidth(0.75f)
.padding(start = 12.dp, end = 12.dp, bottom = 8.dp),
onClick = {
openDialog.value = false
}) {
Text("This is the Confirm Button")
}
},
dismissButton = {
Button(
modifier = Modifier
.fillMaxWidth(0.75f)
.padding(start = 12.dp, end = 12.dp, bottom = 8.dp),
onClick = {
openDialog.value = false
}) {
Text("This is the dismiss Button")
}
}
)
}
}
}
}
and I see the buttons at 75% width.
Doesn't explain why you see the described issue, but does give you a solution.
The issue I faced was that I needed AlertDialog with some kind of List items (e. g. LazyColumn) and the TextField to search across these items. I wanted to display all the Dialog layout even when Keyboard is opened. But what I got is a Keyboard that cover some part of Dialog layout itself. I tried to use imePadding() for Dialog's Modifier but seems that Dialog ignoring that. I didn't find any solution for this on the Internet.
My code looks like so:
AlertDialog(
modifier = Modifier.fillMaxWidth()
.padding(AppTheme.margins.edge)
.imePadding(),
onDismissRequest = {
searchText = TextFieldValue("")
viewModel.clearSearchQuery()
dismissCallback?.invoke()
},
text = {
Column(
modifier = Modifier.wrapContentHeight()
) {
Text(
text = stringResource(R.string.dlg_select_content_title),
style = AppTheme.textStyles.hugeTitleText
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(top = AppTheme.margins.divRegular),
value = searchText,
placeholderText = stringResource(R.string.dlg_select_content_search_placeholder),
onValueChange = { newValue ->
searchText = newValue
viewModel.onSearchTextTyped(newValue.text)
}
)
RadioGroup(
modifier = Modifier
.verticalScroll(rememberScrollState()),
options = labels.map {
RadioOption(
title = it.name,
description = null,
selected = vmState.selectedLabel?.id == it.id,
tag = it.id
)
},
onOptionSelected = {
searchText = TextFieldValue("")
viewModel.clearSearchQuery()
viewModel.saveLabelSelection(it.tag as Int) {
dismissCallback?.invoke()
}
}
)
}
},
properties = DialogProperties(
usePlatformDefaultWidth = false
),
confirmButton = {
// Nothing
}
)
And the result:
I am not able to interact with several last items in list because Keyboard covers it.
I have implemented a solution for this issue. The solution is quite ugly, but working. If someone knows a more elegant solution, feel free to write it in an answer in this question.
Even though the dialog ignores the imePadding() we still can set the height. So, first of all we should to know what screen height available above keyboard.
#Composable
private fun TrickyHeight(
onHeightChanged: (Dp) -> Unit,
) {
val density = LocalDensity.current
Box(
modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(bottom = 30.dp) // additional padding
.onSizeChanged {
onHeightChanged.invoke(with(density) { it.height.toDp() })
}
)
}
Next step is to create wrapper over AlertDialog:
#Composable
fun TrickyDialog(
onDismissRequest: () -> Unit,
confirmButton: #Composable () -> Unit,
dismissButton: #Composable (() -> Unit)? = null,
icon: #Composable (() -> Unit)? = null,
title: #Composable (() -> Unit)? = null,
text: #Composable (() -> Unit)? = null,
shape: Shape = AlertDialogDefaults.shape,
containerColor: Color = AppTheme.colors.surfaceColor,
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
titleContentColor: Color = AlertDialogDefaults.titleContentColor,
textContentColor: Color = AlertDialogDefaults.textContentColor,
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
properties: DialogProperties = DialogProperties()
) {
val maxDialogHeight = remember { mutableStateOf(0.dp) }
TrickyHeight(onHeightChanged = { maxDialogHeight.value = it })
AlertDialog(
modifier = Modifier
.fillMaxWidth()
.heightIn(0.dp, maxDialogHeight.value)
.padding(AppTheme.margins.edge),
onDismissRequest = onDismissRequest,
confirmButton = confirmButton,
dismissButton = dismissButton,
icon = icon,
title = title,
text = text,
shape = shape,
containerColor = containerColor,
iconContentColor = iconContentColor,
titleContentColor = titleContentColor,
textContentColor = textContentColor,
tonalElevation = tonalElevation,
properties = properties
)
}
Also, do not forget to add correct android:windowSoftInputMode in Manifest: android:windowSoftInputMode="adjustResize"
Now you can use TrickyDialog instead of AlertDialog. Again, this solution is not elegant. But maybe it will be helpful for someone who faced the same issue. Also, this solution will not work properly for Landscape Screen Orientation.
As of Compose UI 1.3.0-beta01, you can set DialogProperties.decorFitsSystemWindows to false and imePadding() will work.
https://issuetracker.google.com/issues/229378542
https://developer.android.com/jetpack/androidx/releases/compose-ui#1.3.0-beta01
AlertDialog(
modifier = Modifier.imePadding(),
properties = DialogProperties(decorFitsSystemWindows = false),
onDismissRequest = {
// ...
},
title = {
// ...
},
text = {
// ...
},
confirmButton = {
// ..
},
)
The Code A displays a dialog box based AlertDialog, and I get Image A when I run Code A.
I find the space between title = { Text(text = dialogTitle) } and text = {...} is too closer in Image A.
So I set Modifier.padding(top = 100.dp) to wish to increase the space between the two controls, but I only get Image B, it seems that Modifier.padding(top = 100.dp) doesn't work as expected, how can I fix it?
Code A
#Composable
fun EditTextDialog(
isShow: Boolean,
onDismiss: () -> Unit,
onConfirm: (String) -> Unit,
saveTitle: String = stringResource(R.string.dialog_save_title),
cancelTitle:String = stringResource(R.string.dialog_cancel_title),
dialogTitle:String ="Edit",
editFieldContent:String ="",
) {
var mText by remember(editFieldContent){ mutableStateOf(editFieldContent) }
val cleanAndDismiss = {
mText = editFieldContent
onDismiss()
}
if (isShow) {
AlertDialog(
title = { Text(text = dialogTitle) },
text = {
Column(
Modifier.padding(top = 20.dp)
//Modifier.padding(top = 100.dp)
//Modifier.height(100.dp), //The same result as Image A
//verticalArrangement = Arrangement.Center
) {
TextField(
value = mText,
onValueChange = { mText = it }
)
}
},
confirmButton = {
TextButton(onClick = { onConfirm(mText) }) {
Text(text = saveTitle)
}
},
dismissButton = {
TextButton(onClick = cleanAndDismiss) {
Text(text = cancelTitle)
}
},
onDismissRequest = cleanAndDismiss
)
}
}
Image A
Image B
With M3 AlertDialog (androidx.compose.material3.AlertDialog) it works.
With M2 AlertDialog, one solution is to remove the title attribute and use the text attribute for the whole layout.
AlertDialog(
onDismissRequest = {},
text = {
Column(){
Text(text = "Title")
Spacer(Modifier.height(30.dp))
TextField(
value = "mText",
onValueChange = { },
)
}
},
//buttons..
)
I don't understand what you're trying to do. If you want more space between the TextField and the dialog buttons, then you don't want a top padding. You want padding below the TextField, so it would be bottom padding on the column.
Also, there's a chance that it won't work properly inside a Column, and you might have to switch it out for Box. And if that doesn't work for some reason, just add a spacer below the TextField:
Spacer(Modifier.height(20.dp).fillMaxWidth())
I assume you are using Material AlertDialog? If yes try using the Material3 variant. It should work then.
Just implement following library:
implementation "androidx.compose.material3:material3:1.0.0-beta02"
And make sure to use the Material3 AlertDialog Composable which is imported with the library.
I'm using the new BackdropScaffold composable to make a similar looking screen like Google Map with a Map on the back and a list on the front. (See the image)
As you can see there is a problem with the corner around the front layer. Currently is displayed the surface under (pale blue). What I would like to achieve is having the Google Map shown in those corners. I tried to play with the size and padding of GoogleMap composable or the front panel but no luck.
UPDATE
The following example code shows the issue I'm facing. As you can see the BackdropScaffold background is correctly applied (RED). The corners of the front layer are transparent. The issue comes out when you have a different color in your background layer (BLUE). If the background layer contains a map you have the same issue.
BackdropScaffold is dividing the space but not overlaying any layer. The front layer should overlay a bit the back layer to fix this problem.
#OptIn(ExperimentalMaterialApi::class)
#Composable
internal fun test() {
val scope = rememberCoroutineScope()
val selection = remember { mutableStateOf(1) }
val scaffoldState = rememberBackdropScaffoldState(BackdropValue.Concealed)
val frontLayerHeightDp = LocalConfiguration.current.screenHeightDp / 3
LaunchedEffect(scaffoldState) {
scaffoldState.conceal()
}
BackdropScaffold(
scaffoldState = scaffoldState,
appBar = {
TopAppBar(
title = { Text("Backdrop scaffold") },
navigationIcon = {
if (scaffoldState.isConcealed) {
IconButton(onClick = { scope.launch { scaffoldState.reveal() } }) {
Icon(Icons.Default.Menu, contentDescription = "Localized description")
}
} else {
IconButton(onClick = { scope.launch { scaffoldState.conceal() } }) {
Icon(Icons.Default.Close, contentDescription = "Localized description")
}
}
},
actions = {
var clickCount by remember { mutableStateOf(0) }
IconButton(
onClick = {
// show snackbar as a suspend function
scope.launch {
scaffoldState.snackbarHostState
.showSnackbar("Snackbar #${++clickCount}")
}
}
) {
Icon(Icons.Default.Favorite, contentDescription = "Localized description")
}
},
elevation = 0.dp,
backgroundColor = Color.Transparent
)
},
backLayerContent = {
LazyColumn(modifier = Modifier.background(Color.Blue)) {
items(if (selection.value >= 3) 3 else 5) {
ListItem(
Modifier.clickable {
selection.value = it
scope.launch { scaffoldState.conceal() }
},
text = { Text("Select $it", color = Color.White) }
)
}
}
},
backLayerBackgroundColor = Color.Red,
frontLayerShape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
headerHeight = frontLayerHeightDp.dp,
frontLayerBackgroundColor = Color.White,
frontLayerContent = {
LazyColumn {
items(50) {
ListItem(
text = { Text("Item $it") },
icon = {
Icon(
Icons.Default.Favorite,
contentDescription = "Localized description"
)
}
)
}
}
}
)
}
BackdropScaffold is creating a Surface for from layer under the hood, when you create your own inside frontLayerContent it's displayed on top of built-in one.
Instead use frontLayerShape and frontLayerBackgroundColor parameters:
frontLayerShape = BottomSheetShape,
frontLayerBackgroundColor = Color.White,
frontLayerContent = {
LazyColumn(
modifier = Modifier.padding(16.dp)
) {
items(
items = moorings,
itemContent = { mooring ->
...
}
)
}
}
p.s. some comments about your code:
When you have modifier parameter, you should only apply it for the topmost container in your view - here you've applied it for content of frontLayerContent, which may cause unexpected behaviour.
You don't need to wrap LazyColumn in a Column - it has no effect when Column has only one child, and if you need to apply a modifier you can do it directly for LazyColumn
Building a simple keyboard is fairly simple and straightforward in Jetpack Compose.
I built a really simple KeyRow by using this:
Key.kt
#Composable
fun Key(modifier: Modifier = Modifier, label: String, onClick: () -> Unit) {
val shape = RoundedCornerShape(4.dp)
//TODO: make clickable outside but don't show ripple
Box(modifier = modifier
.padding(2.dp)
.clip(shape)
.clickable(onClick = onClick)
.background(Color.White)
.padding(vertical = 12.dp, horizontal = 4.dp), contentAlignment = Alignment.Center) {
Text(text = label, fontSize = 20.sp)
}
}
KeyRow.kt
#Composable
fun KeyRow(keys: List<String>) {
Row(modifier = Modifier.fillMaxWidth().background(color = grey200)) {
keys.forEach {
Key(modifier = Modifier.weight(1f), label = it, onClick = { })
}
}
}
That's what it looks like:
I want to achieve this animation:
However, I'm currently stuck with this
![4]
Hierachy
-Keyboard
--KeyRow
---KeyLayout
----Key
----KeyPressedOverlay (only visible when pressed)
My main problem is that I don't know how to show the KeyPressedOverlay Composale (which is larger than the Key Composable) without making the parent Layout larger. As a result, I need to overflow the parent layout in some way.
Not sure if it's the best way (probably not), but I found a solution using ConstraintLayout...
val keys = listOf("A", "B", "C", "D")
ConstraintLayout(
modifier = Modifier.graphicsLayer(clip = false)
) {
val refs = keys.map { createRef() }
refs.forEachIndexed { index, ref ->
val modifier = when (index) {
0 -> Modifier.constrainAs(ref) {
start.linkTo(parent.start)
}
refs.lastIndex -> Modifier.constrainAs(ref) {
start.linkTo(refs[index - 1].end)
end.linkTo(parent.end)
}
else -> Modifier.constrainAs(ref) {
start.linkTo(refs[index - 1].end)
end.linkTo(refs[index + 1].start)
}
}
val modifierPressed = Modifier.constrainAs(createRef()) {
start.linkTo(ref.start)
end.linkTo(ref.end)
bottom.linkTo(ref.bottom)
}
KeyboardKey(
keyboardKey = keys[index],
modifier = modifier,
modifierPressed = modifierPressed,
pressed = { s -> /* Do something with the key */}
)
}
}
One important detail here is graphicLayer(clip = false) (which is similar to the clipChildren in View Toolkit). Then, I'm creating a modifier to each key and to the pressed key. Noticed that the modifierPressed is aligned to the center/bottom of the other modifier.
Finally the KeyboardKey is described below.
#Composable
fun KeyboardKey(
keyboardKey: String,
modifier: Modifier,
modifierPressed: Modifier,
pressed: (String) -> Unit
) {
var isKeyPressed by remember { mutableStateOf(false) }
Text(keyboardKey, Modifier
.then(modifier)
.pointerInput(Unit) {
detectTapGestures(onPress = {
isKeyPressed = true
val success = tryAwaitRelease()
if (success) {
isKeyPressed = false
pressed(keyboardKey)
} else {
isKeyPressed = false
}
})
}
.background(Color.White)
.padding(
start = 12.dp,
end = 12.dp,
top = 16.dp,
bottom = 16.dp
),
color = Color.Black
)
if (isKeyPressed) {
Text(
keyboardKey, Modifier
.then(modifierPressed)
.background(Color.White)
.padding(
start = 16.dp,
end = 16.dp,
top = 16.dp,
bottom = 48.dp
),
color = Color.Black
)
}
}
This is the result I got:
Edit:
Adding some more logic, I was able to get this...
I hope it helps this time ;)
Here's the gist just in case...
https://gist.github.com/nglauber/4cb1573efba9024c008ea71f3320b4d8
I guess you're looking for the pressIndicatorGestureFilter modifier...
I tried this and worked for me...
var pressed by remember { mutableStateOf(false) }
val padding = if (pressed) 32.dp else 16.dp
Text("A", Modifier
.pressIndicatorGestureFilter(
onStart = {
pressed = true
},
onStop = {
pressed = false
},
onCancel = {
pressed = false
}
)
.background(Color.White)
.padding(start = 16.dp, end = 16.dp, top = padding, bottom = padding)
)