Jetpack compose: scrolling in dialog gives strange effects - kotlin

I'm trying to create a dialog where you can pick a number. I use FlowRow from Accompanist. But it gives strange results when scrolling. I guess that I'm missing something here.
The code:
#Composable
fun AlertDialogErrorTest() {
var showDlg by remember { mutableStateOf(false)}
val scrollState = rememberScrollState()
if (showDlg) {
AlertDialog(
onDismissRequest = { showDlg = false },
title = { Text(text = "Pick something from the list") },
text = {
FlowRow(modifier = Modifier.verticalScroll(scrollState)) {
for (i in 1..100) {
Box (modifier = Modifier.size(48.dp).padding(8.dp).background(Color.LightGray), contentAlignment = Alignment.Center) {
Text (i.toString())
}
}
}
},
confirmButton = {
TextButton(onClick = { showDlg = false }) {
Text("Done")
}
},
dismissButton = {
TextButton(onClick = { showDlg = false }) {
Text("Cancel")
}
}
)
}
Button(onClick = {showDlg = true}) {
Text("Show dialog")
}
}
The result when scrolling:

This is because AlertDialog does not currently support scrollable content. If you think this is a bug, you can report it in the Compose issue tracker.
Meanwhile, you can use a plain Dialog, which lays under the AlertDialog.
Dialog(
onDismissRequest = { showDlg = false },
content = {
Column(Modifier.background(Color.White)) {
Text(
text = "Pick something from the list",
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
FlowRow(
modifier = Modifier
.verticalScroll(scrollState)
.weight(1f)
) {
for (i in 1..100) {
Box(
modifier = Modifier
.size(48.dp)
.padding(8.dp)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(i.toString())
}
}
}
FlowRow(
mainAxisSpacing = 8.dp,
crossAxisSpacing = 12.dp,
modifier = Modifier
.align(Alignment.End)
.padding(horizontal = 8.dp, vertical = 2.dp)
) {
TextButton(onClick = { showDlg = false }) {
Text("Cancel")
}
TextButton(onClick = { showDlg = false }) {
Text("Done")
}
}
}
},
)
Result:

I have faced the same issue, and the work around is to pass null in both title , and text, and to put the whole content in the buttons, you might check the below example, and please forgive my naming :).
AlertDialog(
onDismissRequest = {
state.onDismiss(state.data)
},
title = null,
text = null,
buttons = {
Column {
val expandedState =
state.data.expandedState
expandedState?.let {
RenderFieldPerCorrespondingState(it.title)
LazyColumnViewGroupComposable(
viewGroupState = it.itemsState,
scrollTo = scrollTo
)
ViewGroupComposable(viewGroupState = it.actionsState)
}
}
}
)

Related

Mutablestateof - State hoisting

What to do to manage one state in two functions? (openDialog)
How to transfer openDialog state from ProductDialog() to AppBar()?
I know I can make a separate class per state but I don't want to.
#Composable
fun ProductDialog() {
val openDialog = rememberSaveable() { mutableStateOf(true) }
val productNameState = remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = { openDialog.value = false },
title = { Text(
modifier = Modifier.padding(bottom = 8.dp),
text = "Add new product",
fontSize = 20.sp,
fontWeight = FontWeight.Bold
) },
confirmButton = { Button(onClick = { openDialog.value = false }) {
Text(text = "Add") }
},
dismissButton = { Button(onClick = { openDialog.value = false }) {
Text(text = "Cancel")
}
},
text = {
Column() {
TextField(value = productNameState.value, onValueChange = {
})
TextField(value = productNameState.value, onValueChange = {})
}
}
)
}
#Composable
fun AppBar() {
val openDialog = rememberSaveable() { mutableStateOf(false) }
if (openDialog.value) ProductDialog()
Scaffold(
topBar = {
TopAppBar(
actions = { Icon(imageVector = Icons.Default.Menu, contentDescription = null) },
title = { Text(text = "Shopping List Compose") })
},
floatingActionButton = {
FloatingActionButton(modifier = Modifier.padding(10.dp), onClick = { openDialog.value = true }) {
Icon(Icons.Default.Add, contentDescription = null)
}}
) {
ListItems()
}
}
What to do to manage one state in two functions? (openDialog)
From the example you shared, it seems that the only use of openDialog in ProductDialog is to close the dialog. You can instead paas a lambda to ProductDialog to close the dialog.
#Composable
fun ProductDialog(closeDialog: () -> Unit) {
AlertDialog(
onDismissRequest = closeDialog,
title = { ... },
confirmButton = {
Button(onClick = closeDialog) {
Text(text = "Add")
}
},
dismissButton = {
Button(onClick = closeDialog) {
Text(text = "Cancel")
}
},
text = { ... }
)
}
#Composable
fun AppBar() {
val openDialog by rememberSaveable() { mutableStateOf(false) }
if (openDialog) ProductDialog { openDialog = false}
...
}

