Getting scroll direction for LazyList Jetpack Compose [duplicate] - android

For a LazyRow, or Column, how to I know whether the user has scrolled left or right ( or up or... you know). We do not need callbacks in compose for stuff like that, since mutableStateOf objects always anyway trigger recompositions so I just wish to know a way to store it in a variable. Okay so there's lazyRowState.firstVisibleItemScrollOffset, which can be used to mesaure it in a way, but I can't find a way to store its value first, and then subtract the current value to retrieve the direction (based on positive or negative change). Any ideas on how to do that, thanks

Currently there is no built-in function to get this info from LazyListState.
You can use something like:
#Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
Then just use listState.isScrollingUp() to get the info about the scroll.
This snippet is used in a google codelab.

Got it
{ //Composable Scope
val lazyRowState = rememberLazyListState()
val pOffset = remember { lazyRowState.firstVisibleItemScrollOffset }
val direc = lazyRowState.firstVisibleItemScrollOffset - pOffset
val scrollingRight /*or Down*/ = direc > 0 // Tad'aa
}

Related

How to get LazyRow's visible items in viewmodel

I have created a LazyRow in jetpack compose. At a certain point in the viewmodel, I want to get the list of currently visible items from that LazyRow.
I know that I can get the list of visible items in the Composable function using the following code:
val listState = rememberLazyListState()
val visibleItemIds = remember {
derivedStateOf { listState.layoutInfo.visibleItemsInfo.map { it.key.toString() } }
}
The problem is how can I pass this data to the viewmodel during a viewmodel event (not a button click etc)
You can add a side effect to know what are the visible items in any time.
LaunchedEffect(visibleItemIds){
//update your viewModel
}
You can also have a List<T> instead of State<List<String>> as in your code with:
val state = rememberLazyListState()
val visibleItemIds: List<Int> by remember {
derivedStateOf {
val layoutInfo = state.layoutInfo
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) {
emptyList()
} else {
visibleItemsInfo.map { it.index }
}
}
}
Also note that visibleItemsInfo returns also the partially visible items.

Android Paging 3 library loading infinitely without scroll with Jetpack Compose

I'm attempting to make a paged list of books using Jetpack Compose and Android's Paging 3 library. I am able to make the paged list and get the data fine, but the load() function of my paging data source is being called infinitely, without me scrolling the screen.
My paging data source looks like this:
class GoogleBooksBookSource #Inject constructor(
private val googleBooksRepository: GoogleBooksRepository,
private val query: String
): PagingSource<Int, Book>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Book> {
val position = params.key ?: 0
return try {
val response = googleBooksRepository.searchForBookStatic(query, position)
if (response is Result.Success) {
LoadResult.Page(
data = response.data.items,
prevKey = if (position == 0) null else position - 1,
nextKey = if (response.data.totalItems == 0) null else position + 1
)
} else {
LoadResult.Error(Exception("Error loading paged data"))
}
} catch (e: Exception) {
Log.e("PagingError", e.message.toString())
return LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Book>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
and this is the UI:
Column() {
// other stuff
LazyColumn(
modifier = Modifier.padding(horizontal = 24.dp),
content = {
for (i in 0 until searchResults.itemCount) {
searchResults[i]?.let { book ->
item {
BookCard(
book = book,
navigateToBookDetail = { navigateToBookDetail(book.id) }
)
}
}
}
}
)
}
As far as I can tell, the data loads correctly and in the correct order, but when I log the API request URLs, it's making infinite calls with an increasing startIndex each time. That would be fine if I was scrolling, since Google Books searches often return thousands of results, but it does this even if I don't scroll the screen.
The issue here was the way I was creating elements in the LazyColumn - it natively supports LazyPagingItem but I wasn't using that. Here is the working version:
LazyColumn(
modifier = Modifier.padding(horizontal = 24.dp),
state = listState,
content = {
items(pagedSearchResults) { book ->
book?.let {
BookCard(
book = book,
navigateToBookDetail = { navigateToBookDetail(book.id) }
)
}
}
}
)
In your original example, you have to use peek to check for non-null and access the list as you do only inside item block, which is lazy. Otherwise the paging capabilities will be lost and it will load the entire dataset in one go.

jetpack compose: scroll to bottom listener (end of list)

I am wondering if it is possible to get observer inside a #Compose function when the bottom of the list is reached (similar to recyclerView.canScrollVertically(1))
Thanks in advance.
you can use rememberLazyListState() and compare
scrollState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == scrollState.layoutInfo.totalItemsCount - 1
How to use example:
First add the above command as an extension (e.g., extensions.kt file):
fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
Then use it in the following code:
#Compose
fun PostsList() {
val scrollState = rememberLazyListState()
LazyColumn(
state = scrollState,),
) {
...
}
// observer when reached end of list
val endOfListReached by remember {
derivedStateOf {
scrollState.isScrolledToEnd()
}
}
// act when end of list reached
LaunchedEffect(endOfListReached) {
// do your stuff
}
}
For me the best and the simplest solution was to add LaunchedEffect as the last item in my LazyColumn:
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(someItemList) { item ->
MyItem(item = item)
}
item {
LaunchedEffect(true) {
//Do something when List end has been reached
}
}
}
I think, based on the other answer, that the best interpretation of recyclerView.canScrollVertically(1) referred to bottom scrolling is
fun LazyListState.isScrolledToTheEnd() : Boolean {
val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
return lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset
}
Starting from 1.4.0-alpha03 you can use LazyListState#canScrollForward to check if you are at the end of the list.
Something like:
val state = rememberLazyListState()
val isAtBottom = !state.canScrollForward
LaunchedEffect(isAtBottom){
if (isAtBottom) doSomething()
}
Before this release you can use the LazyListState#layoutInfo that contains information about the visible items. Note the you should use derivedStateOf to avoid redundant recompositions.
Use something:
#Composable
private fun LazyListState.isAtBottom(): Boolean {
return remember(this) {
derivedStateOf {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
false
} else {
val lastVisibleItem = visibleItemsInfo.last()
val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset
(lastVisibleItem.index + 1 == layoutInfo.totalItemsCount &&
lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)
}
}
}.value
}
The code above checks not only it the last visibile item == last index in the list but also if it is fully visible (lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight).
And then:
val state = rememberLazyListState()
var isAtBottom = state.isAtBottom()
LaunchedEffect(isAtBottom){
if (isAtBottom) doSomething()
}
LazyColumn(
state = state,
){
//...
}
Simply use the firstVisibleItemIndex and compare it to your last index. If it matches, you're at the end, else not. Use it as lazyListState.firstVisibleItemIndex
Found a much simplier solution than other answers. Get the last item index of list. Inside itemsIndexed of lazyColumn compare it to lastIndex. When the end of list is reached it triggers if statement. Code example:
LazyColumn(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
itemsIndexed(events) { i, event ->
if (lastIndex == i) {
Log.e("console log", "end of list reached $lastIndex")
}
}
}

