Multi style text editing for TextField composable in android jetpack compose - android-jetpack-compose

I want to change the text style (fontSize , color , fontWeight , ...) of a selected text in a TextFiled() composable , with a button in android jetpack compose.
(The main problem is, when i change the text style of a selected text ,the TextField can not save it , or when i add/remove a letter in TextField , the TextField deletes the previous text style.)
In other words, when the recomposition process occurs, the text styles disappears in the TextField()
my code is :
#Composable
fun Show() {
val inputText = remember{ mutableStateOf(TextFieldValue("This is a annotated text text"))}
Column(
modifier = Modifier.fillMaxSize().padding(5.dp) ,
horizontalAlignment = Alignment.CenterHorizontally
) {
//=================== TextField
CustomTextField(textInput = inputText)
//==================== Button
Button(onClick = {
inputText.value = changeSegmentColor(inputText.value)
}) {
Text(text = "Change the text style of selected text")
}
//======================
}
}
#Composable
fun CustomTextField (
textInput:MutableState<TextFieldValue>
) {
TextField(
value = textInput.value , onValueChange = {
textInput.value = it
},
modifier = Modifier.fillMaxWidth().heightIn(min = 200.dp) ,
)
}
private fun changeSegmentColor(textFVal: TextFieldValue):TextFieldValue{
val txtAnnotatedBuilder = AnnotatedString.Builder()
val realStartIndex = textFVal.getTextBeforeSelection(textFVal.text.length).length
val endIndex = realStartIndex + textFVal.getSelectedText().length
txtAnnotatedBuilder.append(textFVal.annotatedString)
val myStyle = SpanStyle(
color = Color.Red ,
fontSize = 16.sp ,
background = Color.Green
)
txtAnnotatedBuilder.addStyle(myStyle ,realStartIndex ,endIndex)
return textFVal.copy(annotatedString = txtAnnotatedBuilder.toAnnotatedString())
}

Related

Bug in default behavior of ScalingLazyColumn (Jetpack Compose Wear OS)

