Jetpack Compose: TextField aligns outside of AlertDialog as text grows - kotlin

If you run the following composable and enter a long multiline text into the textfield, you will see that as the text grows, the textfield leaves the AlertDialog.
Is there a way to fix this?
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
#Preview
#Composable
fun MyComposable() {
var text by remember {
mutableStateOf("Press enter a couple of times to see the problem")
}
AlertDialog(
onDismissRequest = { },
title = {
OutlinedTextField(
value = text,
onValueChange = { text = it },
textStyle = MaterialTheme.typography.h5,
label = { Text(text = "Text") },
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
},
confirmButton = {
TextButton(
onClick = {}
) {
Text("Done")
}
},
dismissButton = {
TextButton(
onClick = { }
) {
Text("Cancel")
}
}
)
}

Modifier.heightIn constrain the height of the content to be between mindp and maxdp as permitted by the incoming measurement Constraints. If the incoming constraints are more restrictive the requested size will obey the incoming constraints and attempt to be as close as possible to the preferred size.
Column {
var text by remember { mutableStateOf("some text")}
OutlinedTextField(value = text,
onValueChange = {text = it},
textStyle = MaterialTheme.typography.headlineSmall,
label = { Text(text = "Text") },
modifier = Modifier.fillMaxWidth()
.heightIn(min = 0.dp, max = 150.dp))
}
Textfield will grow to max height specified in heightIn modifier and then start scrolling.

Related

How can I position snackbar on top of each other with floating action button in kotlin compose?

I have been doing dictionary app for a while. when I delete dictionary snackBar shows and writes dictionary is deleted but there is a floating action button and when the snackBar appears on the screen ,the snackbar appears above the floating action button, I don't want it to appear on it. It just stays on the screen for 1-2 seconds. I want the floating action button and snackbar to appear on top of each other. I couldn't adapt this to my own code. How can I do it ? I will share my code and image
CreateYourOwnDictionaryScreen
#Composable
fun CreateYourOwnDictionaryScreen(
navController: NavController,
viewModel: CreateYourOwnDictionaryViewModel = hiltViewModel()
) {
val scaffoldState = rememberScaffoldState()
val state = viewModel.state.value
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
backgroundColor = bar,
title = {
androidx.compose.material3.Text(
text = "your dictionaries",
modifier = Modifier.fillMaxWidth(),
color = Color.White,
fontSize = 22.sp
)
},
navigationIcon = {
IconButton(onClick = {
navController.navigate(Screen.MainScreen.route)
}) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Go Back"
)
}
}
)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
FloatingActionButton(
onClick = { navController.navigate(Screen.CreateDicScreen.route) },
backgroundColor = bar,
) {
Icon(Icons.Filled.Add, "fab")
}
}
) {
Box(modifier = Modifier.background(MaterialTheme.colors.background)) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(state.dictionaries) { dictionary ->
CreateYourOwnDictionaryItem(
dictionary = dictionary,
modifier = Modifier
.fillMaxWidth()
.clickable {
},
onDeleteClick = {
viewModel.onEvent(
CreateYourOwnDictionaryEvents.DeleteDictionary(dictionary)
)
scope.launch {
val result = scaffoldState.snackbarHostState.showSnackbar(
message = "dictionary is deleted",
actionLabel = "Undo",
duration = SnackbarDuration.Short
)
}
},
onEditClick = {
})
}
}
}
}
}
}
Image
I'm afraid this is difficult in Compose, you can dig the ScaffoldLayout and you'll find this code block.
val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
} else {
0
}
Scaffold will always offset the snackbar on top of a fab or a bottombar
And based on the answer in this post regarding material specs
This is specifically one of the "Don't" examples from the Material guidelines: https://material.io/components/snackbars#behavior
Making sure visual elements don't move out from underneath (say, when users are about to tap the FAB) is one of the key points to making a predictable UI
Also based on the Material Guidelines
Consecutive snackbars should appear above persistent bottom navigation

Push Front View up with Keyboard, don't push Background View up in Kotlin Compose

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.

How to vertically align the text and leading/trailing icons in a TextField

