Is it possible to get value of clicked Text using Modifier.clickable? - kotlin

This is my scenario, I need to get text value of clicked text:
#Composable
fun keyboard(){
val letters: Array<Pair<String, String>> = letterMap.toList().toTypedArray();
val txtModifier = Modifier
.clickable(onClick = { btnTapAction( /* send a letter */ ) })
for(i in 0..3)
Row(){
for(j in (i*8)..(i*8+7))
if (j<30)
Text(letters[j].first, txtModifier)
}
}
I have 30 Text's, each containing one cyrillic letter, they act like keyboard buttons, and I want to send clicked letter to function btnTapAction() which handles entered letters.
I have looked at documentation and I could not find a way to do it, but I hope it can be done?

If you wish to return value of clicked item create a separate Composable using state-hoisting
#Composable
fun Keyboard(){
val letters: Array<Pair<String, String>> = letterMap.toList().toTypedArray();
val txtModifier = Modifier// You can do some styling here such as rounded shape, etc
for(i in 0..3)
Row(){
for(j in (i*8)..(i*8+7))
if (j<30)
Letter( txtModifier, letters[j].first){char:String->
// You can pass this letter from keyboard to parent Composable using another callback or process here
}
}
}
#Composable
private fun Letter(modifier: Modifier = Modifier, char: String, onClick: (String) -> Unit) {
Text(text = char,
modifier = modifier.clickable {
onClick(char)
}
)
}
Edit
Initial question was like a question to get position of a letter in a Text, you wish to get position of any character inside a Text you can use answer below
You can use ClickableText Composable to get position of click
val context = LocalContext.current
val text = "Some clickable text"
ClickableText(
text = buildAnnotatedString {
append(text)
},
onClick = { position: Int ->
Toast.makeText(
context,
text.substring(startIndex = 0, endIndex = position),
Toast.LENGTH_SHORT
).show()
}
)

Related

How to prevent the user from entering two dots in a text field jetpack compose

I have a Text Field in android jetpack compose, which used to get number from user including floating points. How to prevent user from entering 2 dots in the input.
For Eg: Number should be 56.54,.266, 367.5
Should not be like this: 34.3.5
You can use onValueChange of TextField to filter the text:
#Composable
fun MyTextField(
) {
var text by remember { mutableStateOf("") }
TextField(
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
value = text,
onValueChange = {
val newString = it.filter { char ->
char == ".".first()
}
if (newString.length <= 1)
text = it
}
)
}

Can I change the value of a component from a separate button in Compose Multiplatform?

I am trying to make a desktop application that allows you to search through a number of predefined locations stored in Kotlin classes in a separate directory. To accomplish this, I've used the reflections and compose-jb libraries.
The problem I've run into is that I can't figure out how to update a Column of Boxes (located in another Box component) to change when I press the search button after entering tags that I want to search by.
My code is below (for the Main.kt file) that describes the entire desktop application.
val reflections = Reflections("io.github.mobomega.project.attractions")
var display = mutableSetOf<Attraction>()
fun main() = application {
val stateVertical = rememberScrollState(0)
val stateHorizontal = rememberScrollState(0)
var state = Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
)
Window(
onCloseRequest = ::exitApplication,
title = "Search",
state = rememberWindowState(width = 2256.dp, height = 1504.dp)
) {
val count = remember { mutableStateOf(1) }
MaterialTheme {
Column {
val text = remember { mutableStateOf("") }
OutlinedTextField(
value = text.value,
singleLine = true,
onValueChange = { text.value = it },
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Row (modifier = Modifier.size(2256.dp, 50.dp), horizontalArrangement = Arrangement.Center) {
Button(modifier = Modifier.align(Alignment.Top),
onClick = {
val tags = text.value.split(", ", ",")
for (tag in tags) {
search(tag.lowercase())
println("$display have tag $tag")
}
// Setting the new value of the Box
state = create(stateVertical, stateHorizontal)
// Creates error:
// "#Composable invocations can only happen from the context of a #Composable function"
}) {
Text("Search")
}
Button (modifier = Modifier.align(Alignment.Top),
onClick = {
display.clear()
}) {
Text("Reset")
}
}
Row (horizontalArrangement = Arrangement.Center) {
Box(
modifier = Modifier.fillMaxSize()
.background(color = Color(red = 0xFF, green = 0xFF, blue = 0xFF))
.padding(10.dp)
) {
state // Creating the state Box component in the Row
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical)
)
HorizontalScrollbar(
modifier = Modifier.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(end = 12.dp),
adapter = rememberScrollbarAdapter(stateHorizontal)
)
}
}
}
}
}
}
#Composable
fun textBox(text: String = "Item") {
Box(
modifier = Modifier.height(32.dp)
.width(400.dp)
.background(color = Color(200, 0, 0, 20))
.padding(start = 10.dp),
contentAlignment = Alignment.CenterStart
) {
Text(text = text)
}
}
#Composable
fun create(stateVertical: ScrollState, stateHorizontal: ScrollState) = Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
) {
Column {
var x = 0
for (attr in display) {
x++
textBox(attr.name)
if (x < display.size) {
Spacer(modifier = Modifier.height(5.dp).align(Alignment.CenterHorizontally))
}
}
}
}
fun search(text: String) {
for (attr in reflections.getSubTypesOf(Attraction::class.java)) {
val temp = attr.getConstructor().newInstance()
println("${temp.name} has tags ${temp.tags}")
if (temp.matches(text) && (temp !in display)) {
display += temp
}
}
}
I have tried to update the value of the Box that contains all of the items that match any of the search criteria, but I have run into a number of issues, such as the "onClick" function in which I set the new value of the "state" variable (storing all of the matching items) not being a Composable function, and therefore I can't change the value.
How would I accomplish changing the value of a Component such as a Box from another Component, such as a Button?
In Compose you can't create a view like you're doing with state variable. Result of your call is just Unit, and when you later call it you should see a warning "The expression is unused". The view is added at the tree hierarchy at the moment your variable is created.
To solve your problem you need to declare display as a mutable state - it's a new thing made especially for Compose, which allows triggering recomposition when this state changes:
val display by mutableStateOf<Attraction>(setOf())
And then update like this in your search:
val mutableDisplay = mutableSetOf<Attraction>()
// for
// ...
mutableDisplay += temp
// ...
display = mutableDisplay
Note that you can't use mutable set inside your mutable state, as mutable state won't be able to track changes of this set.
To learn more about state in Compose I suggest you checking this youtube video which explains the basic principles, and Compose mental model for better understanding of how to work with it.

