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)
}
}
}
Related
I have a similar search bar I pulled out from this other stackoverflow question. I, however changed lots of it and ended up with a much simpler SearchTextField composable.
#Composable
fun SearchTextField(
text: String,
placeholder: String,
modifier: Modifier = Modifier,
onTextChange: (String) -> Unit = { },
onIconClick: () -> Unit = { },
onFocusChange: (Boolean) -> Unit = { }
) {
val focusRequester = remember { FocusRequester() }
val isTextEmpty = text.isEmpty()
TextField(
value = text,
maxLines = 1,
singleLine = true,
placeholder = { Text(text = placeholder, overflow = TextOverflow.Visible) },
onValueChange = onTextChange,
shape = RoundedCornerShape(percent = 50),
trailingIcon = { IconDecoration(isTextEmpty = isTextEmpty, onClick = onIconClick) },
colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent),
modifier = modifier
.wrapContentHeight()
.padding(top = 0.dp, bottom = 0.dp, start = 0.dp, end = 0.dp)
.onFocusChanged { onFocusChange(it.isFocused) }
.focusRequester(focusRequester))
}
#Composable
fun IconDecoration(
isTextEmpty: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier) {
IconButton(onClick = onClick, modifier = modifier) {
Icon(
imageVector = when {
isTextEmpty -> { Icons.Default.Search }
else -> { Icons.Default.Close }
},
contentDescription = null,
tint = Color.DarkGray)
}
}
And from the the previews I get, it looks just as expected:
However, when I try to implement it on a TopAppBar title, it looks like the inner text field is not vertically aligned correctly. Below is an example of the implementation and its preview. I noticed this happens due to the SearchTextField padding modifier.
I do need that padding, but there is nowhere to set that in my current TextField, since the current modifier affects the whole composable. Where am I wrong?
#Composable
fun TopBar(
state: TopBarState,
modifier: Modifier = Modifier
) {
TopAppBar(
modifier = modifier,
title = {
SearchTextField(
text = state.query,
placeholder = "Search",
onTextChange = { query ->
state.query = query
state.suggestions = state.onSearchSuggestions(query)
},
onFocusChange = { state.isSearchFocused = it },
onIconClick = { state.query = "" },
modifier = Modifier
.padding(vertical = 8.dp) // This is causing the overflow
.wrapContentHeight(Alignment.Top)) // This won't work
},
navigationIcon = {
NavigationIcon(
isSearchFocused = state.isSearchFocused,
onMenuClick = state.onMenuClick,
onBackClick = { state.isSearchFocused = false })
},
actions = {
ActionItems(
pages = arrayListOf(Page.Notifications),
onNavigate = state.onNavigate)
})
}
#Composable
fun NavigationIcon(
isSearchFocused: Boolean,
onMenuClick: () -> Unit,
onBackClick: () -> Unit
) {
if(isSearchFocused) {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null)
}
} else {
IconButton(onClick = onMenuClick) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = null)
}
}
}
#Composable
fun ActionItems(
pages: List<Page>,
onNavigate: (Page) -> Unit
) {
pages.forEach { page ->
IconButton(onClick = { onNavigate(page) }) {
Icon(
imageVector = page.icon,
contentDescription = null)
}
}
}
I want to do something like this:
Where there is a view that pops on top of the open keyboard.
I've tried to do this, and I have this so far:
However, when I put this view in a Box, as the second view, the first view is also pushed up, here's the code:
#OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
#ExperimentalComposeUiApi
fun Modifier.bringIntoViewAfterImeAnimation(): Modifier = composed {
var focusState by remember { mutableStateOf<FocusState?>(null) }
val relocationRequester = remember { BringIntoViewRequester() }
val isImeVisible = WindowInsets.isImeVisible
LaunchedEffect(
isImeVisible,
focusState,
relocationRequester
) {
if (isImeVisible && focusState?.isFocused == false) {
relocationRequester.bringIntoView()
}
relocationRequester.bringIntoView()
}
bringIntoViewRequester(relocationRequester).onFocusChanged { focusState = it }
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun SpaceCreator(navController: NavController) {
Column(
modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(10.dp)),
verticalArrangement = Arrangement.Bottom
) {
SpaceCreatorContainer()
}
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun SpaceCreatorContainer() {
Card(
modifier = Modifier
.bringIntoViewAfterImeAnimation()
.shadow(elevation = 10.dp, shape = RoundedCornerShape(10.dp), clip = true)
.background(color = colors.background)
) {
SpaceCreatorWrapper()
}
}
#Composable
fun SpaceCreatorWrapper() {
val localFocusManager = LocalFocusManager.current
Column(
modifier = Modifier.padding(15.dp).clip(RoundedCornerShape(10.dp))
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Top Text"
)
Text(text = "Content")
OutlinedTextField(
value = "ss",
onValueChange = { },
label = { Text("Email Address") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = { localFocusManager.clearFocus() }
)
)
OutlinedTextField(
value = "ss",
onValueChange = { },
label = { Text("Email Address") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { localFocusManager.clearFocus() }
)
)
Text(text = "Content")
}
}
That's the code for SpaceCreator(), and then I add it to a Box where it's supposed to float over another view, like so:
BoxWithConstraints(
propagateMinConstraints = true
) {
Box(
modifier = Modifier
.fillMaxSize()
) {
MainView()
SpaceCreator(navController = navController)
}
}
How do I only push up the second view in the Box when the keyboard is opened, and not the entire box?
Also,
Currently, I have a AnimationNavigation screen which is a Box(fillMaxSize), and the content of the keyboard modal stuck to the bottom. I also have a auto focus on the input, which is making the navigation a bit slow, unlike other apps I've seen.
Is there a smooth way to do this on low-mid-range devices like Samsung A23?
Thank you.
In a compose desktop application I'm displaying an AlertDialog with rounded corners shape but there still appears a white rectangle at the corners.
Here is my code:
AlertDialog(
modifier = Modifier
.size(280.dp, 260.dp)
.shadow(elevation = 20.dp),
onDismissRequest = {},
buttons = {
Button(
modifier = Modifier.padding(start = 100.dp, top = 0.dp),
onClick = { onClose() }
) {
Text(
text = "OK",
textAlign = TextAlign.Center
)
}
},
title = {
Text(
"A Title"
)
},
text = {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Some Text")
}
},
shape = RoundedCornerShape(24.dp),
backgroundColor = Color.Red
)
How can I get rid of the background white corners that are visible behind the dialog.
#Composable
fun CustomAlertDialog(showingDialog: Boolean = false, onClickButton: () -> Unit) {
val color = if (isSystemInDarkTheme()) WhiteColor else Black90
val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
AlertDialog(
modifier = Modifier.clip(RoundedCornerShape(12.sdp)), // corner rounded
onDismissRequest = {
openDialog.value = false
},
title = {
Text(text = "Title")
},
text = {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center
) {
Text(stringResource(R.string.send_password_email), color = color)
}
},
buttons = {
Row(
modifier = Modifier.padding(all = 8.sdp),
horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.fillMaxWidth()
.clickable { openDialog.value = false }, text = "Ok", color = ColorMain, textAlign = TextAlign.Center
)
}
}
)
}
}
I was having an issue with .clip() not working because of the fact that Order of modifiers matters, the .clip() effect was being overwritten by my .background() modifier.
This doesn't work:
modifier = Modifier
.background(MyTheme.colors.background) // overwrites .clip()
.clip(RoundedCornerShape(28.dp)), // .clip() evaluated first
This does work:
modifier = Modifier
.clip(RoundedCornerShape(28.dp)) // then we can safely .clip()
.background(MyTheme.colors.background), // .background() evaluated first
Here is what the AlertDialog code looks like:
AlertDialog(
modifier = Modifier
.clip(RoundedCornerShape(28.dp))
.background(MyTheme.colors.background),
onDismissRequest = { /* On Dismiss */ },
text = { /* Text */ },
buttons = { /* Buttons */ },
title = { /* Title * / },
)
Just remember that the order of the modifiers is of significant importance.
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)
}
}
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)
}
}
}
)