Set a single AnimatedVisibility to true in a LazyColumn - android-jetpack-compose

I have a Composable function for operating system infos which expands it details upon click and reverts when clicked again.
#ExperimentalAnimationApi
#Composable
fun OSCard(os: OS) {
var expanded by remember {
mutableStateOf(false)
}
Column(modifier = Modifier
.clickable { expanded = !expanded }
.fillMaxWidth()) {
Text(
modifier = Modifier
.padding(20.dp),
text = os.name,
style = MaterialTheme.typography.h6
)
AnimatedVisibility(visible = expanded) {
Text(text = os.description, modifier = Modifier.padding(20.dp))
}
Divider(Modifier.height(2.dp))
}
}
I created a list of it and passed it through a LazyColumn
var OSs = listOf<OS>(
OS(
"Android",
"Android is a mobile/desktop operating system..."
),
OS(
"Microsoft Windows",
"Microsoft Windows, commonly referred to as Windows..."
),
OS(
"Linux",
"Linux is a family of open-source Unix-like operating systems..."
)
)
Surface(color = MaterialTheme.colors.background) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items = OSs){
os -> OSCard(os)
}
}
}
While it works as expected, I want to make it such that if a card is opened and another card is selected, the previously opened card will be closed.
This is what I am trying to avoid, can someone give me a tip on how to?

Here is one solution. Add a parameter to your OS object called isExpanded. It only gets set to true when you click on a card. The click handler will clear the flag in all the other cards.
I also added an id parameter which makes it easier to find the item being clicked. The name parameter could have been used.
The state variable expandCard needs to read for it to trigger a recompose, so var e = expandCard is used to read the value. Also note that the creation of your list MUST be outside of the composable, otherwise it would just end up getting re-created and the isExpanded field would be set to false on all items.
And lastly, I'm not sure why you're using a LazyColumn for this. LazyColumn is really meant for large datasets and primarily for paging. You could just use a normal Column with vertical scrolling if you have a reasonable number of items.
class MainActivity : ComponentActivity() {
#ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var OSs = listOf(
OS(
id = "android",
name = "Android",
description = "Android is a mobile/desktop operating system..."
),
OS(
id = "mswindows",
name = "Microsoft Windows",
description = "Microsoft Windows, commonly referred to as Windows..."
),
OS(
id = "linux",
name ="Linux",
description = "Linux is a family of open-source Unix-like operating systems..."
)
)
setContent {
var expandCard by remember { mutableStateOf(false) }
var e = expandCard
Surface() {
LazyColumn( modifier = Modifier.fillMaxSize()) {
itemsIndexed(
items = OSs,
key = { index, os ->
os.name
}
) { index, os ->
OSCard(os) {osClicked ->
val isExpanded = osClicked.isExpanded
OSs.forEach { it.isExpanded = false }
OSs.first { it.id == osClicked.id }.isExpanded = isExpanded
expandCard = !expandCard
}
}
}
}
}
}
}
#ExperimentalAnimationApi
#Composable
fun OSCard(
os: OS,
onClick: (os: OS) -> Unit
) {
Column(modifier = Modifier
.clickable {
os.isExpanded = !os.isExpanded
onClick(os)
}
.fillMaxWidth()) {
Text(
modifier = Modifier
.padding(20.dp),
text = os.name,
style = MaterialTheme.typography.h6
)
AnimatedVisibility(visible = os.isExpanded) {
Text(text = os.description, modifier = Modifier.padding(20.dp))
}
Divider(Modifier.height(2.dp))
}
}
class OS(var id: String, var name: String, var description: String, var isExpanded: Boolean= false)

Related

How to prevent accessibility focus from moving to controls behind ExposedDropdownMenuBox

