I have implemented a discrete slider like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
YourProjectNameTheme(darkTheme = false) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 4.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
MyUI()
}
}
}
}
}
}
#Composable
private fun MyUI() {
var sliderValue by remember {
mutableStateOf(1f)
}
Slider(
value = sliderValue,
onValueChange = { sliderValue_ ->
sliderValue = sliderValue_
},
onValueChangeFinished = {
// this is called when the user completed selecting the value
},
valueRange = 1f..21f,
steps = 6
)
Text(text = sliderValue.toString())
}
The output:
I'm expecting exact numbers (like 3, 6, 9, 12, 15, 18) when I tap on the tick marks. But, it is showing the nearest float value. How to fix this?
The steps attribute if greater than 0, specifies the amounts of discrete values, evenly distributed between across the whole value range.
In you case you have to use valueRange = 0f..21f
Slider(
//...
valueRange = 0f..21f,
steps = 6
)
That's because you are starting from 1 instead of 0. Please plan the value and the steps accordingly,
#Preview
#Composable
private fun MyUI() {
var sliderValue by remember {
mutableStateOf(0)
}
Slider(value = sliderValue.toFloat(), onValueChange = { sliderValue_ ->
sliderValue = sliderValue_.toInt()
}, onValueChangeFinished = {
// this is called when the user completed selecting the value
}, valueRange = 0f..21f, steps = 6
)
Text(text = sliderValue.toString())
}
Related
I try to change the value of a card with a slider, but it won't work. The slider is not movable and the value in the card don't change. Do you have an idea what I'm doing wrong?
View Model:
class CardSelectionViewModel: ViewModel() {
private var _state by mutableStateOf(
CardState()
)
val state: CardState
get() = _state
fun setCardValue(value: Float){
CardState().value = (Math.round(value * 2) / 2.0).toFloat()
}
}
data class CardState(
var value: Float = 0f
)
View:
#Composable
fun CardSelectionScreen(state: CardState) {
val viewModel = CardSelectionViewModel()
CardSelectionScreenTheme {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
CardContent(remember{viewModel.state.value})
Slider(
value = viewModel.state.value,
onValueChange = {
viewModel.setCardValue(it)
},
valueRange = 0f..7f,
modifier = Modifier.width(300.dp)
)
Button(onClick = {
state.value ++
println(state.value) }) {
Text(text = stringResource(R.string.Continue))
}
}
}
}
You are creating new instance of CardState() inside setCardValue instead of setting _state = CardState((Math.round(value * 2) / 2.0).toFloat())
New to Compose and struggling hard with more complex state cases.
I cant seem to change the text dynamically in these cards, only set the text manually. Each button press should change the text in a box, starting left to right, leaving the next boxes empty.
What is wrong here?
UI:
val viewModel = HomeViewModel()
val guessArray = viewModel.guessArray
#Composable
fun HomeScreen() {
Column(
modifier = Modifier
.fillMaxWidth()
) {
WordGrid()
Keyboard()
}
}
#Composable
fun WordGrid() {
CardRow()
}
#Composable
fun Keyboard() {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
MyKeyboardButton(text = "A", 35)
MyKeyboardButton(text = "B", 35)
}
}
#Composable
fun MyCard(text: String) {
Card(
modifier = Modifier
.padding(4.dp, 8.dp)
.height(55.dp)
.aspectRatio(1f),
backgroundColor = Color.White,
border = BorderStroke(2.dp, Color.Black),
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = text,
fontSize = 20.sp,
)
}
}
}
#Composable
fun CardRow() {
guessArray.forEach { rowCards ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
rowCards.forEach {
MyCard(it)
println(it)
}
}
}
}
#Composable
fun MyKeyboardButton(text: String, width: Int) {
Button(
onClick = {
guessArray[viewModel.currentRow][viewModel.column] = text
viewModel.column += 1
},
modifier = Modifier
.width(width.dp)
.height(60.dp)
.padding(0.dp, 2.dp)
) {
Text(text = text, textAlign = TextAlign.Center)
}
}
ViewModel:
class HomeViewModel : ViewModel() {
var currentRow = 0
var guessArray = Array(5) { Array(6) { "" }.toMutableList() }
var column = 0
}
The grid is created, but the text is never changed.
To make Compose view recompose with the new value, a mutable state should be used.
You're already using it with remember in your composable, but it also needs to be used in your view model for properties, which should trigger recomposition.
class HomeViewModel : ViewModel() {
var currentRow = 0
val guessArray = List(5) { List(6) { "" }.toMutableStateList() }
var column = 0
}
Starting to Learn JetPack Compose. I'm struggling now with State hosting. I have this simple example to press a Button to show in a Text component the content of an array. I'm able to make it work if the variable is inside my #Composable. When applying State Hoisting (taking the variable for my composable) I'm finding some issues.
This is the code that is working fine
var listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
NextValue(listadoNumeros)
#Composable
fun NextValue(listado: List<Int>) {
var position by rememberSaveable {mutableStateOf (0)}
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
){
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { position += 1 }) {
Text(text = "Next")
}}}
This is the code that is not working correctly
var listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
var n by rememberSaveable {mutableStateOf (0)}
NextValue(position = n,listadoNumeros)
#Composable
fun NextValue(position:Int,listado: List<Int>) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { position += 1 }) {
Text(text = "Next")
}}}
Error message, as you can expect, is "position can't be reassigned". I see why, but don't know how to fix it. I read about onValueChanged in TextField, etc, but don't know if it's applicable here.
position += 1 is equivalent to position = position + 1. In Kotlin, function arguments are val and cannot be reassigned within the scope of the function. That is why the compiler will complain and prevent you from doing that.
What you want to do is to add an extra event callback within the function and perform this addition at the function call site.
#Composable
fun NextValue(position: Int, listado: List<Int>, onPositionChange: (Int) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { onPositionChange(position + 1) }) {
Text(text = "Next")
}
}
}
You can use the above composable as
val listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
var n by remember { mutableStateOf(0) }
NextValue(position = n, listadoNumeros, onPositionChange = { newPosition -> n = newPosition})
Whenever the user clicks on the button, an event is sent back up to the caller and the caller decides what to do with the updated information. In this case, recreate the composable with an updated value.
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())
}
}
}
}
}
In my viewModel I have "state" for every single screen. e.g.
class MainState(val commonState: CommonState) {
val text = MutableStateFlow("text")
}
I pass viewModel to my JetpackCompose screen.
#Composable
fun MainScreen(text: String, viewModel: HomeViewModel) {
val textState = remember { mutableStateOf(TextFieldValue()) }
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = viewModel.state.mainState.text.value,
color = Color.Blue,
fontSize = 40.sp
)
Button(
onClick = { viewModel.state.mainState.text.value = "New text" },
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Green
),
modifier = Modifier.padding(16.dp)
) {
Text(text)
}
TextField(
value = textState.value,
onValueChange = { textState.value = it },
label = { Text("Input text") }
)
}
}
When I click button I change value of state and I expect UI will update but it does not. I have to click on TextField and then text in TextView updates.
Any suggestion why UI does not update automatically?
That's how I pass components and start whole screen in startActivity;
class HomeActivity : ComponentActivity() {
private val viewModel by viewModel<HomeViewModel>()
private val homeState: HomeState get() = viewModel.state
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RateMeAppTheme {
ContentScreen(viewModel, homeState)
}
}
}
}
In this simple case u should use mutableStateOf("text") in class MainState instead of mutableStateFlow
class MainState(val commonState: CommonState) {
val text = mutableStateOf("text")
}
Using MutableStateFlow
To use MutableStateFlow (which is not required in the current scenario) , we need to collect the flow.
Like the following:-
val state = viewModel.mainState.text.collectAsState() // we can collect a stateflow as state in a composable function
Then we can use the observed state value in the Text using:-
Text(text = state.value, ..)
Finally your composable function should look like:-
#Composable
fun MainScreen(text: String, viewModel: HomeViewModel) {
val textState = remember { mutableStateOf(TextFieldValue()) }
val state = viewModel.mainState.text.collectAsState()
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = state.value,
color = Color.Blue,
fontSize = 40.sp
)
Button(
onClick = { viewModel.mainState.text.value = "New text" },
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Green
),
modifier = Modifier.padding(16.dp)
) {
Text(text)
}
TextField(
value = textState.value,
onValueChange = { textState.value = it },
label = { Text("Input text") }
)
}
}