I'm using ScalingLazyColumn with a very long Text inside as follows:
#Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
#Composable
fun Test(modifier: Modifier = Modifier) {
val scalingLazyState = remember { ScalingLazyListState() }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = modifier,
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
state = scalingLazyState,
) {
item {
Text(
longText,
Modifier
.padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
}
val longText =
"Take the plunge\n" +
"\n" +
"commit oneself to a course of action about which one is nervous.\n" +
"\n" +
"\"she wondered whether to enter for the race, but decided to take the plunge\"\n" +
"\"They're finally taking the plunge and getting married.\"\n" +
"\n" +
"\n" +
"plunge:\n" +
"jump or dive quickly and energetically.\n" +
"\"our daughters whooped as they plunged into the sea\"\n"
But for some reason when I launch the app the focus goes to the bottom of the text, instead of the beginning, which looks like a bug. I've tried playing with different parameters of ScalingLazyColumn (anchorType, autoCentering, scalingParams) to no avail.
Any idea how to fix it and make the ScalingLazyColumn focus on the beginning of the first element when I launch the app?
Switching off autoCentering is an option, but I would try and avoid it in most cases as it will will make handling getting the padding right on different devices sizes more difficult and often results in being able to over scroll the list items either at the beginning or the end.
I am not sure exactly what you want to achieve when you say that you want the focus to be on the start of the first item but the following should give you what you need.
Set the state initial item to 0
Set the anchor type to ScalingLazyListAnchorType.ItemStart
Remove top padding from your item
Apply an offset to the state initialItem initialCenterItemScrollOffset to shift the start of you item up a little.
Optionally adjust the autoCentering to make sure that the limit of the scrolling matches the initial position selected in the state
#Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
#Composable
fun SingleItemSLCWithLongText(modifier: Modifier = Modifier) {
val scalingLazyState = remember { ScalingLazyListState(initialCenterItemIndex = 0, initialCenterItemScrollOffset = 80) }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = modifier.background(Color.Black),
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
autoCentering = AutoCenteringParams(itemIndex = 0, itemOffset = 80),
state = scalingLazyState,
anchorType = ScalingLazyListAnchorType.ItemStart
) {
item {
Text(
longText,
Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
}
Here is a screenshot of how the screen initially looks
Initial screen
This test activity let's you play with all the params to see starting position
https://github.com/google/horologist/blob/a1241ff25b7008f7c1337f4425b98d14ce30d96d/sample/src/main/java/com/google/android/horologist/scratch/ScratchActivity.kt
After a few hours of frustration I finally found a solution.
If you read the documentation for ScalingLazyColumn it says:
"If the developer wants custom control over position and spacing they
can switch off autoCentering and provide contentPadding."
So all you need to do is to just add autoCentering = null in ScalingLazyColumn.
This is a working code where the focus will be in the beginning of the Text:
val scalingLazyState = remember { ScalingLazyListState() }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = modifier,
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
state = scalingLazyState,
autoCentering = null,
) {
item {
Text(
longText,
Modifier
.padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}
ScalingLazyListState defaults to the center of the second item (index 1). You can tell it to instead start in the first item and even jn the ScalingLazyColumn parameters use the start of items.
val scalingLazyState = remember { ScalingLazyListState(initialCenterItemIndex = 0) }
val focusRequester = remember { FocusRequester() }
Scaffold(
modifier = Modifier,
positionIndicator = { PositionIndicator(scalingLazyListState = scalingLazyState) }
) {
ScalingLazyColumn(
modifier = Modifier.scrollableColumn(focusRequester, scalingLazyState),
state = scalingLazyState,
anchorType = ScalingLazyListAnchorType.ItemStart
) {
item {
Text(
longText,
Modifier
.padding(top = 20.dp, start = 16.dp, end = 16.dp, bottom = 48.dp),
textAlign = TextAlign.Center,
)
}
}
}

Why is MediumTopAppBar (and Large) showing two TextField in compose?

I am trying to make the title of a screen editable.
MediumTopAppBar(
title = {
val name: String? = "Some Title"
var input by remember { mutableStateOf(name ?: "") }
when (state.isEditingTitle) {
true ->
TextField(
value = input,
onValueChange = { input = it },
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
callbacks.onEditTitleChange(editTitle = false, updatedTitle = input)
})
)
false -> {
Text(
modifier = Modifier.clickable { callbacks.onEditTitleChange(true, null) },
text = name ?: "(No Title)"
)
}
}
},
... more app bar parameters
}
When I click on the title Text(...) and the view gets recomposed the AppBar shows two TextFields
How do I ignore the top one and only show the one in the bottom, like the Text() is only shown in the bottom?
(Fyi: the two TextInputs have their own remembered state and calls the callback with their own respective value)
Bonus question: How do I handle the remembered state "input" so that it resets every time the onDone keyboard action is triggered? Instead of val name: String? = "Some Title" it would of course be something in the line of val name: String? = state.stateModel.title
I found out why it does this, but I have no idea how to solve it (except for just making my own views and placing it close by)
It's easy to see when looking at the function for the MediumTopBar
// androidx.compose.material3.AppBar.kt
#ExperimentalMaterial3Api
#Composable
fun MediumTopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable () -> Unit = {},
actions: #Composable RowScope.() -> Unit = {},
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null
) {
TwoRowsTopAppBar(
modifier = modifier,
title = title,
titleTextStyle = MaterialTheme.typography.fromToken(TopAppBarMediumTokens.HeadlineFont),
smallTitleTextStyle = MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont),
titleBottomPadding = MediumTitleBottomPadding,
smallTitle = title, // <- this thing, right here
navigationIcon = navigationIcon,
actions = actions,
colors = colors,
windowInsets = windowInsets,
maxHeight = TopAppBarMediumTokens.ContainerHeight,
pinnedHeight = TopAppBarSmallTokens.ContainerHeight,
scrollBehavior = scrollBehavior
)
}
There's some internal state shenanigans going on, probably checking for a Text being shown in the 2nd TopAppBarLayout (more digging required to find that), but not for any other view.
TwoRowsTopAppBar and TopAppBarLayout are not public, and can't be used directly.
This is explains why, but it would be interesting to see how to solve it (still using Medium or Large -TopAppBar)
it is stupid thing devs overlooked and should be warned against, at least. The answer is do not give default colors to your Typography TextStyles.
private val BodySmall = TextStyle(
fontSize = 10.sp,
lineHeight = 12.sp,
fontWeight = FontWeight.SemiBold,
fontFamily = Manrope,
color = Color.Black // REMOVE THIS
)
val OurTypography = Typography(
...
bodySmall = BodySmall
)

How to handle error on outlined edit text checking regex in compose

I have this outlined edit text, and I want to display a red error color when user tries to put '#' on the outline edit text when they're trying to sign in. I was wondering how I can handle error on an edit text in jetpack compose
UserInputTextField(
fieldState = usernameState.value,
onFieldChange = { usernameState.value = it },
label = "Enter Name",
)
#Composable
fun UserInputTextField(
fieldState: String,
onFieldChange: (String) -> Unit,
label: String,
modifier: Modifier = Modifier,
) {
androidx.compose.material.OutlinedTextField(
value = fieldState, onValueChange = {
onFieldChange(it)
},
label = { androidx.compose.material.Text(text = label) },
modifier = modifier
.fillMaxWidth()
.padding(top = 16.dp)
.semantics { testTag = TestTags.LoginContent.USERNAME_FIELD },
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Blue,
unfocusedBorderColor = Color.Black
)
)
}
Use isError attribute of TextField.
Example
isError = fieldState.contains("#"),
Sample code for reference
#Composable
fun ErrorCheck() {
val (text, setText) = remember {
mutableStateOf("")
}
UserInputTextField(
fieldState = text,
onFieldChange = setText,
label = "Email",
)
}
#Composable
fun UserInputTextField(
fieldState: String,
onFieldChange: (String) -> Unit,
label: String,
modifier: Modifier = Modifier,
) {
androidx.compose.material.OutlinedTextField(
value = fieldState,
onValueChange = {
onFieldChange(it)
},
isError = fieldState.contains("#"),
modifier = modifier
.fillMaxWidth()
.padding(
all = 16.dp,
),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Blue,
unfocusedBorderColor = Color.Black,
)
)
}

