How to manage Focus state in Jetpack's Compose - android

I have a custom composable View (Surface + Text essentially) and I want to change the color of the surface depending on the focus state. The FocusManager#FocusNode is marked internal and I am unaware of any way to achieve this. Is this simply just not available yet? Any one else have to tackle this?

With 1.0.x you can use the Modifier.onFocusChanged to observe focus state events.
Something like:
var color by remember { mutableStateOf(Black) }
val focusRequester = FocusRequester()
Text(
modifier = Modifier
.focusRequester(focusRequester)
.onFocusChanged { color = if (it.isFocused) Green else Black }
.focusModifier()
.pointerInput(Unit) { detectTapGestures { focusRequester.requestFocus() } },
text = "Text",
color = color
)

As of dev11, FocusManagerAmbient has been deprecated in favor of FocusModifier. For more examples check out the KeyInputDemo, ComposeInputFieldFocusTransition or FocusableDemo.
#Composable
private fun FocusableText(text: MutableState<String>) {
val focusModifier = FocusModifier()
Text(
modifier = focusModifier
.tapGestureFilter { focusModifier.requestFocus() }
.keyInputFilter { it.value?.let { text.value += it; true } ?: false },
text = text.value,
color = when (focusModifier.focusState) {
Focused -> Color.Green
NotFocused -> Color.Black
NotFocusable -> Color.Gray
}
)
}
Compose has since updated and made the FocusManager members public; although, I'm not exactly sure how final the api is as of dev10. But as of now you can create a FocusNode and listen for changes using FocusManager.registerObserver.
val focusNode = remember {
FocusNode().apply {
focusManager.registerObserver(node = this) { fromNode, toNode ->
if (toNode == this) {
// has focus
} else {
// lost focus
}
}
}
}
If you'd like to gain focus, you can now call FocusManager.requestFocus:
val focusManager = FocusManagerAmbient.current
focusManager.requestFocus(focusNode)
You can also set a focusIdentifier on your FocusNode:
val focusNode = remember {
FocusNode().apply {
...
focusManager.registerFocusNode("your-focus-identifier", node = this)
}
}
To gain focus for a specific identifier, you just call FocusManager.requestFocusById
Using that you can easily create a Composable that can provide and request focus for you, for instance:
#Composable
fun useFocus(focusIdentifier: String? = null): Pair<Boolean, () -> Unit> {
val focusManager = FocusManagerAmbient.current
val (hasFocus, setHasFocus) = state { false }
val focusNode = remember {
FocusNode().apply {
focusManager.registerObserver(node = this) { fromNode, toNode ->
setHasFocus(toNode == this)
}
focusIdentifier?.let { identifier ->
focusManager.registerFocusNode(identifier, node = this)
}
}
}
onDispose {
focusIdentifier?.let { identifier ->
focusManager.unregisterFocusNode(identifier)
}
}
return hasFocus to {
focusManager.requestFocus(focusNode)
}
}
val (hasFocus, requestFocus) = useFocus("your-focus-identifier")
You could also compose children along with it:
#Composable
fun FocusableTextButton(
text: String,
focusedColor: Color = Color.Unset,
unFocusedColor: Color = Color.Unset,
textColor: Color = Color.White,
focusIdentifier: String? = null
) {
val (hasFocus, requestFocus) = useFocus(focusIdentifier)
Surface(color = if (hasFocus) focusedColor else unFocusedColor) {
TextButton(onClick = requestFocus) {
Text(text = text, color = textColor)
}
}
}
Alternatively, there's also FocusModifier, which as of now is:
/**
* A [Modifier.Element] that wraps makes the modifiers on the right into a Focusable. Use a
* different instance of [FocusModifier] for each focusable component.
*
* TODO(b/152528891): Write tests for [FocusModifier] after we finalize on the api (API
* review tracked by b/152529882).
*/
But I don't think you can apply an identifier with it right now.
val focusModifier = FocusModifier()
val hasFocus = focusModifier.focusDetailedState == FocusDetailedState.Active
Surface(
modifier = focusModifier,
color = if (hasFocus) focusedColor else unFocusedColor
) {
TextButton(onClick = { focusModifier.requestFocus() }) {
Text(text = text, color = textColor)
}
}
All that being said, I'm not 100% sure this is the intended way to handle focus right now. I referenced CoreTextField a lot to see how it was being handled there.
Example:
#Composable
fun FocusTest() {
val focusManager = FocusManagerAmbient.current
val selectRandomIdentifier: () -> Unit = {
focusManager.requestFocusById(arrayOf("red,", "blue", "green", "yellow").random())
}
Column(verticalArrangement = Arrangement.SpaceBetween) {
FocusableTextButton(
text = "When I gain focus, I turn red",
focusedColor = Color.Red,
focusIdentifier = "red"
)
FocusableTextButton(
text = "When I gain focus, I turn blue",
focusedColor = Color.Blue,
focusIdentifier = "blue"
)
FocusableTextButton(
text = "When I gain focus, I turn green",
focusedColor = Color.Green,
focusIdentifier = "green"
)
FocusableTextButton(
text = "When I gain focus, I turn yellow",
focusedColor = Color.Yellow,
focusIdentifier = "yellow"
)
Button(onClick = selectRandomIdentifier) {
Text(text = "Click me to randomly select a node to focus")
}
}
}

