How can I create a scrolling row that scrolls automatically at a fixed speed that loops around the content of a list of images?
I have a lazy row of images as defined below, but haven't found a good way to loop it (like a carousel).
var images: List<String> = listOf()
repeat(8) {
images = images.plus("https://place-puppy.com/300x300")
}
val state = rememberLazyListState()
LazyRow(
modifier = modifier.fillMaxWidth(),
state = state
) {
items(count = images.size) { i ->
val image = images.get(i)
Column(
modifier = Modifier
.width(40.dp)
.aspectRatio(1f)
) {
Image(
painter = rememberImagePainter(image),
contentDescription = null,
modifier = Modifier
.fillMaxSize()
}
}
}
firstly, create an infinite auto-scrolling effect that will be running as long as the composable is active & displayed:
LazyRow() {
....
}
LaunchedEffect(Unit) {
autoScroll(lazyListState)
}
private tailrec suspend fun autoScroll(lazyListState: LazyListState) {
lazyListState.scroll(MutatePriority.PreventUserInput) {
scrollBy(SCROLL_DX)
}
delay(DELAY_BETWEEN_SCROLL_MS)
autoScroll(lazyListState)
}
private const val DELAY_BETWEEN_SCROLL_MS = 8L
private const val SCROLL_DX = 1f
Secondly, update positions of items in the list accordingly:
val lazyListState = rememberLazyListState()
LazyRow(
state = lazyListState,
modifier = modifier,
) {
items(images) {
...
if (it == itemsListState.last()) {
val currentList = images
val secondPart = currentList.subList(0, lazyListState.firstVisibleItemIndex)
val firstPart = currentList.subList(lazyListState.firstVisibleItemIndex, currentList.size)
rememberCoroutineScope().launch {
lazyListState.scrollToItem(0, maxOf(0, lazyListState.firstVisibleItemScrollOffset - SCROLL_DX.toInt()))
}
images = firstPart + secondPart
}
}
}
That should give you the looping behavior.
Credits: https://proandroiddev.com/infinite-auto-scrolling-lists-with-recyclerview-lazylists-in-compose-1c3b44448c8
Related
I have a Grid with two columns and 6 elements, but I need that the user select only 3 elements
In the first #Composable I put the value in this way:
LazyVerticalGrid(
columns = GridCells.Fixed(2),
) {
items(list.values.size) {
InterestsItems(
value = list.values[it].text,
)
}
}
And the second #Composable(InterestsItems) is a Box with an Image inside. I put the value like this:
var isSelected by remember { mutableStateOf(false) }
Box(modifier = Modifier.noRippleClickable { isSelected = !isSelected })
The result is that I select all the elements, is not what I want.
You can create a data class to hold selected flag for any item
data class InterestsItem(val text: String, val isSelected: Boolean = false)
A ViewModel that keeps items and indexes of selected items and function to toggle between selected and not selected
class InterestsViewModel : ViewModel() {
val interestItems = mutableStateListOf<InterestsItem>()
.apply {
repeat(6) {
add(InterestsItem(text = "Item$it"))
}
}
private val selectedItems = mutableListOf<Int>()
fun toggleSelection(index: Int) {
val item = interestItems[index]
val isSelected = item.isSelected
if (isSelected) {
interestItems[index] = item.copy(isSelected = false)
selectedItems.remove(index)
} else if (selectedItems.size < 3) {
interestItems[index] = item.copy(isSelected = true)
selectedItems.add(index)
}
}
}
MutableStateListOf will trigger recomposition when we change item
#Composable
private fun SelectItemsFromGridSample(interestsViewModel: InterestsViewModel) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(interestsViewModel.interestItems) { index, item ->
InterestsItemCard(interestsItem = item) {
interestsViewModel.toggleSelection(index)
}
}
}
}
And some Composable with callback to pass that item is clicked
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun InterestsItemCard(interestsItem: InterestsItem, onClick: () -> Unit) {
ElevatedCard(
modifier = Modifier.size(100.dp),
onClick = onClick
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = interestsItem.text, fontSize = 30.sp)
if (interestsItem.isSelected) {
Icon(
modifier = Modifier
.size(50.dp)
.background(Color.Green, CircleShape),
imageVector = Icons.Default.Check,
tint = Color.White,
contentDescription = null
)
}
}
}
}
Result
You are apparently using a common isSelected variable for all the items. If you use a loop to render the items, move the variable declaration inside the loop.
Take the basic programming codelabs on the Android Developers Official Website, to walk through the basics. A strong base in general programming is foundational to learning about any specific programming paradigms/fields. Definitely take the Compose Pathway, now renamed to the Jetpack Compose Course, before anything. In this scenario, you'll need to create either a list representing the selected items, or a hash-map, which is basically a list in which each element is a pair of two objects. When the size of the list/map gets to 3, handle the event inside the on-click, to prevent further selection. That'll depend on what you want to do, and should be custom to your project.
Put the isSelected variable inside the items block.
Example;
val list = remember { mutableStateOf(listOf("0","1","2","3","4","5") )}
LazyVerticalGrid(
columns = GridCells.Fixed(2),
) {
items(list.value.size){
var isSelected by remember { mutableStateOf(false) }
Text(
modifier = Modifier.clickable{
isSelected = !isSelected
},
text = list.value[it],
color = if (isSelected) Color.Red else Color.Black
)
}
}
Result;
I want to display a list of items using LazyColumn. I wrote the needed code but when I ran the app I observed that the UI is very laggy. I put some Logs to find out where the problem may be and I discovered that the LazyColumn inifinitely recomposes the items. I don't know why LazyColumn behaves like it did
list composable:
fun listOfReceivers(
receivers: List<ReceivedPingItem>,
) {
if (receivers.isNotEmpty()) {
val listState = rememberLazyListState()
val firstVisibleIndex = listState.firstVisibleItemIndex
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.lastIndex + listState.firstVisibleItemIndex
if (listState.isScrolledToTheEnd()) {
Log.d("*****", "Scrolled to the End")
viewModel.onRecyclerViewScrolledToLast()
}
//LazyColumn -> equivalent of the RecyclerView
LazyColumn(
state = listState,
contentPadding = PaddingValues(horizontal = 15.dp, vertical = 15.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(receivers) { receiverItem ->
Log.d("*****", "ADDED $receiverItem")
ReceiversListItem(
receiver = receiverItem,
modifier = Modifier.fillMaxWidth()
)
}
}
// AppViewState
if (firstVisibleIndex == 0) {
appViewState.viewState = ViewState.ReceivedPingsViewState(firstVisibleIndex)
} else {
appViewState.viewState = ViewState.DefaultViewState
}
// Seen status check
if (firstVisibleIndex != -1 && lastVisibleItemIndex != -1) {
viewModel.onRecyclerViewScrolledSetViewed(
receivers.subList(
firstVisibleIndex,
lastVisibleItemIndex
)
)
}
}
else{
EmptyPingsScreen()
}
}
Due to this video - youtu.be/EOQB8PTLkpY?t=284 , i did smthn like this, and it works perfectly:
#Composable
fun ListOfReceivers(
receivers: List<ReceivedPingItem>,
) {
if (receivers.isNotEmpty()) {
val listState = rememberLazyListState()
val derivedListState by remember { derivedStateOf { listState.isScrolledToTheEnd() } }
if (derivedListState) {
viewModel.onRecyclerViewScrolledToLast()
}
// LazyColumn -> equivalent of the RecyclerView
LazyColumn(
state = listState,
contentPadding = PaddingValues(horizontal = 15.dp, vertical = 15.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(receivers) { receiverItem ->
Log.d("*****", "ADDED $receiverItem")
ReceiversListItem(
receiver = receiverItem,
modifier = Modifier.fillMaxWidth()
)
}
}
// derivedStateOf - prevents infinite recomposition
val visibleItemsCount by remember { derivedStateOf { listState.layoutInfo.visibleItemsInfo.size } }
val firstVisibleItemIndex = listState.firstVisibleItemIndex
val lastVisibleItemIndex = firstVisibleItemIndex + visibleItemsCount - 1
// AppViewState
if (firstVisibleItemIndex == 0) {
appViewState.viewState = ViewState.ReceivedPingsViewState(firstVisibleItemIndex)
} else {
appViewState.viewState = ViewState.DefaultViewState
}
I need to have a component that scrolls vertically and horizontally.
Here is what I did so far:
#Composable
fun Screen() {
val scope = rememberCoroutineScope()
val scrollOffset = remember {
mutableStateOf(value = 0F)
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(10) {
SimpleRow(
onScrollOffsetChange = {
scrollOffset.value = it
}
)
}
}
}
#Composable
private fun SimpleRow(
onScrollOffsetChange: (Float) -> Unit,
) {
val lazyRowState = rememberLazyListState()
LazyRow(
modifier = Modifier.fillMaxWidth(),
state = lazyRowState
) {
item {
Text(
text = "firstText"
)
}
for (i in 1..30) {
item {
Text(text = "qwerty")
}
}
}
}
When anyone of these SimpleRow is scrolled I want to scroll all of the SimpleRows together.
And I do not know how many SimpleRows I have because they came from our server.
Is there any kind of composable or trick that can do it for me?
You can use multiple rows inside a column that scrolls horizontally and vertically, like this:
#Composable
fun MainContent() {
val scrollStateHorizontal = rememberScrollState()
val scrollStateVertical = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollStateVertical)
.horizontalScroll(state = scrollStateHorizontal)
) {
repeat(40){ c ->
Row {
repeat(40){ r ->
Item("col: $c | row: $r")
}
}
}
}
}
#Composable
fun Item(text: String) {
Text(
modifier = Modifier
.padding(5.dp)
.background(Color.Gray),
text = text,
color = Color.White
)
}
In my app there's a LazyColumn that contains nested LazyRows. I have a memory issue - when there are 30-40 rows and about 10-20 elements per row in the grid, it's possible to reach Out-of-Memory (OOM) by simply scrolling the list vertically up and down about 20 times. An item is a Card with some Boxes and texts. It seems that the resulting composable for each of the items is stored, even when the item is out of composition.
Here is a sample that demonstrates this. It shows a simple grid of 600 elements (they are just Text) and on my emulator gets to a memory usage of about 200 MB. (I use Android TV emulator with landscape, 120 elements are visible at once).
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LazyColumnTestTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
runTest()
}
}
}
}
}
#Composable
fun runTest() {
var itemsState: MutableState<List<TestDataBlock>> = remember {
mutableStateOf(listOf())
}
LaunchedEffect(Unit) {
delay(1000)
itemsState.value = MutableList<TestDataBlock>(30) { rowIndex ->
val id = rowIndex
TestDataBlock(id = id.toString(), data = 1)
}
}
List(dataItems = itemsState.value)
}
#Preview
#Composable
fun List(
dataItems: List<TestDataBlock> = listOf(TestDataBlock("1",1), TestDataBlock("2",2))
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
) {
itemsIndexed(items = dataItems,
key = { _, item ->
item.id
}
) { _, rowItem ->
drawElement(rowItem)
}
}
}
#Composable
fun drawElement(rowItem: TestDataBlock) {
Text(text = "${rowItem.id}")
LazyRow() {
itemsIndexed(items = rowItem.testDataItems,
key = { _, item ->
item.id
}
) { _, item ->
Text(text = "${item.id }", color = Color.Black, modifier = Modifier.width(100.dp))
}
}
}
TestDataBlock.kt
#Immutable
data class TestDataBlock(
val id: String,
val data: Int,
) {
val testDataItems: List<TestDataItem> = (0..20).toList().map{ TestDataItem(it.toString()) }
}
TestDataItem.kt
#Immutable
data class TestDataItem(
val id: String
)
I am trying to update the title of the TopAppBar based on a live data in the ViewModel, which I update on different screens. It looks like the live data is getting updated properly, but the update is not getting reflected on the title of the TopAppBar. Here is the code:
class MainActivity : ComponentActivity() {
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LazyVerticalGridActivityScreen()
}
}
}
#ExperimentalFoundationApi
#Composable
fun LazyVerticalGridActivityScreen(destinationViewModel: DestinationViewModel = viewModel()) {
val navController = rememberNavController()
var canPop by remember { mutableStateOf(false) }
// getting the latest title value from the view model
val title: String by destinationViewModel.title.observeAsState("")
Log.d("MainActivity_title", title) // not getting called
navController.addOnDestinationChangedListener { controller, _, _ ->
canPop = controller.previousBackStackEntry != null
}
val navigationIcon: (#Composable () -> Unit)? =
if (canPop) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
}
}
} else {
null
}
Scaffold(
topBar = {
TopAppBar(title = { Text(title) }, navigationIcon = navigationIcon) // updating the title
},
content = {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController) }
}
}
}
)
}
#ExperimentalFoundationApi
#Composable
fun HomeScreen(navController: NavHostController, destinationViewModel: DestinationViewModel = viewModel()) {
val destinations = DestinationDataSource().loadData()
// updating the title in the view model
destinationViewModel.setTitle("Lazy Grid Layout")
LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = 140.dp),
contentPadding = PaddingValues(8.dp)
) {
itemsIndexed(destinations) { index, destination ->
Row(Modifier.padding(8.dp)) {
ItemLayout(destination, index, navController)
}
}
}
}
#Composable
fun ItemLayout(
destination: Destination,
index: Int,
navController: NavHostController
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.background(MaterialTheme.colors.primaryVariant)
.fillMaxWidth()
.clickable {
navController.navigate("details/$index")
}
) {
Image(
painter = painterResource(destination.photoId),
contentDescription = stringResource(destination.nameId),
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
Text(
text = stringResource(destination.nameId),
color = Color.White,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
}
#Composable
fun DetailsScreen(
index: String,
navController: NavController,
destinationViewModel: DestinationViewModel = viewModel()
) {
val dataSource = DestinationDataSource().loadData()
val destination = dataSource[index.toInt()]
val destinationName = stringResource(destination.nameId)
val destinationDesc = stringResource(destination.descriptionId)
val destinationImage = painterResource(destination.photoId)
// updating the title in the view model
destinationViewModel.setTitle("Destination Details")
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Image(
painter = destinationImage,
contentDescription = destinationName,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
text = destinationName,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 16.dp)
)
Text(text = destinationDesc, lineHeight = 24.sp)
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = {
navController.navigate("home") {
popUpTo("home") { inclusive = true }
}
},
modifier = Modifier.padding(top = 24.dp)
) {
Image(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryVariant),
modifier = Modifier.size(20.dp)
)
Text("Back to Destinations", modifier = Modifier.padding(start = 16.dp))
}
}
}
}
}
EDIT: The ViewModel
class DestinationViewModel : ViewModel() {
private var _title = MutableLiveData("")
val title: LiveData<String>
get() = _title
fun setTitle(newTitle: String) {
_title.value = newTitle
Log.d("ViewModel_title", _title.value.toString())
Log.d("ViewModelTitle", title.value.toString())
}
}
Can anyone please help to find the bug? Thanks!
Edit:
Here is the GitHub link of the project: https://github.com/rawhasan/compose-exercise-lazy-vertical-grid
The reason it's not working is because those are different objects created in different scopes.
When you're using a navController, each destination will have it's own scope for viewModel() creation. By the design you may have a view model for each destination, like HomeViewModel, DestinationViewModel, etc
You can't access an other destination view model from current destination scope, as well as you can't access view model from the outer scope(which you're trying to do)
What you can do, is instead of trying to retrieve it with viewModel(), you can pass outer scope view model to your composable:
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController, destinationViewModel) }
}
Check out more details about viewModel() in the documentation
Another problem with your code is that you're calling destinationViewModel.setTitle("Lazy Grid Layout") inside composable function. So this code will be called many times, which may lead to recomposition.
To call any actions inside composable, you need to use side-effects. LaunchedEffect in this case:
LaunchedEffect(Unit) {
destinationViewModel.setTitle("Destination Details")
}
This will be called only once after view appear. If you need to call it more frequently, you need to specify key instead of Unit, so it'll be recalled each time when the key changes
You might wanna have a look here https://stackoverflow.com/a/68671477/15880865
Also, you do not need to paste all the code in the question. Just the necessary bit. We shall ask for it if something is missing in the question. Just change your LiveData to mutableStateOf and then let me know if that fixed your problem. Also, you do not need to call observeAsState after modifying the type. Just refer to the link it contains all the info.
Thanks