How to Snap the Row/Column elements to end in JetPack Compose

I am using the below code to show the textviews, image and a textfield to show at each corners after the image (start and end). It is basically a Card with Image on the start and later a column with a two text views and with another column with a textview and basictextfield for input.
#Composable
fun BaseCard() {
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(Alignment.CenterVertically, true)
.wrapContentHeight()
.clip(RoundedCornerShape(8.dp)),
elevation = 10.dp,
backgroundColor = Color.White
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
)
{
Image(
modifier = Modifier
.size(35.dp, 35.dp) //50dp
.clip(RoundedCornerShape(8.dp))
.clickable {
navController.navigate("meta") {
launchSingleTop = true
}
},
painter = img,
alignment = Alignment.Center,
contentDescription = "",
contentScale = ContentScale.Crop,
)
Spacer(modifier = Modifier.width(8.dp))
Column(horizontalAlignment = Alignment.Start) {
Text(
text = "US Dollar" + "($)",
color = Color.Gray,
style = Typography.subtitle2
)
Text(
text = "USD",
Modifier.padding(0.dp),
color = MaterialTheme.colors.onPrimary,
style = Typography.h5,
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(horizontalAlignment = Alignment.End) {
Text(
text = "Amount",
color = Color.Gray,
style = Typography.subtitle2
)
BasicTextField(
value = typedValue,
onValueChange = {
----
},
textStyle = LocalTextStyle.current.copy(
fontFamily = NunitoFontFamily,
fontWeight = FontWeight.W600,
letterSpacing = 0.sp,
fontSize = 24.sp,
textAlign = TextAlign.End
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
)
)
}
}
}
}
However, it is showing up as below.
How to remove the extra space and align the elements properly at each end?
There are multiple ways to implement what you ask
One is using Spacer(modifier=Modifier.weight(1f) instead of
Spacer(modifier = Modifier.width(8.dp))
for the Spacer here, since it's inside a Row it will take all the space that don't have weight modifier.
// Change this Spacer's modifier to Modifier.weight(1f)
Spacer(modifier = Modifier.width(8.dp))
Column(horizontalAlignment = Alignment.End) {
Text(
text = "Amount",
color = Color.Gray,
style = Typography.subtitle2
)
BasicTextField(
value = typedValue,
onValueChange = {
----
},
textStyle = LocalTextStyle.current.copy(
fontFamily = NunitoFontFamily,
fontWeight = FontWeight.W600,
letterSpacing = 0.sp,
fontSize = 24.sp,
textAlign = TextAlign.End
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
)
)
}
Second one is to use a Box instead of Row and have 2 composables one aligned to
Box(modifier = Modifier
.fillMaxWidth()
.padding(8.dp)) {
Column(modifier = Modifier.align(Alignment.CenterStart)) {
}
Column(modifier = Modifier.align(Alignment.CenterEnd)) {
}
}

Making TextField Scrollable in Jetpack Compose

I am working in Android Jetpack Compose.
I have three TextField in a in a Column. I would like the third and/or last text field to be scrollable. How can I do that?
TextField example with horizontalScroll
#Composable
fun Test() {
TextField(
value = "long1 long2 long3 long4 long5 long6 long7 long8 long9 long10 long11 long12 text",
onValueChange = {},
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
)
}
TextField example with scrollable
#Composable
fun Test() {
var offset by remember { mutableStateOf(0f) }
TextField(value = "long1 long2 long3 long4 long5 long6 long7 long8 long9 long10 long11 long12 text",
onValueChange = {},
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.scrollable(
orientation = Orientation.Horizontal,
state = rememberScrollableState { delta ->
offset += delta
delta
}
)
)
}
more info there
https://developer.android.com/jetpack/compose/gestures?authuser=1#scrollable-modifier.

Resources