this code works for me
var haveFocus by remember { mutableStateOf(false) }
Surface(
modifier = Modifier.onFocusEvent {
haveFocus = it.isFocused
},
shape = RectangleShape
){...}

This answer is base on Gabriele Mariotti answer but I have change
Use remember { FocusRequester() } to prevent crash FocusRequester is not initialized after do 2nd click on Text
Use focusTarget instead of focusModifier because deprecated
.
var color by remember { mutableStateOf(Color.Black) }
val focusRequester = remember { FocusRequester() }
Text(
text = "Hello",
modifier = Modifier
.focusRequester(focusRequester)
.onFocusChanged { color = if (it.isFocused) Color.Green else Color.Black }
.focusTarget()
.pointerInput(Unit) { detectTapGestures { focusRequester.requestFocus() } }
)

Related

Kotlin JetPack Compose, changing color of dynamic composed buttons

So I have this code:
#Composable
fun Botones(strings: MutableMap<String, Color>) {
Column{
strings.forEach { color ->
Button(
onClick = {
// Update the button color
if (color.value == Color.Red) {
color to Color.Blue
} else if (colo.value == Color.Blue) {
color to Color.Green
} else {
color to Color.Red
}
},
colors = ButtonDefaults.buttonColors(backgroundColor = color.value)
) {
// Place the string in the buton, set font color to white
Text(colorsito.key, color = Color.White)
}
}
}
}
Initially, all colors should be red, and every time a button is clicked, it should change its color, while the other buttons should remain the same. I don't necesarely need to use a mutable map, what I want is, for every string, make a red button, that when clicked, changes it color depending on which color it had before.
Problem is I can't get it to work because apparently it doesn't recompose. I tried using remember, but it doesn't detect when a value of a mutable map changes, and I sincerely don't know how to get it to work. Help would be much appreciated, I've been googlin' for a while with 0 results, and not even chatGPT was able to get it to work correctly.
EDIT: So magically Copilot suggested this, and it works, but I would very much appreciate if someone could explain why:
#Composable
fun MyComposable(items: List<String>) {
var itemsio by remember { mutableStateOf(items) }
LazyColumn {
items(itemsio.size) { index ->
var item = itemsio[index]
var color by remember { mutableStateOf(Color.Red) }
Text(item, modifier = Modifier.background(color).clickable(
interactionSource = remember {MutableInteractionSource()},
indication = null
) {
if (color == Color.Red) {
color = Color.Blue
} else if (color == Color.Blue) {
color = Color.Green
} else {
color = Color.Red
}
}
)
}
}
interactionSource and indication makes it work? but why
It looks like CoPilot or chatGPT need some time to take our jobs 😁
Code suggested by CoPilot actually doesn't work, when you scroll up or down it resets back to Red because of how LazyList works.
When you scroll an item out of Viewport it exits composition, when that item is visible again it enters composition with block inside remember is calculated with default value.
I made a small cosmetic change to display why that code doesn't work also it contains redundant code either.
#Composable
fun MyComposable(items: List<String>) {
// CoPilot why var and why do you need this list here if it's a param? And if list changes it won't be recomposed with new list either
var itemsio by remember { mutableStateOf(items) }
LazyColumn(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(itemsio.size) { index ->
var item = itemsio[index]
var color by remember { mutableStateOf(Color.Red) }
Text(item,
modifier = Modifier
.background(color)
.fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
if (color == Color.Red) {
color = Color.Blue
} else if (color == Color.Blue) {
color = Color.Green
} else {
color = Color.Red
}
}
.padding(10.dp)
)
}
}
}
This can be achieved in various ways, i will post only 2.
Using rememberSaveable
#Composable
fun MyComposable(items: List<String>) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(items) { item ->
var colorId by rememberSaveable { mutableStateOf(0) }
val color = when (colorId) {
1 -> {
Color.Blue
}
2 -> {
Color.Green
}
else -> {
Color.Red
}
}
Text(item,
modifier = Modifier
.background(color)
.fillMaxWidth()
.clickable {
colorId = (colorId + 1) % 3
}
.padding(10.dp)
)
}
}
}
Using a model and ViewModel/Presenter
You can create a model that contains color or select id for selection status such as 0 for not selected, 1 for red and so on or enum. Then inside a ViewModel, presenter or any class that your choosing should handle color changes while keeping models updated with latest color.
Result
In this answer i change selected status but it can easily be applied. I used int to keep Compose class Color out of ViewModel
#Immutable
data class MyItem(val text: String, val colorId: Int = 0)
ViewModel implementation
class MyViewModel : ViewModel() {
private val myItems = mutableStateListOf<MyItem>()
.apply {
repeat(20) {
add(MyItem(text = "Row $it"))
}
}
fun items() = myItems
fun toggleSelection(index: Int) {
val item = myItems[index]
val newColorId = (item.colorId + 1) % 3
myItems[index] = item.copy(colorId = newColorId)
}
}
Usage
#Composable
fun MyComposable(viewModel: MyViewModel) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
itemsIndexed(items = viewModel.items()) { index: Int, item: MyItem ->
val color = when (item.colorId) {
1 -> {
Color.Blue
}
2 -> {
Color.Green
}
else -> {
Color.Red
}
}
Text(text = item.text,
modifier = Modifier
.background(color)
.fillMaxWidth()
.clickable {
viewModel.toggleSelection(index)
}
.padding(10.dp)
)
}
}
}
Using SnapshotStateMap
Other alternative is using SnapshotStateMap which contains indices and color as Int or as compose Color and update index with next color
val map: SnapshotStateMap<Int, Int> = remember {
mutableStateMapOf()
}
You can use remember in forEach block
Column {
strings.forEach { color ->
var _color by remember { mutableStateOf(color.value) }
Button(
onClick = {
_color = when (_color) {
Color.Red -> Color.Green
Color.Green -> Color.Blue
Color.Blue -> Color.Red
else -> Color.Red
}
},
colors = ButtonDefaults.buttonColors(containerColor = _color)
) {
Text(color.key, color = Color.White)
}
}
}

