How can I get the transformed value from VisualTransformation in Jetpack compose TextField? since the only thing that is changing is the visual text not the actual input/buffered text?
class CurrencyAmountInputVisualTransformation(
private val currencySymbol: String,
private val unbufferedValueChange: (String) -> Unit
) : VisualTransformation {
private val symbols = DecimalFormat().decimalFormatSymbols
private val numberOfDecimals: Int = 2
override fun filter(text: AnnotatedString): TransformedText {
val zero = symbols.zeroDigit
val inputText = text.text
val intPart = inputText
.dropLast(numberOfDecimals)
.reversed()
.chunked(3)
.joinToString(symbols.groupingSeparator.toString())
.reversed()
.ifEmpty {
zero.toString()
}
val fractionPart = inputText.takeLast(numberOfDecimals).let {
if (it.length != numberOfDecimals) {
List(numberOfDecimals - it.length) {
zero
}.joinToString("") + it
} else {
it
}
}
val formattedNumber = intPart + symbols.decimalSeparator.toString() + fractionPart
val value = inputText.dropLast(numberOfDecimals)
unbufferedValueChange("$value.$fractionPart")
val newText = AnnotatedString(
text = "$currencySymbol $formattedNumber",
spanStyles = text.spanStyles,
paragraphStyles = text.paragraphStyles
)
...
return TransformedText(newText, ...)
}
I'm getting duplicate re-composition because of this approach since I have 2 mutable state, one for the input and one for the value that I'm getting from a lambda callback I passed to the VisualTransformation.
#Composable
internal fun CurrencyField(
) {
val pattern = remember { Regex("^\\d*\\.?\\d*\$") }
var input by remember { mutableStateOf("") }
var amountInput by remember { mutableStateOf(0.00) }
OutlinedTextInputField(
text = input,
onTextChanged = {
if (it.isEmpty() || it.matches(pattern)) {
input = it
}
},
keyboardType = KeyboardType.NumberPassword,
visualTransformation = CurrencyAmountInputVisualTransformation("PHP") {
amountInput = it.toDouble()
}
)
}
Output:
Input screenshot
Is there any way I can get
232323.23
without using a callback in the VisualTransformation class?
Thanks.
You have to apply the same filter used by the VisualTransformation
var text by remember { mutableStateOf("") }
val visualTransformation = MyVisualTransformation()
TextField(
value = text,
onValueChange = { text = it },
visualTransformation = visualTransformation
)
val transformedText = remember(text, visualTransformation) {
visualTransformation.filter(AnnotatedString(text))
}.text.text
Related
I am trying to create a some dynamic views when the user clicks add button it starts a new set of text fields and fill the required details and add all the set to final list of array i facing a problem of how to deal with the mutable state of each text field i.e value and onValueChange the text fields are sharing same viewModel mutable state.
How to deal with the dynamic views so that only the required text field value changes instead of all similar text field changing
Ui code
#Composable
fun CombineFields(viewModel: MainContentUploadViewModel = hiltViewModel()) {
val containerTitle = viewModel.containerTitle.value
val containerAbout = viewModel.containerAbout.value
OutlinedTextField(value = containerTitle.innerStateTitle,
onValueChange = { viewModel.onEvent(MainContentEvent.ContainerTitle(it)) },
label = { Text(text = "Title") })
Spacer(modifier = Modifier.height(8.dp))
TextField(value = containerAbout.innerStateAbout,
onValueChange = { viewModel.onEvent(MainContentEvent.ContainerAbout(it)) },
modifier = Modifier.height(100.dp))
Spacer(modifier = Modifier.height(8.dp))
val options = listOf("Products", "Banners", "Categories")
var expanded by remember { mutableStateOf(false) }
var selectedOptionText by remember { mutableStateOf(options[0]) }
// We want to react on tap/press on TextField to show menu
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
TextField(
readOnly = true,
value = selectedOptionText,
onValueChange = { },
label = { Text("Label") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
options.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selectedOptionText = selectionOption
expanded = false
}
) {
Text(text = selectionOption)
}
}
}
}
}
ViewModel
#HiltViewModel
class MainContentUploadViewModel #Inject constructor(private val useCases: UseCases) : ViewModel() {
private val _containerTitle = mutableStateOf(MainContentFieldState())
val containerTitle : State<MainContentFieldState> = _containerTitle
private val _containerAbout = mutableStateOf(MainContentFieldState())
val containerAbout : State<MainContentFieldState> = _containerAbout
private val _containerPriority = mutableStateOf(MainContentFieldState())
val containerPriority : State<MainContentFieldState> = _containerPriority
private val _selectedContent = mutableStateOf(MainContentFieldState())
val selectedContent : State<MainContentFieldState> = _selectedContent
private val _selectedTags = mutableStateOf(MainContentFieldState())
val selectedTags : State<MainContentFieldState> = _selectedTags
private val _allInnerContent = mutableStateOf(MainContentFieldState())
val allInnerContent : State<MainContentFieldState> = _allInnerContent
Data Class
data class InnerContainerItems(
val containerName:String? = null,
val containerAbout:String? = null,
val containerTags:List<String>? = null,
val containerType:String? = null,
val containerPriority:Int? = null,
)
data class MainScreenContainer(
val ScreenContainer:List<InnerContainerItems>? = null
)
can anyone help me how to move further for the dynamic fields thanks.
As i can see you are using var selectedOptionText by remember { mutableStateOf(options[0]) }
I found this https://stackoverflow.com/a/68887484/20839582. Check if it is useful as it mentions using mutableStateListOf and rememberSaveable.
I am trying to add a some text fields compose dynamically. when clicking a add button a new compose fields appear and add some required texts my problem is text fields changes for all the layouts instead of the particular one. how can use the mutableStateOf for dynamic ones
my view
val containerTitle = viewModel.containerTitle.value
val containerAbout = viewModel.containerAbout.value
OutlinedTextField(value = containerTitle.innerStateTitle,
onValueChange = { viewModel.onEvent(MainContentEvent.ContainerTitle(it)) },
label = { Text(text = "Title") })
Spacer(modifier = Modifier.height(8.dp))
TextField(value = containerAbout.innerStateAbout,
onValueChange = { viewModel.onEvent(MainContentEvent.ContainerAbout(it)) },
modifier = Modifier.height(100.dp))
Spacer(modifier = Modifier.height(8.dp))
val options = listOf("Products", "Banners", "Categories")
var expanded by remember { mutableStateOf(false) }
var selectedOptionText by remember { mutableStateOf(options[0]) }
// We want to react on tap/press on TextField to show menu
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
TextField(
readOnly = true,
value = selectedOptionText,
onValueChange = { },
label = { Text("Label") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
options.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selectedOptionText = selectionOption
expanded = false
}
) {
Text(text = selectionOption)
}
}
}
}
viewModel
#HiltViewModel
class MainContentUploadViewModel #Inject constructor(private val
useCases: UseCases) : ViewModel() {
private val _containerTitle = mutableStateOf(MainContentFieldState())
val containerTitle : State<MainContentFieldState> = _containerTitle
private val _containerAbout = mutableStateOf(MainContentFieldState())
val containerAbout : State<MainContentFieldState> = _containerAbout
private val _containerPriority = mutableStateOf(MainContentFieldState())
val containerPriority : State<MainContentFieldState> = _containerPriority
private val _selectedContent = mutableStateOf(MainContentFieldState())
val selectedContent : State<MainContentFieldState> = _selectedContent
private val _selectedTags = mutableStateOf(MainContentFieldState())
val selectedTags : State<MainContentFieldState> = _selectedTags
private val _allInnerContent = mutableStateOf(MainContentFieldState())
val allInnerContent : State<MainContentFieldState> = _allInnerContent
fun onEvent(event: MainContentEvent){
when(event) {
is MainContentEvent.ContainerTitle -> {
_containerTitle.value = containerTitle.value.copy(innerStateTitle = event.value)
}
is MainContentEvent.ContainerAbout -> {
_containerAbout.value = containerAbout.value.copy(innerStateAbout = event.about)
}
is MainContentEvent.ContainerType -> {
_selectedContent.value = selectedContent.value.copy(selectedContent = event.value)
}
is MainContentEvent.ContainerTags -> {
_selectedTags.value = selectedTags.value.copy(selectedTagsList = event.containerTags)
}
is MainContentEvent.ContainerPriority -> {
_containerPriority.value = containerPriority.value.copy(containerPriority = event.value)
}
data class
data class InnerContainerItems(
val containerName:String? = null,
val containerAbout:String? = null,
val containerTags:List<String>? = null,
val containerType:String? = null,
val containerPriority:Int? = null,
)
data class MainScreenContainer(
val ScreenContainer:List<InnerContainerItems>? = null
)
How to solve the value of all text fields changing when one text is changed thanks
Probably system share one viewmodel to each containers, if you use separated viewmodels for them. You can try using the parent viewmodel to storage information. For this you should:
create a
data class ContainerData(
val title: String,
val description: String,
)
add to parent viewmodel
var containersData by mutableStateOf<List<ContainerData>>(emptyList())
get list from UI (if you will use delegate by and add imports, you can use field without .value when you set and get data)
val containersData = viewModel.containersData
use this data for getting information about container in the LazyColumn or simple Column by index
hand over this information to your Container
How can I add a suffix to TextField input that flows (moves) with the user input text?
With M3 starting from the 1.1.0-alpha06 you can use the suffix attribute:
TextField(
value = text,
onValueChange = { text = it },
suffix = { Text ("€") }
)
Before M3 1.1.0-alpha06 or with M2 or you can use the visualTransformation attribute.
Something like:
TextField(
value = text,
onValueChange = { text = it },
singleLine = true,
visualTransformation = SuffixTransformation(" €"),
)
class SuffixTransformation(val suffix: String) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val result = text + AnnotatedString(suffix)
val textWithSuffixMapping = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return offset
}
override fun transformedToOriginal(offset: Int): Int {
if (text.isEmpty()) return 0
if (offset >= text.length) return text.length return offset
}
}
return TransformedText(result, textWithSuffixMapping )
}
}
If you have the placeholder you can put a condition in the visualTransformation attribute.
Something like:
TextField(
value = text,
onValueChange = { text = it },
singleLine = true,
visualTransformation = if (text.isEmpty())
VisualTransformation.None
else
SuffixTransformation(" €"),
placeholder = { Text("Placeholder") }
)
I found Gabriele Mariotti's answer buggy. Needed to change transformedToOriginal function to this:
override fun transformedToOriginal(offset: Int): Int {
if (offset > text.length) return text.length
return offset
}
This is easily done in Compose like this:
const val SUFFIX = " $"
#Composable
fun SuffixedText() {
var text by remember { mutableStateOf("") }
TextField(
text,
singleLine = true,
visualTransformation = SuffixTransformer(SUFFIX),
onValueChange = { text = it }
)
}
class SuffixTransformer(val suffix: String) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val result = text + AnnotatedString(suffix)
return TransformedText(result, OffsetMapping.Identity)
}
}
The above component probably can be used in traditional Views too. See this post
Also, see the following:
How to add a prefix to a TextField in Compose?
GitHub issue to add flowing suffix in Views in Material components for Android
Put constant text inside EditText in View which should be non-editable
I have a Composable with a Boxand some text and it also returns a value
How to use that value
#Composable
fun dummyAndUselessUI(String:String) : String{
val text = remember { mutableStateOf("") }
Box(modifier = Modifier.size(100.dp)){ Text(String) }
return text.value
}
You don't need a function that return a value, in Compose you handle State
#Composable
fun dummyScreen() {
var text by rememberSaveable { mutableStateOf("") }
dummyAndUselessUI(text = text, onNameChange = { text = it })
}
#Composable
fun dummyAndUselessUI(text: String, onTextChange: (String) -> Unit) {
Box(modifier = Modifier.size(100.dp)){
OutlinedTextField(
value = text,
onValueChange = onTextChange,
label = { Text("Name") }
)
}
}
I am trying to create a reusable NumberField component:
#Composable
fun NumberField(
value: Number?,
onNumberChange: (Number) -> Unit,
) {
TextField(
value = value?.toString() ?: "",
onValueChange = {
it.toDoubleOrNull()?.let { value ->
if (value % 1.0 == 0.0) {
onNumberChange(value.toInt())
} else {
onNumberChange(value)
}
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
To be used as:
#Composable
fun NumberContent() {
val number = remember { mutableStateOf<Number?>(null) }
NumberField(value = number.value) {
number.value = it
}
}
I would like the number to be an Int or Double depending on what the user is typing. What I have above works until you try to enter a decimal number, as it seems "5.", does not parse as double. I want to allow the user to type 5. and then fill in rest. As such I don't want to add a zero after decimal automatically because that might not be the next number they want to enter. Is this the best way to go about it? I know that I can just accept any text and then try to format the text they entered later as an int or double and make them fix it then, just thought it would be nice to bundle it all in the composable.
You can use something like:
TextField(
value = text,
onValueChange = {
if (it.isEmpty()){
text = it
} else {
text = when (it.toDoubleOrNull()) {
null -> text //old value
else -> it //new value
}
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Here is an implementation that handles all stated conditions while also exposing the state to parents.
#Composable
fun NumberField(
value: Number?,
onNumberChange: (Number?) -> Unit,
) {
val number = remember { mutableStateOf(value) }
val textValue = remember(value != number.value) {
number.value = value
mutableStateOf(value?.toDouble()?.let {
if (it % 1.0 == 0.0) {
it.toInt().toString()
} else {
it.toString()
}
} ?: "")
}
val numberRegex = remember { "[-]?[\\d]*[.]?[\\d]*".toRegex() }
// for no negative numbers use "[\d]*[.]?[\d]*"
TextField(
value = textValue.value,
onValueChange = {
if (numberRegex.matches(it)) {
textValue.value = it
number.value = it.toDoubleOrNull()
onNumberChange(number.value)
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
An example usage is shown below.
#Composable
fun DemoUsage() {
Column {
val number = remember { mutableStateOf<Number?>(null) }
NumberField(value = number.value) {
number.value = it
}
Button(onClick = { number.value = number.value?.toDouble()?.plus(1) }) {
Text("Increment")
}
}
}