Change BottomDrawer's gesturesEnabled according to drawerState in Jetpack Compose?

I have an BottomDrawer but i want that when BottomDrawer is closed the gesturesEnabled should be false else if its open then gesturesEnabled should be true
Here's the
code I know its not my but same
val scope = rememberCoroutineScope()
val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
val gesturesEnabled = !drawerState.isClosed
Column {
Row(
modifier = Modifier.fillMaxWidth()
) {
Checkbox(gesturesEnabled, null)
Text(text = if (gesturesEnabled) "Gestures Enabled" else "Gestures Disabled")
}
BottomDrawer(
gesturesEnabled = gesturesEnabled,
drawerState = drawerState,
drawerContent = {
Button(
modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp),
onClick = { scope.launch { drawerState.close() } },
content = { Text("Close Drawer") }
)
LazyColumn {
items(25) {
ListItem(
text = { Text("Item $it") },
icon = {
Icon(
Icons.Default.Favorite,
contentDescription = "Localized description"
)
}
)
}
}
},
content = {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val openText = if (gesturesEnabled) "▲▲▲ Pull up ▲▲▲" else "Click the button!"
Text(text = if (drawerState.isClosed) openText else "▼▼▼ Drag down ▼▼▼")
Spacer(Modifier.height(20.dp))
Button(onClick = { scope.launch { drawerState.open() } }) {
Text("Click to open")
}
}
}
)
}

Cannot make ExposedDropdownMenu same width as OutlinedTextField

I faced the following problem - items of the dropdown are not the same width as OutlinedTextField
Looked for the solution - found the following:
Add the variable to keep textField width:
var textFieldSize by remember { mutableStateOf(Size.Zero) }
Set the value in the onGloballyPositioned of TextField
onGloballyPositioned { coordinates ->
textFieldSize = coordinates.size.toSize()
}
Read the value in ExposedDropdownMenu
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.background(Color.White)
.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
)
The problem is that it works fine with DropdownMenu, but doesn't work with ExposedDropdownMenu. What's the problem?
Here's the full code:
var expanded by remember { mutableStateOf(false) }
val sexList by remember { mutableStateOf(listOf("Male", "Female")) }
var textFieldSize by remember { mutableStateOf(Size.Zero) }
val icon = if (expanded)
Icons.Filled.ArrowDropUp
else
Icons.Filled.ArrowDropDown
ExposedDropdownMenuBox(
modifier = modifier
.clickable(onClick = { expanded = true }),
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
textFieldSize = coordinates.size.toSize()
},
colors = TextFieldDefaults.textFieldColors(
backgroundColor = BorderColor,
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = BrandColor,
focusedLabelColor = BrandColor,
),
leadingIcon = {
Image(
painter = painterResource(id = R.drawable.ic_complete_registration_sex),
contentDescription = null
)
},
trailingIcon = { Icon(icon, null) },
shape = RoundedCornerShape(Dimen.Dimen14),
label = {
Text(
"Choose Gender",
style = PoppinsNormalStyle14
)
},
readOnly = true
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.background(Color.White)
.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
) {
sexList.forEach {
DropdownMenuItem(
onClick = { expanded = false },
) {
Text(it, style = PoppinsNormalStyle12Gray2)
}
}
}
}
ExposedDropdownMenuBox is built to calculate width for you so you don't have to use all this onGloballyPositioned related logic.
The fact that it doesn't work is a known issue.
Until it's fixed it's recommended to use DropdownMenu with Modifier.exposedDropdownSize()(this modifier will apply the width calculated by ExposedDropdownMenuBox) instead of ExposedDropdownMenu:
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.background(Color.White)
.exposedDropdownSize()
) {
sexList.forEach {
DropdownMenuItem(
onClick = { expanded = false },
) {
Text(it, style = PoppinsNormalStyle12Gray2)
}
}
}

Clickable Toggle Continuously Repeating Causing UI Glitch in Search Bar - Jetpack Compose Android Studio Kotlin