Animate visibility in compose

I have a text which need to be animated to show and hide with the value is null or not. it would have been straight forward if the visibility is separately handle, but this is what I got.
In the bellow code the enter animation works but the exit animation dont as the text value is null.
I can think of something with remembering the old value but not sure how.
#Composable
fun ShowAnimatedText(
text : String?
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = text != null,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
I think the fade-out animation is actually working "per-se".
I suspect the parameter text: String? is a value coming from a hoisted "state" somewhere up above ShowAnimatedText, and since you are directly observing it inside the animating scope, when you change it to null it instantly removes the Text composable, and your'e not witnessing a slow fade out.
AnimatedVisibility(
...
) {
text?.let { // your'e directly observing a state over here
Text(text = it)
}
}
This is my attempt completing your snippet based on my assumption and making it work, the fade-in works, but the desired fade-out is instantly happening.
#Composable
fun SomeScreen() {
var text by remember {
mutableStateOf<String?>("Initial Value")
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
text = "New Value"
}) {
Text("Set New Value")
}
Button(onClick = {
text = null
}) {
Text("Remove Value")
}
AnimatedText(text = text)
}
}
#Composable
fun ShowAnimatedText(
text : String?
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = text != null,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
You can solve it by modifying the text to a non-state value and change your visibility logic from using a nullability check to some "business logic" that would require it to be visible or hidden, modifying the codes above like this.
#Composable
fun SomeScreen() {
var show by remember {
mutableStateOf(true)
}
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(onClick = {
show = !show
}) {
Text("Set New Value")
}
AnimatedText(text = "Just A Value", show)
}
}
#Composable
fun ShowAnimatedText(
text : String?,
show: Boolean
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = show,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
text?.let {
Text(text = it)
}
}
}
}
I fixed it by remembering the previous state (Or don't set the null value) until the exit animation is finished, if the text is null.
Thank you z.y for your suggestion.
#Composable
fun ShowAnimatedText(
text : String?,
show: Boolean
) {
var localText by remember {
mutableStateOf<String?>(null)
}
AnimatedContent(show, localText)
LaunchedEffect(key1 = text, block = {
if(text == null){
delay(2000)
}
localText = text
})
}
#Composable
private fun AnimatedContent(show: Boolean, localText: String?) {
Column(
modifier = Modifier.fillMaxWidth()
) {
AnimatedVisibility(
visible = show,
enter = fadeIn(animationSpec = tween(2000)),
exit = fadeOut(animationSpec = tween(2000))
) {
localText?.let {
Text(text = it)
}
}
}
}

Enable and focus Textfield at once in Jetpack Compose

I'm trying to implement a TextField in Jetpack Compose with the following functionality: at first it is disabled, but when a user presses the Button, it gets enabled and at the same moment receives focus. This was my approach:
var text by remember { mutableStateOf("text") }
var enabled by remember { mutableStateOf(false)}
val focusRequester = remember { FocusRequester() }
Column {
TextField(
value = text,
onValueChange = { text = it },
enabled = enabled,
modifier = Modifier.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 24.sp)
)
Button(onClick = {
enabled = true
focusRequester.requestFocus()
}) {
Text("Enable and request focus")
}
But when the button is pressed, the TextField only gets enabled, not focused. To focus it, user has to click it once again. What am I doing wrong and what is the possible workaround?
You have to listen the change of the enabled parameter to give the focus to the TextField.
You can change your code to:
Button(onClick = {
enabled = true
}) {
Text("Enable and request focus")
}
LaunchedEffect(enabled) {
if (enabled){
focusRequester.requestFocus()
}
}
Simply add the delay after enabling textfield like this:
var text by remember { mutableStateOf("text") }
var enabled by remember { mutableStateOf(false)}
val focusRequester = remember { FocusRequester() }
val scope = rememberCoroutineScope()
Column {
TextField(
value = text,
onValueChange = { text = it },
enabled = enabled,
modifier = Modifier.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 24.sp)
)
Button(onClick = {
scope.launch {
enabled = true
delay(100)
focusRequester.requestFocus()
}
}) {
Text("Enable and request focus")
}
}
I once had a similar issue and I solved it by using interactionSource. The code looked something like this:
var text by remember { mutableStateOf("text") }
var enabled by remember { mutableStateOf(false)}
val focusRequester = remember { FocusRequester() }
val interactionSource = remember { MutableInteractionSource() } // add this
Column {
TextField(
value = text,
onValueChange = { text = it },
enabled = enabled,
modifier = Modifier
.focusable(interactionSource = interactionSource) // and this
.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 24.sp)
)
...
IIRC it was important to have .focusable() and .focusRequester() in the right order, but cannot remember which exactly, so try to swap them if it won't work immediately.

