Is there a way to slow down the sliding interval when auto-scrolling to next page in Accompanist Pager?
In the latest version, support for the same has been removed.
The animationSpec, initialVelocity and skipPages parameters on animateScrollToPage() have been removed. The lazy components handle this automatically.
Currently, the auto scroll to next page moves very quickly.
pager.animateScrollToPage(pageIndex)
Accompanist Pager version used: 0.22.1-rc
An answer can be found here.
This animates Card components that auto-scroll forward then back to the start, in a loop.
States
val pagerState = rememberPagerState()
val isDragged by pagerState.interactionSource.collectIsDraggedAsState()
var pageSize by remember { mutableStateOf(IntSize.Zero) }
val lastIndex by remember(pagerState.currentPage) {
derivedStateOf {pagerState.currentPage == items.size - 1 }
}
Scroll
if (!isDragged) {
LaunchedEffect(Unit) {
while (true) {
yield()
delay(2000)
pagerState.animateScrollBy(
value = if (lastIndex) - (pageSize.width.toFloat() * items.size) else pageSize.width.toFloat(),
animationSpec = tween(if (lastIndex) 2000 else 1400)
)
}
}
}
pageSize is assigned with an onSizeChanged Modifier on the Card
HorizontalPager(
count = items.size,
state = pagerState
) { pageIndex ->
Card(modifier = modifier.onSizeChanged { pageSize = it } ) { /* card content */ }
}
Related
Currently Paging library handles when to load the data automatically when a user scrolls down. But what if you want to give the user full authority for when they want the next page of data to be loaded i.e when button is clicked show next page of movies. How can you handle this in Paging library? See below how I've implemented the paging to load data as a user scrolls down
Here below this how I implemented the Paging to load next page when user scrolls down
class MoviesPagingDataSource(
private val repo: MoviesRepository,
) : PagingSource<Int, Movies>() {
override fun getRefreshKey(state: PagingState<Int, Movies>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movies> {
return try {
val nextPageNumber = params.key ?: 0
val response = repo.getMovies(page = nextPageNumber, size = 10)
LoadResult.Page(
data = response.content,
prevKey = null,
nextKey = if (response.content.isNotEmpty()) response.number + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
This is how I emit the state in ViewModel for the UI to observe
#HiltViewModel
class MoviesViewModel #Inject constructor(
private val moviesRepository: MoviesRepository
): ViewModel {
....
//emitting the data to the UI to observe
val moviesPagingDataSource = Pager(PagingConfig(pageSize = 10)) {
MoviesPagingDataSource(moviesRepository)
}.flow.cachedIn(viewModelScope)
}
How I'm observing it in the UI
#Composable
fun MoviesList(viewModel: MoviesViewModel) {
val moviesList = viewModel.moviesPagingDataSource.collectAsLazyPagingItems()
LazyColumn {
items(moviesList) { item ->
item?.let { MoviesCard(movie = it) }
}
when (moviesList.loadState.append) {
is LoadState.NotLoading -> Unit
LoadState.Loading -> {
item {
LoadingItem()
}
}
is LoadState.Error -> {
item {
ErrorItem(message = "Some error occurred")
}
}
}
when (moviesList.loadState.refresh) {
is LoadState.NotLoading -> Unit
LoadState.Loading -> {
item {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Center
) {
CircularProgressIndicator()
}
}
}
is LoadState.Error -> TODO()
}
}
}
So currently I'm adding 1 to the previous page every time a user clicks the button to load more movies then saving this movies to the list state. Also making sure the current page is greater than or equal to total pages before loading more data and adding to the list state of previous loaded movies
you can use Channel to block LoadResult returning from load, when user clicks the button, send an element to the Channel. here is the simple
class MoviesPagingDataSource(
private val repo: MoviesRepository
) : PagingSource<Int, Movies>() {
private val channel: Channel<Unit> = Channel(1, BufferOverflow.DROP_LATEST)
override fun getRefreshKey(state: PagingState<Int, Movies>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movies> {
return try {
val nextPageNumber = params.key ?: 0
// load initial page
if (nextPageNumber == 0) loadNextPage()
val response = repo.getMovies(page = nextPageNumber, size = 10)
/*
* block next page data return, when user click the button,
* call loadNextPage. if you don't want to automatically
* request data as the user scrolls toward the end of the
* loaded data. put this line above the repo.getMovies.
**/
channel.receive()
LoadResult.Page(
data = response.content,
prevKey = null,
nextKey = if (response.content.isNotEmpty()) response.number + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
suspend fun loadNextPage() {
channel.send(Unit)
}
}
I have an animateDpAsState(..), whenever this animation is triggered it changes the Modifier.size(value) of an Image(...) thus causing recomposition.
Is there a way to skip composition phase for this specific scenario? Allowing an image to change its size?
You can do it using Modifier.drawWithContent, Modifier.drawBeheind or using Canvas which is a Spacer with Modifier.drawBehind. Modifiers with lambda trigger Layout, Layout->Draw or Draw phases skipping Composition as in this answer.
The snippet below changes size with animation and if you want size changes to be applied from center you can add translate either
#Composable
private fun ImageSizeAnimationSample() {
val painter = painterResource(id = R.drawable.landscape1)
var enabled by remember { mutableStateOf(true) }
val sizeDp by animateDpAsState(if (enabled) 200.dp else 0.dp)
val density = LocalDensity.current
val context = LocalContext.current
SideEffect {
println("🔥 Composing...")
Toast.makeText(context, "Composing...", Toast.LENGTH_SHORT).show()
}
Canvas(modifier = Modifier.size(200.dp)) {
val dimension = density.run { sizeDp.toPx() }
with(painter) {
draw(size = Size(dimension, dimension))
}
}
Button(onClick = { enabled = !enabled }) {
Text("Enabled: $enabled")
}
}
With translation
Canvas(modifier = Modifier.size(200.dp)) {
val dimension = density.run { sizeDp.toPx() }
with(painter) {
translate(left = (size.width - dimension) / 2, top = (size.height - dimension) / 2) {
draw(size = Size(dimension, dimension))
}
}
}
In these examples only one recomposition is triggered for animation because
val sizeDp by animateDpAsState(if (enabled) 200.dp else 0.dp)
reads enabled value but you can handle animations with Animatable which won't trigger any recomposition either.
#Composable
private fun ImageSizeAnimationWithAnimatableSample() {
val painter = painterResource(id = R.drawable.landscape1)
val animatable = remember { Animatable(0f) }
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
SideEffect {
println("🔥 Composing...")
Toast.makeText(context, "Composing...", Toast.LENGTH_SHORT).show()
}
Canvas(modifier = Modifier.size(200.dp)) {
with(painter) {
val dimension = size.width * animatable.value
translate(left = (size.width - dimension) / 2, top = (size.height - dimension) / 2) {
draw(size = Size(dimension, dimension))
}
}
}
Button(onClick = {
coroutineScope.launch {
val value = animatable.value
if(value == 1f){
animatable.animateTo(0f)
}else {
animatable.animateTo(1f)
}
}
}) {
Text("Animate")
}
}
I found the solution! To skip recomposition but still affect things around the layout, you can do it do it in layout phase, otherwise move it to Draw phase!
Apply this Modifier to the Image(...) modifier parameter.
Modifier.layout { measurable, constraints ->
val size = animationSize.toPx() // getting the size of the current animation
val placeable = measurable.measure(Constraints.fixed(size.toInt(), size.toInt())) // setting the actual constraints of the image
// Set the layout with the same width and height of the image.
// Inside the layout we will place the image. This layout function is like a "box"
layout(placeable.width,placeable.height) {
// And then we will place the image inside the "box"
placeable.placeRelative(0, 0)
}
}
I made LazyColumn with vertical scroll bar and it's work good, but when i scroll mouse, column just jumping(not smooth), but when I scroll vert. bar, it's smooth
#ExperimentalFoundationApi
#OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
#Composable
fun App() {
Box(modifier = Modifier.fillMaxSize().padding(10.dp)
) {
val animatedpr = remember { androidx.compose.animation.core.Animatable(initialValue = 0.8f) }
val stateVertical = rememberLazyListState(0)
LaunchedEffect(Unit){animatedpr.animateTo(targetValue = 1f, animationSpec = tween(300, easing = LinearEasing))}
LazyColumn(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, state = stateVertical) {
items(1100) {
OutlinedCard(modifier = Modifier.size(500.dp, 100.dp).padding(20.dp).animateItemPlacement().graphicsLayer(scaleY = animatedpr.value, scaleX = animatedpr.value)) {
}
}
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
adapter = rememberScrollbarAdapter(
scrollState = stateVertical
)
)
}
}
better sollution
if(it.changes.first().scrollDelta.y == 1.0f){
scope.launch { stateVertical.animateScrollBy(200.0f) }
}
else{
scope.launch {
scope.launch { stateVertical.animateScrollBy(-200.0f) }
}
}
The problem is that you use a single scroll state for two different scroll views. This is causing the jumping while recomposing.
I fixed it by adding "scrollhandler"(onPointerEvent(PointerEventType.Scroll))
val scope = rememberCoroutineScope() // coroutine for scroling(idk, i know coroutines very bad)
val stateVertical = rememberLazyListState(0)
.....
LazyColumn(modifier = Modifier.fillMaxSize().onPointerEvent(PointerEventType.Scroll){
var currentItem = stateVertical.layoutInfo.visibleItemsInfo[0].index
val itemsToScrolling = stateVertical.layoutInfo.visibleItemsInfo.size/2 // how many items we scrolling
if(it.changes.first().scrollDelta.y == 1.0f){ // scroll down
scope.launch { stateVertical.animateScrollToItem(currentItem+itemsToScrolling) }
}
else{ // scroll up
if(currentItem < itemsToScrolling){currentItem = itemsToScrolling} // because we cannot animate to negative number
scope.launch { stateVertical.animateScrollToItem(currentItem-itemsToScrolling) }
}
}, state = stateVertical){
*items*
}
I find myself in this situation quite often. I have some value like plates in the sample below, and I want to show/hide it depending on if its null or not. But hiding it always fails since nothing is rendered whenever its null, and the animation just snaps to empty nothingness.
How can I make this work? Id like to keep plates around until the animation finishes.
AnimatedVisibility(
visible = plates != null,
content = {
if (plates != null) {
// Render plates
} else {
// The animation snaps to nothingness, as opposed to animating out
}
})
This is what I ended up doing, and it works as expected!
#Composable
inline fun <T> AnimatedValueVisibility(
value: T?,
enter: EnterTransition = fadeIn(
animationSpec = FloatSpec
) + expandVertically(
animationSpec = SizeSpec,
clip = false
),
exit: ExitTransition = fadeOut(
animationSpec = FloatSpec
) + shrinkVertically(
animationSpec = SizeSpec,
clip = false
),
crossinline content: #Composable (T) -> Unit
) {
val ref = remember {
Ref<T>()
}
ref.value = value ?: ref.value
AnimatedVisibility(
visible = value != null,
enter = enter,
exit = exit,
content = {
ref.value?.let { value ->
content(value)
}
}
)
}
Animation always requires content even when there is nothing to show. Just display a Surface when the content is null (or empty):
AnimatedVisibility(
visible = plates != null,
content = {
if (plates != null) {
// Render plates
} else {
Surface(modifier = Modifier.fillMaxSize()) { }
}
})
I have UI like this:
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize(1F)
.padding(horizontal = 16.dp)
.verticalScroll(scrollState)
) {
TextField(...)
// multiple textfields
TextField(
//...
modifier = Modifier.focusOrder(countryFocus).onFocusChanged {
if(it == FocusState.Active) {
// scroll to this textfield
}
},
)
}
I have multiple TextFields in this column and when one of them is focused I want to scroll Column to it. There is a method in scrollState scrollState.smoothScrollTo(0f) but I have no idea how to get a focused TextField position.
Update:
It seems that I've found a working solution. I've used onGloballyPositioned and it works. But I'm not sure if it the best way of solving this.
var scrollToPosition = 0.0F
TextField(
modifier = Modifier
.focusOrder(countryFocus)
.onGloballyPositioned { coordinates ->
scrollToPosition = scrollState.value + coordinates.positionInRoot().y
}
.onFocusChanged {
if (it == FocusState.Active) {
scope.launch {
scrollState.smoothScrollTo(scrollToPosition)
}
}
}
)
There is a new thing in compose called RelocationRequester. That solved the problem for me. I have something like this inside of my custom TextField.
val focused = source.collectIsFocusedAsState()
val relocationRequester = remember { RelocationRequester() }
val ime = LocalWindowInsets.current.ime
if (ime.isVisible && focused.value) {
relocationRequester.bringIntoView()
}
Also you can use BringIntoViewRequester
//
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val coroutineScope = rememberCoroutineScope()
//--------
TextField( ..., modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent {
if (it.isFocused) {
coroutineScope.launch {
bringIntoViewRequester.bringIntoView()
}
}
}
It seems that using LazyColumn and LazyListState.animateScrollToItem() instead of Column could be a good option for your case.
Reference: https://developer.android.com/jetpack/compose/lists#control-scroll-position
By the way, thank you for the information about onGloballyPositioned() modifier. I was finding a solution for normal Column case. It saved me a lot of time!
Here's some code I used to make sure that the fields in my form were not cut off by the keyboard:
From: stack overflow - detect when keyboard is open
enum class Keyboard {
Opened, Closed
}
#Composable
fun keyboardAsState(): State<Keyboard> {
val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
val view = LocalView.current
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
Keyboard.Opened
} else {
Keyboard.Closed
}
}
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
}
}
return keyboardState
}
and then in my composable:
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
val isKeyboardOpen by keyboardAsState()
if (isKeyboardOpen == Keyboard.Opened) {
val view = LocalView.current
val screenHeight = view.rootView.height
scope.launch { scrollState.scrollTo((screenHeight * 2)) }
}
Surface(modifier = Modifier
.fillMaxHeight()
.verticalScroll(scrollState),
) {
//Rest of your Composables, Columns, Rows, TextFields, Buttons
//add this so the screen can scroll up and keyboard cannot cover the form fields - Important!
/*************************************************/
if (isKeyboardOpen == Keyboard.Opened) {
Spacer(modifier = Modifier.height(140.dp))
}
}
Hope it helps someone. I was using:
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scope = rememberCoroutineScope()
val view = LocalView.current
DisposableEffect(view) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
scope.launch { bringIntoViewRequester.bringIntoView() }
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
Surface(modifier.bringIntoViewRequester(bringIntoViewRequester)) {
///////////rest of my composables
}
But this did not work.