I am having an issue where the accessibility focus is going to controls behind the PopUp Window when using a ExposedDropdownMenuBox
If there is a single ExposedDropdownMenuBox everything works as expected, but when I add a second ExposedDropdownMenuBox or another control the focus goes to the second ExposedDropdownMenuBox before going to the PopUp Window.
GIF of single dropdown behavior
https://giphy.com/gifs/gapy0XK1CGmbyltJxU
GIF of two dropdowns on the same screen
https://giphy.com/gifs/WkL5TcMWlumfcGHPmD
Source
#Composable
fun Screen() {
Column (
modifier = Modifier
.wrapContentSize(Alignment.TopCenter)
.padding(top = 48.dp)
) {
Text(
text = stringResource(id = R.string.greeting),
fontSize = 30.sp,
modifier = Modifier.padding(bottom = 24.dp)
)
LocaleDropdownMenu()
Spacer(modifier = Modifier.height(8.dp))
// LocaleDropdownMenu()
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun LocaleDropdownMenu() {
val localeOptions = mapOf(
R.string.en to "en",
R.string.fr to "fr",
R.string.hi to "hi",
R.string.ja to "ja"
).mapKeys { stringResource(it.key) }
// boilerplate: https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#ExposedDropdownMenuBox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
TextField(
readOnly = true,
value = stringResource(R.string.language),
onValueChange = { },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
}
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
localeOptions.keys.forEach { selectionLocale ->
DropdownMenuItem(
onClick = {
expanded = false
// set app locale given the user's selected locale
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(
localeOptions[selectionLocale]
)
)
},
content = { Text(selectionLocale) }
)
}
}
}
}
A repository that reproduces this issue is here:
https://github.com/dazza5000/ExposedDropdownMenuBox-accessibility-issue

I have a composable not setting button text as expected; wondering why. Have a reproducible example

this started as a new compose project
with the following code the intent is to change the text to the picked time. The code is commented where the behavior occurs
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTestTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TimeCardButton(id = 1, symbol ="In", enabled=true,modifier = Modifier) { entry ->
Log.d("click", "$entry result")
}
}
}
}
}
}
data class TimeCardEntry(val id: Int = -1, var entry: String = "")
#Composable
fun TimeCardButton(
id: Int,
symbol: String,
enabled: Boolean = false,
modifier: Modifier,
onValueChange: (TimeCardEntry) -> Unit = {},
) {
// Value for storing time as a string
val timeState = remember {
mutableStateOf(TimeCardEntry(id, symbol))
}
val validState = remember {
timeState.value.entry.trim().isNotEmpty()
}
val mTime = remember { mutableStateOf(symbol) }
if (enabled) {
// Fetching local context
val mContext = LocalContext.current
// Declaring and initializing a calendar
val mCalendar = Calendar.getInstance()
val mHour = mCalendar[Calendar.HOUR_OF_DAY]
val mMinute = mCalendar[Calendar.MINUTE]
// Creating a TimePicker dialog
val mTimePickerDialog = TimePickerDialog(
mContext,
{ _, mHour: Int, mMinute: Int ->
timeState.value.entry = "$mHour:$mMinute"
mTime.value = "$mHour:$mMinute"
onValueChange(timeState.value)
}, mHour, mMinute, false
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
TextButton(onClick = { mTimePickerDialog.show() }.also {
Log.d("click", "id $id clicked!") }) {
Column() {
// if I use just this it works [in changes to the time picked]
//Text(text = mTime.value)
// if i use both of these BOTH are set when the date picker is invoked
// if I just use the second one alone, the text never changes
Text(text = timeState.value.entry)
}
}
}
} else {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
Text(text = symbol, color =
MaterialTheme.colors.onBackground)
}
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyApplicationTestTheme {
}
}
First of all how to fix it:
Your problem basically is this. The easiest way to fix it would be to reassign the whole value of TimeState, not just entry by calling
timeState.value = timeState.value.copy(entry = "$mHour:$mMinute")
The reason it doesn't work with only the second one is that the change of a property doesn't trigger recomposition, even if the variable containing it is a mutableState. To fix (as outlined in the answers to the question linked above) this you either have to reassign the whole variable or make the parameter you want to observe observable (for example changing the String to State<String>)
PS: if you use by with mutableStateOf (i.e. val timeState = remember { mutableStateOf(TimeCardEntry(id, symbol)) }) you don't have to use .value every time. I find that a lot cleaner and more readable