Is there a way to set the trailing/leading icons and the text on the same level? On default it is not as shown in the image below. I tried changing the fontStyle parameter in TextField but no success. The text is shown higher than the icons
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Icon
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication {
var text by remember { mutableStateOf("TEXT") }
OutlinedTextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.fillMaxWidth(),
leadingIcon = {
Icon(contentDescription = null,
imageVector = Icons.Default.ArrowForward)
},
trailingIcon = {
Icon(contentDescription = null,
imageVector = Icons.Default.ArrowForward)
})
}
There are several solutions. For desktop Compose, offset the icons. For mobile Compose, adjust the baseline. And finally, just create a custom TextField.
The icons may have extra padding at the bottom. It could also be that the icons and text are always aligned to the top. You could replace the icons with ones that have no bottom padding or shift the icons up. You can tell that the icons are affecting the vertical alignment because if you comment out your icons, the text does center vertically. You could also try reducing the size of the icons.
On desktop Compose, shift the icons up:
OutlinedTextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.fillMaxWidth(),
leadingIcon = {
Icon(
modifier = Modifier.offset(y = -3.dp),
contentDescription = null,
imageVector = Icons.Default.ArrowForward
)
},
trailingIcon = {
Icon(
modifier = Modifier.offset(y = -3.dp),
contentDescription = null,
imageVector = Icons.Default.ArrowForward
)
})
On mobile devices, you can adjust the baseline of the text:
OutlinedTextField(
value = text,
textStyle = TextStyle(baselineShift = BaselineShift(-0.2f)),
onValueChange = { text = it },
modifier = Modifier.fillMaxWidth(),
leadingIcon = {
Icon(
contentDescription = null,
imageVector = Icons.Default.ArrowForward
)
},
trailingIcon = {
Icon(
contentDescription = null,
imageVector = Icons.Default.ArrowForward
)
})
Here is also a custom TextField:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CustomTextField(
initialText = "cool",
onValueChange = {
},
onLeftButtonClick = {
},
onRightButtonClick = {
}
)
}
}
}
#Composable
fun CustomTextField(
initialText: String,
onValueChange: (text: String) -> Unit,
onLeftButtonClick: () -> Unit,
onRightButtonClick: () -> Unit
) {
var text by remember { mutableStateOf(initialText) }
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize()
.background(color = Color.White, shape = RoundedCornerShape(8.dp))
.border(width = 1.dp, shape = RoundedCornerShape(8.dp), color = Color(0xFF585858))
) {
ConstraintLayout(
modifier = Modifier.fillMaxWidth()
) {
val (left, mid, right) = createRefs()
IconButton(onClick = onLeftButtonClick,
modifier = Modifier.constrainAs(left) {
start.linkTo(parent.start, margin = 10.dp)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}) {
Icon(
contentDescription = null,
imageVector = Icons.Default.ArrowForward,
)
}
IconButton(onClick = onRightButtonClick,
modifier = Modifier.constrainAs(right) {
end.linkTo(parent.end, margin = 10.dp)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}) {
Icon(
contentDescription = null,
imageVector = Icons.Default.ArrowForward,
)
}
TextField(
value = text,
onValueChange = {
text = it
onValueChange(it)
},
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.White
),
modifier = Modifier
.offset(y = 4.dp)
.constrainAs(mid) {
start.linkTo(left.end, margin = 10.dp)
top.linkTo(parent.top)
end.linkTo(right.start, margin = 10.dp)
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints
})
}
}

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)
}
}

Box doesn't capture key events in Compose Desktop

Keys are printed when the TextField is focused but not when the Box itself if focused.
Box(
modifier = Modifier.onKeyEvent {
println(it.key)
false
}.fillMaxSize().focusable()
) {
val fieldValue = remember { mutableStateOf(TextFieldValue("")) }
TextField(
value = fieldValue.value,
onValueChange = { fieldValue.value = it }
)
}
Inspired by this answer, I change the code.
When you click on the Box, you remove the focus from the TextField but you don't give it to the Box. This has to be done manually.
val focusRequester = FocusRequester()
Box(
modifier = Modifier.onKeyEvent {
println(it.key)
false
}.fillMaxSize()
.focusRequester(focusRequester)
.focusable()
.clickable (
interactionSource = remember { MutableInteractionSource() },
indication = null // To disable the ripple effect
) {
focusRequester.requestFocus()
}
) {
val fieldValue = remember { mutableStateOf(TextFieldValue("")) }
TextField(
value = fieldValue.value,
onValueChange = { fieldValue.value = it }
)
}