Avoid accidental tap/touch in a Jetpack Compose Slider which is placed inside in a scrollable column

I have a Slider which is placed inside a Column which is scrollable. When i scroll through the components sometimes accidentally slider value changes because of accidental touches. How can i avoid this?
Should i be disable taps on slider? If yes how can i do it?
Is there any alternate like Nested scroll instead of Column which can prevent this from happening?
#Composable
fun ColumnScope.FilterRange(
title: String,
range: ClosedFloatingPointRange<Float>,
rangeText: String,
valueRange: ClosedFloatingPointRange<Float>,
onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
) {
Spacer(modifier = Modifier.height(Size_Regular))
Text(
text = title,
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(Size_X_Small))
Text(
text = rangeText,
style = MaterialTheme.typography.subtitle1
)
RangeSlider(
modifier = Modifier.fillMaxWidth(),
values = range,
valueRange = valueRange,
onValueChange = {
onValueChange(it)
})
Spacer(modifier = Modifier.height(Size_Small))
Divider(thickness = DividerSize)
}
I would disable the RangeSlider and only enable it when you tap on it. You disable it by tapping anywhere else within the Column. This is a similar behavior used to mimic losing focus. Here's an example:
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var rangeEndabled by remember { mutableStateOf(false)}.apply { this.value }
var sliderPosition by remember { mutableStateOf(0f..100f) }
Text(text = sliderPosition.toString())
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.pointerInput(Unit) {
detectTapGestures(
onTap = {
rangeEndabled = false
}
)
}) {
repeat(30) {
Text(it.toString())
}
RangeSlider(
enabled = rangeEndabled,
values = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 0f..100f,
onValueChangeFinished = {
// launch some business logic update with the state you hold
// viewModel.updateSelectedSliderValue(sliderPosition)
},
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
rangeEndabled = true
}
)
}
)
repeat(30) {
Text(it.toString())
}
}
}
}
}