How to retrieve the scrolling direction for LazyRow

For a LazyRow, or Column, how to I know whether the user has scrolled left or right ( or up or... you know). We do not need callbacks in compose for stuff like that, since mutableStateOf objects always anyway trigger recompositions so I just wish to know a way to store it in a variable. Okay so there's lazyRowState.firstVisibleItemScrollOffset, which can be used to mesaure it in a way, but I can't find a way to store its value first, and then subtract the current value to retrieve the direction (based on positive or negative change). Any ideas on how to do that, thanks
Currently there is no built-in function to get this info from LazyListState.
You can use something like:
#Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
Then just use listState.isScrollingUp() to get the info about the scroll.
This snippet is used in a google codelab.
Got it
{ //Composable Scope
val lazyRowState = rememberLazyListState()
val pOffset = remember { lazyRowState.firstVisibleItemScrollOffset }
val direc = lazyRowState.firstVisibleItemScrollOffset - pOffset
val scrollingRight /*or Down*/ = direc > 0 // Tad'aa
}

Jetpack Compose LazyRow scroll with snap only to start of next or previous element

Is there a way to horizontally scroll only to start or specified position of previous or next element with Jetpack Compose?
Snappy scrolling in RecyclerView
You can check the scrolling direction like so
#Composable
private fun LazyListState.isScrollingUp(): Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex > firstVisibleItemIndex
} else {
previousScrollOffset >= firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
}
}
}.value
}
Of course, you will need to create a rememberLazyListState(), and then pass it to the list as a parameter.
Then, based upon the scrolling direction, you can call lazyListState.scrollTo(lazyListState.firstVisibleItemIndex + 1) in a coroutine (if the user is scrolling right), and appropriate calls for the other direction.
(Example for a horizontal LazyRow)
You could do a scroll to the next or previous item to create a snap effect. Check the offset of the first visible item to see which item of the list takes up more screen space and then scroll left or right to the most visible one.
#Composable
fun SnappyLazyRow() {
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyRow(
state = listState,
modifier = Modifier.fillMaxSize(),
content = {
items(/*list of items*/) { index ->
/* Item view */
if(!listState.isScrollInProgress){
if(listState.isHalfPastItemLeft())
coroutineScope.scrollBasic(listState, left = true)
else
coroutineScope.scrollBasic(listState)
if(listState.isHalfPastItemRight())
coroutineScope.scrollBasic(listState)
else
coroutineScope.scrollBasic(listState, left = true)
}
}
})
}
private fun CoroutineScope.scrollBasic(listState: LazyListState, left: Boolean = false){
launch {
val pos = if(left) listState.firstVisibleItemIndex else listState.firstVisibleItemIndex+1
listState.animateScrollToItem(pos)
}
}
#Composable
private fun LazyListState.isHalfPastItemRight(): Boolean {
return firstVisibleItemScrollOffset > 500
}
#Composable
private fun LazyListState.isHalfPastItemLeft(): Boolean {
return firstVisibleItemScrollOffset <= 500
}

Categories

Resources