I'm trying to have the text field appear when the "Search Movies" button is selected, and disappear when not selected.
When I tap on the "Search Movies" button, the app seems to recognize the clicks constantly.
Here's a video of what it looks like: https://youtu.be/5LQ8k0Y05Cc
As you can see, the text field also doesn't disappear when the "Popular Movies" button is selected.
Here's the composable function:
#Composable
fun MaterialButtonToggleGroup() {
val options = listOf(
"Popular Movies",
"Search Movies"
)
var selectedOption by remember {
mutableStateOf("")
}
val onSelectionChange = { text: String ->
selectedOption = text
}
Column() {
var isExpanded by remember { mutableStateOf(false) }
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth(),
) {
options.forEach { text ->
Row(
modifier = Modifier
.padding(
vertical = 8.dp,
),
) {
Text(
text = text,
style = typography.body1.merge(),
color = Color.White,
modifier = Modifier
.clip(
shape = RoundedCornerShape(
size = 12.dp,
),
)
.clickable {
onSelectionChange(text)
}
.background(
if (text == selectedOption) {
Color.Magenta
} else {
Color.Gray
}
)
.padding(
vertical = 12.dp,
horizontal = 16.dp,
),
)
}
}
}
if (selectedOption == "Search Movies") {
isExpanded = !isExpanded
}
if (isExpanded) {
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
modifier = Modifier.fillMaxWidth()
.padding(all = 3.dp),
onValueChange = { text = it },
label = { Text("Enter Movie Info") }
)
}
}
(activity as MainActivity?)?.viewSelection(selectedOption)
}
Surface {
MaterialButtonToggleGroup()
}
Turns out I should just set isExpanded to true or false for each scenario.
I changed:
if (selectedOption == "Search Movies") {
isExpanded = !isExpanded
}
if (isExpanded) {
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
modifier = Modifier.fillMaxWidth()
.padding(all = 3.dp),
onValueChange = { text = it },
label = { Text("Enter Movie Info") }
)
}
To:
if (selectedOption == "Search Movies") {
isExpanded = true
} else {
isExpanded = false
}
if (isExpanded) {
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
modifier = Modifier
.fillMaxWidth()
.padding(all = 3.dp),
onValueChange = { text = it },
label = { Text("Enter Movie Info") }
)
}

Jetpack Compose - Disable TextField long press handler

I have an IconButton in the trailingIcon of OutlinedTextField like:
OutlinedTextField(
modifier = Modifier.weight(1f),
label = { Text(text = "Label") },
value = text,
onValueChange = { text = it },
trailingIcon = {
IconButton2(onClick = {
println("onClick")
}, onLongClick = {
println("onLongClick shows TextToolbar")
}) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = null
)
}
}
)
IconButton2 is just a copy of IconButton but with combinedClickable to include onLongClick instead of clickable.
The problem is that when I long click IconButton2, it shows the TextToolbar for the TextField. Doesn't matter what I do, the text field will handle long click, show the TextToolbar and provide haptic feedback.
Even if I use pointerInput with awaitPointerEvent and consumeAllChanges (like here) it still triggers it. The TextField doesn't answer to any tap or anything but if I long click it, it answers!
The workaround I'm doing for now is wrapping the text field in a Row and add the IconButton beside it instead of "inside" but I needed to have the icon button as the trailingIcon.
Is there any way to properly do it?
Compose 1.0.3 and 1.1.0-alpha05 both behaves the same.
I ended up making a small hack that seems to be working fine, so basically I add a dummy Box as the trailingIcon to get the position of it, then I add an IconButton outside of it (both wrapped in a Box) and I also get the position of it + I offset it using the position of the dummy box. Not the ideal solution but works fine.
Here's the full source if anyone needs it:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colors.background
) {
var text by remember { mutableStateOf("") }
var trailingIconOffset by remember { mutableStateOf(Offset.Zero) }
var iconButtonOffset by remember { mutableStateOf(Offset.Zero) }
val colors = TextFieldDefaults.outlinedTextFieldColors()
Column {
//With hack
Box {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
label = { Text(text = "With hack") },
value = text,
onValueChange = { text = it },
trailingIcon = {
Box(modifier = IconButtonSizeModifier
.onGloballyPositioned {
trailingIconOffset = it.positionInRoot()
}
)
},
colors = colors
)
val contentColor by colors.trailingIconColor(
enabled = true,
isError = false
)
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalContentAlpha provides contentColor.alpha
) {
IconButton2(
modifier = Modifier
.onGloballyPositioned {
iconButtonOffset = it.positionInRoot()
}
.absoluteOffset {
IntOffset(
(trailingIconOffset.x - iconButtonOffset.x).toInt(),
(trailingIconOffset.y - iconButtonOffset.y).toInt()
)
},
onClick = {
text = "onClick"
},
onLongClick = {
text = "onLongClick"
}
) {
Icon(imageVector = Icons.Filled.Menu, contentDescription = null)
}
}
}
//Without hack
Box {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
label = { Text(text = "Without hack") },
value = text,
onValueChange = { text = it },
trailingIcon = {
IconButton2(
onClick = {
text = "onClick"
},
onLongClick = {
text = "onLongClick"
}
) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = null
)
}
},
colors = colors
)
}
}
}
}
}
}
}
private val RippleRadius = 24.dp
private val IconButtonSizeModifier = Modifier.size(48.dp)
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun IconButton2(
modifier: Modifier = Modifier,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: #Composable () -> Unit
) {
Box(
modifier = modifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = RippleRadius)
)
.then(IconButtonSizeModifier),
contentAlignment = Alignment.Center
) {
val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
}
}