So I have this composable in my project ...
#Composable
private fun ShowDialog() {
var showText by remember { mutableStateOf(false) }
val text = if (showText) {
"Hide Text"
} else {
"Show Text"
}
Dialog(onDismissRequest = { }) {
Card(modifier = Modifier.padding(15.dp)) {
Column(modifier = Modifier.padding(15.dp)) {
AnimatedVisibility(visible = showText) {
Text(
text = "Here is the show text sample",
modifier = Modifier.padding(5.dp),
style = MaterialTheme.typography.body1,
color= Color.Black
)
}
Button(onClick = { showText = !showText }) {
Text(text = text)
}
}
}
}
}
If you have gone through the code, you might get what it is supposed to do. i.e it is basically a dialog with one text and a button below it. When the user clicks on a button the text above the button will toggle its visibility.
But the problem with the code is, When I click on the button, the text appears but the button gets invisible in other words the text takes the space and pushes a button to below. But yet the container in this case card or the column doesn't expand its height.
Is it supposed to work like that ? Or is this a bug?
I tried animateContentSize() on Column and Card but it didn't work. And checked similar questions on StackOverflow but didn't found any useful information.
Luckily, I found a temporary working answer for this problem,
What we need to use is just pass DialogProperties(usePlatformDefaultWidth = false) as properties parameter for dialog. This will make the dialog to resizable like this
#Composable
private fun ShowDialog() {
var showText by remember { mutableStateOf(false) }
val text = if (showText) {
"Hide Text"
} else {
"Show Text"
}
Dialog(
onDismissRequest = { },
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Card(
modifier = Modifier
.padding(15.dp)
.wrapContentWidth()
.animateContentSize()
) {
Column(modifier = Modifier.padding(15.dp).fillMaxWidth(1f)) {
AnimatedVisibility(visible = showText) {
Text(
text = "Sample",
modifier = Modifier
.padding(5.dp)
.fillMaxWidth(1f),
style = MaterialTheme.typography.body1,
color = Color.Black
)
}
Button(onClick = { showText = !showText }) {
Text(text = text)
}
}
}
}
}
Caution: It uses #ExperimentalComposeUiApi
This API is experimental and is likely to change in the future.
Related
Which whould be the best approach to display, for example, 2 types of dialogs on a screen and also parametrize the message to each dialog? For now I have a remember mutable state DialogType enum variable which represents if a dialog must be displayed or not, so if the state is MessageDialog or ConfirmationDialog will display one of those. But... what if I need to parametrize the message (or more variables like for example a image, but for this case it will be simplyfied to message) to one of these dialogs? Should I use another remember state variable with the string? If so, then I'm using two different remember state variables. Is that correct?
For sure there are complex ways to achieve this, creating special data classes, wrapping them, passing composable content as parameters to the dialog, etc... but I'm trying to avoid these kind of complex and overprogrammed developments and trying to find a simple and easy way to achieve this.
This is the sample code I did:
var dialogState by remember { mutableStateOf(DialogStateType.HIDDEN) }
var dialogMessage by remember { mutableStateOf("") }
In the buttons i change the state of these variables like this:
onClick = {
dialogMessage = "sample message text"
dialogState = DialogStateType.MESSAGE
}) {
onClick = {
dialogMessage = "sample confirmation dialog text"
dialogState = DialogStateType.CONFIRMATION
}) {
And in the main composable I display them using this:
when (dialogState) {
DialogStateType.MESSAGE -> {
MessageDialog(dialogMessage) {
dialogState = DialogStateType.HIDDEN
}
}
DialogStateType.CONFIRMATION -> {
ConfirmationDialog(dialogMessage,
{
CoroutineScope(Dispatchers.Default).launch {
//DO HARD JOB
}
},
{ dialogState = DialogStateType.HIDDEN },
{ dialogState = DialogStateType.HIDDEN })
}
DialogStateType.HIDDEN -> {}
}
This approach works and it's simple, but seems to be not too much escalable, Is this the recommended way of doing this? or is there a simpler and easier way?
Kotlin's default arguments and ?.let {...} can help
Some class representing state:
data class DialogState(val caption: String? = "Dialog",
val text: String? = null,
val imageResId: String? = null,
val confirmButtonText: String = "OK",
val cancelButtonText: String? = null,
val onConfirm: () -> Unit = {},
val onCancel: () -> Unit = {})
Generic dialog composabe like:
#Composable
fun GenericDialog(dialogState: DialogState, onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Surface {
Column(
modifier = Modifier.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
dialogState.caption?.let {
Text(text = dialogState.caption,
style = TextStyle(fontWeight = FontWeight.ExtraBold))
Spacer(modifier = Modifier.size(10.dp))
}
dialogState.imageResId?.let{
//show image
Spacer(modifier = Modifier.size(10.dp))
}
dialogState.text?.let {
Text(text = dialogState.text)
Spacer(modifier = Modifier.size(10.dp))
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(onClick = dialogState.onConfirm) {
Text(text = dialogState.confirmButtonText)
}
dialogState.cancelButtonText?.let {
Spacer(modifier = Modifier.size(10.dp))
Button(onClick = dialogState.onCancel) {
Text(text = dialogState.cancelButtonText)
}
}
}
}
}
}
}
And then:
var showDialog by remember { mutableStateOf(false) }
var dialogState = remember { DialogState() }
//...
if (showDialog) GenericDialog(dialogState = dialogState) { showDialog = false }
//...
Button(onClick = {
dialogState = DialogState(caption = "Caption",
text = "Text text text text text text text text text text text",
onConfirm = {showDialog = false})
showDialog = true
}) {
Text(text = "Caption, text, one button")
}
// or
Button(onClick = {
dialogState = DialogState(caption = null,
text = "Text text text text text text text text text text text",
cancelButtonText = "Cancel",
onConfirm = {showDialog = false},
onCancel = {showDialog = false})
showDialog = true
}) {
Text(text = "Text, two buttons")
}
I have a text which need to be animated to show and hide with the value is null or not. it would have been straight forward if the visibility is separately handle, but this is what I got.
In the bellow code the enter animation works but the exit animation dont as the text value is null.
I can think of something with remembering the old value but not sure how.
#Composable
fun ShowAnimatedText(
text : String?
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = text != null,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
I think the fade-out animation is actually working "per-se".
I suspect the parameter text: String? is a value coming from a hoisted "state" somewhere up above ShowAnimatedText, and since you are directly observing it inside the animating scope, when you change it to null it instantly removes the Text composable, and your'e not witnessing a slow fade out.
AnimatedVisibility(
...
) {
text?.let { // your'e directly observing a state over here
Text(text = it)
}
}
This is my attempt completing your snippet based on my assumption and making it work, the fade-in works, but the desired fade-out is instantly happening.
#Composable
fun SomeScreen() {
var text by remember {
mutableStateOf<String?>("Initial Value")
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
text = "New Value"
}) {
Text("Set New Value")
}
Button(onClick = {
text = null
}) {
Text("Remove Value")
}
AnimatedText(text = text)
}
}
#Composable
fun ShowAnimatedText(
text : String?
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = text != null,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
You can solve it by modifying the text to a non-state value and change your visibility logic from using a nullability check to some "business logic" that would require it to be visible or hidden, modifying the codes above like this.
#Composable
fun SomeScreen() {
var show by remember {
mutableStateOf(true)
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
show = !show
}) {
Text("Set New Value")
}
AnimatedText(text = "Just A Value", show)
}
}
#Composable
fun ShowAnimatedText(
text : String?,
show: Boolean
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = show,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
I fixed it by remembering the previous state (Or don't set the null value) until the exit animation is finished, if the text is null.
Thank you z.y for your suggestion.
#Composable
fun ShowAnimatedText(
text : String?,
show: Boolean
) {
var localText by remember {
mutableStateOf<String?>(null)
}
AnimatedContent(show, localText)
LaunchedEffect(key1 = text, block = {
if(text == null){
delay(2000)
}
localText = text
})
}
#Composable
private fun AnimatedContent(show: Boolean, localText: String?) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = show,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
localText?.let {
Text(text = it)
}
}
}
}
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.
Is there a font picker library? I'd like to show a dialog in my app, were the user could choose a font. I was able to find this, but there is no explanation on how to use it. Also, I'm using Jetpack Compose, maybe it has a font picker built in?
Thanks!
Since I couldn't find any libraries, I implemented the FontPicker myself.
Here is Jetpack Compose implementation:
mentalTextApi::class)
#Composable
fun FontPicker(
fonts: Map<File, Font>,
initialFontPath: String? = null,
onFontChosen: (font: Font?) -> Unit,
) {
var showMenu by remember { mutableStateOf(false) }
var fontPath by remember { mutableStateOf(initialFontPath) }
Column(
modifier = Modifier
.clickable { showMenu = true }
.fillMaxWidth()
.padding(8.dp),
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text("Font:")
if (fontPath == null) {
Text("Default")
} else {
val fontFile = File(fontPath)
Text(
fontFile.nameWithoutExtension,
fontFamily = FontFamily(Font(fontFile)),
)
}
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
) {
DropdownMenuItem(
text = { Text("Default", fontSize = 25.sp) },
onClick = {
showMenu = false
fontPath = null
onFontChosen(null)
}
)
for ((file, font) in fonts) {
DropdownMenuItem(
text = {
Text(
file.nameWithoutExtension,
fontFamily = FontFamily(font),
fontSize = 25.sp,
)
},
onClick = {
showMenu = false
fontPath = file.path
onFontChosen(font)
}
)
}
}
}
Here is how to use it:
val fonts = File("/system/fonts")
.listFiles()!!.associateWith { Font(it) }
FontPicker(fonts) {
// use picked font here
}
You will get this:
And if you tap on it, a popup menu will be shown:
The popup menu might take some time to show because of all the fonts that have to be loaded, but it's quicker on release builds.
I try show AlertDialog when press a button.
For AlertDialog i have a composable function - showDialog.
It is clear that this function calls a dialog.
I have another composable function which displays some window with text and buttons.
When the button is clicked, I want to call a function that stores the AlertDialog.
AlertDialog body:
fun monthDialog() {
val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
AlertDialog(
onDismissRequest = {
openDialog.value = false
},
title = {
Text(text = "Title")
},
text = {
Text(
"This area typically contains the supportive text " +
"which presents the details regarding the Dialog's purpose."
)
},
buttons = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { openDialog.value = false }
) {
Text("Dismiss")
}
}
}
)
}
first I tried the simplest solution that came to my mind:
IconButton(onClick = monthDialog())
and got error (#Composable invocations can only happen from the context of a #Composable function)
after i tried a normal trigger using mutablestateof and following condition:
val showDialog = remember { mutableStateOf(false)}
if(showDialog.value == true) monthDialog()
and put some like this into onClick event:
monthHeader(onClick = {showDialog.value = !showDialog.value})
and this is work....but ugly and badly.
for a first time this is worf fine. but after the first click, I need to click two more times to trigger that event again.
button code snippet:
fun Calendar (){
//...
val showDialog = remember { mutableStateOf(false)}
if(showDialog.value == true) monthDialog()
Card(
){
Column(){
IconButton(onClick ={
monthDialog() // error here
//showDialog.value = !showDialog.value
}
}
}
after few hours for searching in google
i try my own solution
val clicked = remember { mutableStateOf(false)}
val showDialog = remember { mutableStateOf(false)}
if(showDialog.value) monthDialog()
else if(clicked.value) monthDialog()
Card(){
Column(){
monthHeader(onClick = {
clicked.value = showDialog.value
showDialog.value = !clicked.value
})
}
}
but in my opinion this is crutch/kludge
Leaving a better solution here (imho):
Hoist the state of your dialog.
#Composable
fun MonthDialog(onClose: () -> Unit) {
AlertDialog(
onDismissRequest = onClose,
title = {
Text(text = "Title")
},
text = {
Text(
"This area typically contains the supportive text " +
"which presents the details regarding the Dialog's purpose."
)
},
buttons = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onClose
) {
Text("Dismiss")
}
}
}
)
Noticed that I removed the state from this component and make it stateless. This component will just notify when the dialog is closed.
Now you can call the dialog like this:
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
MonthDialog(onClose = { showDialog = false })
}
Card {
MonthHeader( // use upper case for naming your composables
onClick = {
showDialog = true
}
)
}
One can also use Modifier / PointerInputScope:
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { },
onTap = { },
onDoubleTap = { },
onLongPress = { }
)