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)
}
}
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.
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)
}
}
}
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") }
)
}
Is it possible to center DropdownMenu in my example? or show it wherever I click or tap?
I tried alignment and arrangements and none of them work. I prefer showing the DropdownMenu wherever I tab but I couldn't find a way to do it.
fun main() = Window {
var helloText by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Item("Darian", "Russ") {
helloText = it
}
Item("Maynerd", "Andre") {
helloText = it
}
Item("Sandra", "Victoria") {
helloText = it
}
Spacer(modifier = Modifier.height(2.dp))
Text(text = helloText)
}
}
#Composable
fun Item(text: String, text2: String, onMenuTab: (String) -> Unit) {
var expanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val modifier = Modifier.clickable {
expanded = true
}
DropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
DropdownMenuItem(onClick = {
onMenuTab("hello $text $text2")
expanded = false
}, modifier = Modifier.align(Alignment.CenterHorizontally)) {
Text("Hello")
}
DropdownMenuItem(onClick = { /* Handle settings! */ }) {
Text("Settings")
}
Divider()
DropdownMenuItem(onClick = { /* Handle send feedback! */ }) {
Text("Send Feedback")
}
}
Text(text = text, modifier = modifier)
Text(text = text2, modifier = modifier)
Divider(modifier = Modifier.height(2.dp))
Spacer(modifier = Modifier.height(2.dp))
}
}
For me fixing the alignment of the popup did the trick-
Popup(alignment = Alignment.TopStart)
Earlier it was Popup(alignment = Alignment.CenterStart)
and that was taking my popup view to the top of the screen. But now comes below the clicked item.