LazyColumn does not show me the list of items

I'm just learning how to use LayzyColumn. The LazyColumn does not show me the list of items. When I click the button, the items that are loaded in the list are not displayed.
Here my code:
#Composable
fun MainContent() {
val list = remember { mutableListOf<String>() }
LazyColumn(
contentPadding = PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
itemsIndexed(list) { index, item ->
Card(
backgroundColor = Color(0xFFF7C2D4),
elevation = 4.dp
) {
Row(
Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = item.takeLast(5), Modifier.weight(1F))
}
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
Alignment.BottomEnd
) {
FloatingActionButton(onClick = { list.add(UUID.randomUUID().toString()) }) {
Icon(imageVector = Icons.Default.Add, contentDescription = "")
}
}
}
Compose cannot track changes of plain kotlin types. You need to use Compose mutable states.
In this case mutableStateListOf should be used instead of mutableListOf.
I suggest you start with this youtube video which explains the basic principles of why do you need to use state in Compose. You can continue deepening your knowledge with state in Compose documentation.

jetpack compose not updating icon

the title is updated on changes but the icon ist not.
#Composable
fun topBar(title: String) {
val context = LocalContext.current
var showMenu by remember { mutableStateOf(false) }
val health = remember { Health() }
val healthMin = remember { mutableStateOf(R.drawable.music) }
TopAppBar(title = {
val location = remember { mutableStateOf("") }
SystemBroadcastReceiver(MediaPlaybackService.LOCATION_CHANGE) { intent ->
location.value = intent!!.getStringExtra(MediaPlaybackService.LOCATION_CHANGE)!!
}
Text(text = location.value)
},
navigationIcon = {
SystemBroadcastReceiver(Health.STATUS) { intent ->
health.onReceive(null, intent)
healthMin.value = health.updateLogo()
}
IconButton(onClick = {}) {
Log.i("TEST","Value = ${healthMin.value}")
Icon(
painter = painterResource(healthMin.value),
"contentDescription"
)
}
},
...
I can see the changes in the log. But the UI is not updated
2022-02-24 12:56:24.117 3032-3032/com.example.myapplication I/TEST: Value = 2131231223
2022-02-24 12:56:24.525 3032-3032/com.example.myapplication I/TEST: Value = 2131231226
The SystemBroadcastReceiver is taken from the case-study official documentaion
The code actually works. My problem was, that the visual icon appeared unchanged because the changed icon has only a different color. And the color is overruled by tint. So the change is not visible.
Use this if you want to see the color from the ressource and not the one from the theme.
Icon( painter = painterResource(healthMin.value),
tint = Color.Unspecified,
"contentDescription"
)

Jetpack Compose Lazy Column is stretching items when end of list is reached

Why my LazyColumn items are being stretched?
class RecipeListFragment: Fragment() {
private val viewModel: RecipeListViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
val recipes = viewModel.recipes.value
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "RecipeList",
style = TextStyle(
fontSize = 21.sp
)
)
Spacer(modifier = Modifier.padding(10.dp))
Button(
onClick = {
findNavController().navigate(R.id.viewRecipe)
}
) {
Text(text = "TO RECIPE FRAGMENT")
}
Spacer(modifier = Modifier.padding(10.dp))
LazyColumn{
items(recipes) { recipe ->
// RecipeCard(recipe = recipe, onClick = {})
Text(
text = recipe.title ?: "None",
style = MaterialTheme.typography.h5
)
}
}
}
}
}
}
}
android 12 preview versions prior to ~august build have this problem.
More info can be found here: https://issuetracker.google.com/issues/208694349
Try on the stable android12 and/or on the real device

Resources