Why needn't the author to wrap text.isNotBlank() with remember?

The Code A is from the official Sample project here.
1: I think I can wrap text.isNotBlank() withremember, so I think Code B is good, right?
BTW, I know the system will re-calculate when the text ( val (text, setText) = remember { mutableStateOf("") } ) is changed. So
2: In Code B, val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)} and val iconsVisible = remember {text.isNotBlank() } will be re-launched when the text is changed (val (text, setText) = remember { mutableStateOf("") }) , right?
Code A
#Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = text.isNotBlank()
Column {
Row( /* ... */ ) {
/* ... */
}
if (iconsVisible) {
AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
} else {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
Code B
#Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
val iconsVisible = remember {text.isNotBlank() } //I add remember
Column {
...
}
}
If you use remember as in Code B, iconsVisible will be calculated only once and the same value will be used across all recompositions and not get updates when text changes, which is not what we want here.
If you want to use remember here, you should pass text as a key to it, remember(text) { text.isNotBlank() }. But as this is not a time consuming calculation, you can just skip the remember block and use it as in Code A. The .isNotBlank() function will be invoked in every recomposition but that doesn't matter much here.

Should I use remember with animateDpAsState?

The Code A is from the official sample code here.
I know that in order to preserve state across recompositions, remember the mutable state using remember.
I think that the code val extraPadding by animateDpAsState(...) should be val extraPadding by remember { animateDpAsState(...) }, is it right?
BTW, val extraPadding by remember { animateDpAsState(...) } will cause the error
Composable calls are not allowed inside the calculation parameter of inline fun remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
Code A
#Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState( //Should I add remember
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
No, you shouldn't. This function is marked with #Composable so it should be used directly in the view builder.
animateDpAsState will calculate its value depending on targetValue on each recomposition.
If you check it source code you'll see, that it uses remember inside, that's why it's marked with #Composable, and that's why you shouldn't bother about remembering some values manually.
For anyone that comes across this in the future, instead of attempting to remember the animateDpAsState it would be better to remember the expansion state. Because in this case we're trying to remember the state in an item of a list we use rememberSaveable:
var expanded by rememberSaveable { mutableStateOf(false) }
A very similar case of storing indexes across list items is shown in the official documentation here

Kotlin Compose Search Bar

I have some doubts how to realize Search Bar in my Compose application. I call my SeachView function from ContactContent function where I pass state value.
#Composable
fun ContactContent(navigateToProfile: (Contact) -> Unit) {
val contacts = remember { DataProvider.contactList }
val textState = remember { mutableStateOf(TextFieldValue("")) }
Column(){
SearchView(textState)
LazyColumn() {
items(
items = contacts,
itemContent = {
ContactListItem(contact = it, navigateToProfile)
}
...
}
In SearchView I not sure how should I call onImeActionPerformed search state as mine search state is not be recognized.
#Composable
fun SearchView(state: MutableState<TextFieldValue>) {
Surface(){
TextField(
value = state.value,
onValueChange = { value -> state.value = value},
leadingIcon = {...},
keyBoardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search
),
onImeActionPerformed = {action, softKeyboardController -> if (action == ImeAction.Search){
>HERE IS WHERE I AM NOT SURE WHAT TO DO<
DataProvider.newSearch(textState)
}
...
}
newSearch function snippet
fun newSearch (textState: MutableState<String>){
val result = repository.search(
token = token,
page = 1,
query = "chicken"
)
DataProvider.value = result
}
Maybe you have a different solution how to realize the search bar from the list with with Kotlin Compose.
Do not use it directly in the parenthesis of the onImeActionPerformed, extract it in the parent Composable right before calling the TextField. Store it in a val, then use that val inside your onImeActionPerformed. Alogside, I assume you are creating that parameter with something like () -> Unit, ok so I'm not sure of this, but I think changing that to #Composable () -> Unit, you can access it directly without extracting it in a val first. Try it out