How to handle visibility of a Text in Jetpack Compose?

I have this Text:
Text(
text = stringResource(id = R.string.hello)
)
How can I show and hide this component?
I'm using Jetpack Compose version '1.0.0-alpha03'
As CommonsWare stated, compose being a declarative toolkit you tie your component to a state (for ex: isVisible), then compose will intelligently decide which composables depend on that state and recompose them. For ex:
#Composable
fun MyText(isVisible: Boolean){
if(isVisible){
Text(text = stringResource(id = R.string.hello))
}
}
Also you could use the AnimatedVisibility() composable for animations.
You can simply add a condition like:
if(isVisible){
Text("....")
}
Something like:
var visible by remember { mutableStateOf(true) }
Column() {
if (visible) {
Text("Text")
}
Button(onClick = { visible = !visible }) { Text("Toggle") }
}
If you want to animate the appearance and disappearance of its content you can use the AnimatedVisibility
var visible by remember { mutableStateOf(true) }
Column() {
AnimatedVisibility(
visible = visible,
enter = fadeIn(
// Overwrites the initial value of alpha to 0.4f for fade in, 0 by default
initialAlpha = 0.4f
),
exit = fadeOut(
// Overwrites the default animation with tween
animationSpec = tween(durationMillis = 250)
)
) {
// Content that needs to appear/disappear goes here:
Text("....")
}
Button(onClick = { visible = !visible }) { Text("Toggle") }
}
As stated above, you could use AnimatedVisibility like:
AnimatedVisibility(visible = yourCondition) { Text(text = getString(R.string.yourString)) }
/**
* #param visible if false content is invisible ie. space is still occupied
*/
#Composable
fun Visibility(
visible: Boolean,
content: #Composable () -> Unit
) {
val contentSize = remember { mutableStateOf(IntSize.Zero) }
Box(modifier = Modifier
.onSizeChanged { size -> contentSize.value = size }) {
if (visible || contentSize.value == IntSize.Zero) {
content()
} else {
Spacer(modifier = Modifier.size(contentSize.value.width.pxToDp(), contentSize.value.height.pxToDp()))
}
}
}
fun Int.pxToDp(): Dp {
return (this / getSystem().displayMetrics.density).dp
}
usage:
Visibility(text.value.isNotEmpty()) {
IconButton(
onClick = { text.value = "" },
modifier = Modifier
.padding(bottom = 8.dp)
.height(30.dp),
) {
Icon(Icons.Filled.Close, contentDescription = "Clear text")
}
}

Categories

Resources