Swipe sensitivity for HorizontalPager in Compose - android

I implemented a simple HorizontalPager which works mostly as expected. The app I develop is for one specific device, a 8" Tablet in landscape mode. At the moment it is required to swipe more than 50% of the screen-width to get to the next/prev page. This is a very long swipe and I would like to reduce that to make changing pages easier...
I played around with fling behavior and tried to manually change pages when the offset changed or to intercept the touchevents...Nothing really lead to the desired behavior.
Since the "problem" seems so simple, I really hope that I have just overseen something. Do you have an idea what I could try?

This solved my problem, you can edit minFlingDistanceDp to change the sensitivity:
HorizontalPager(
modifier = Modifier.fillMaxSize(),
state = pagerState,
count = noOfPages,
flingBehavior = flingBehavior(pagerState = pagerState, noOfPages = noOfPages)
) { page ->
//Content
}
val minFlingDistanceDp = 150.dp
#OptIn(ExperimentalPagerApi::class, dev.chrisbanes.snapper.ExperimentalSnapperApi::class)
#Composable
fun flingBehavior(pagerState: PagerState, noOfPages: Int): FlingBehavior {
var currentPageIndex = remember { pagerState.currentPage }
return PagerDefaults.flingBehavior(
state = pagerState,
snapIndex = { layoutInfo, _, _ ->
val distanceToStartSnap = layoutInfo.distanceToIndexSnap(currentPageIndex)
currentPageIndex = when {
distanceToStartSnap < -(minFlingDistanceDp.value) -> {
(currentPageIndex + 1).coerceAtMost(noOfPages - 1)
}
distanceToStartSnap > minFlingDistanceDp.value -> {
(currentPageIndex - 1).coerceAtLeast(0)
}
else -> {
currentPageIndex
}
}
currentPageIndex
}
)
}

Related

Paging 3 list auto refresh on navigation back in jetpack compose navigation

I am using Jetpack Compose, along with Paging 3 library & Jetpack Navigation. The issue I am facing is I have a LazyList which is fetching data from remote source using paging library.
ViewModel
fun getImages(): Flow<PagingData<ObjectImage>> = Pager(
PagingConfig(PAGE_SIZE, enablePlaceholders = false)
) { DataHome(RANDOM) }.flow.cachedIn(viewModelScope)
HomeView
val images = viewModelHome.getImages().collectAsLazyPagingItems()
LazyColumn {
...
}
Now whats happening is when I navigate to another View using navHostController.navigate() and then press back to get to HomeView... the LazyColumn resets itself & start loading items again from network.
So I am stuck with this issue. I tried manually caching in viewModel variable... though it works but it screws up SwipeRefresh (which stops showing refresh state)
data.apply {
when {
// refresh
loadState.refresh is LoadState.Loading -> {
ItemLoading()
}
// reload
loadState.append is LoadState.Loading -> {...}
// refresh error
loadState.refresh is LoadState.Error -> {...}
// reload error
loadState.append is LoadState.Error -> {...}
}
}
implementation("androidx.paging:paging-runtime-ktx:3.1.0")
implementation("androidx.paging:paging-compose:1.0.0-alpha14")
Is this an issue with PagingLibrary which is still in alpha??
Update 1 (I am not sure if this is a good solution, but I am solving
the swipe refresh issue as follows)
// get images
var images: Flow<PagingData<ObjectImage>> = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
// reload items
fun reload(){
images = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
}
// and rather than calling .refresh() method on lazy items... I am calling viewModel.reload()
The problem is that you are creating new Pager every time you call getImages(), which is every time your composable recomposes, that's not how it's supposed to be done.
You should make it a val items = Pager(... for the caching to work.
For the screwed up SwipeRefresh, how do you implement it? There is a refresh() method on LazyPagingItems, you should use that.
EDIT: Ok, so based on the coments and edits to your question:
In your viewmodel, do as I suggested before:
val items = Pager( // define your pager here
Your composable can then look like this:
#Composable
fun Screen() {
val items = viewModel.items.collectAsLazyPagingItems()
val state = rememberSwipeRefreshState(
isRefreshing = items.loadState.refresh is LoadState.Loading,
)
SwipeRefresh(
modifier = Modifier.fillMaxSize(),
state = state,
// use the provided LazyPagingItems.refresh() method,
// no need for custom solutions
onRefresh = { items.refresh() }
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
// display the items only when loadState.refresh is not loading,
// as you wish
if (items.loadState.refresh is LoadState.NotLoading) {
items(items) {
if (it != null) {
Text(
modifier = Modifier.padding(16.dp),
text = it,
)
}
}
// you can also add item for LoadState.Error, anything you want
if (items.loadState.append is LoadState.Loading) {
item {
Box(modifier = Modifier.fillMaxWidth()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp)
)
}
}
}
}
// if the loadState.refresh is Loading,
// display just single loading item,
// or nothing at all (SwipeRefresh already indicates
// refresh is in progress)
else if (items.loadState.refresh is LoadState.Loading) {
item {
Box(modifier = Modifier.fillParentMaxSize()) {
Text(
text = "Refreshing",
modifier = Modifier.align(Alignment.Center))
}
}
}
}
}
}

Is there any way to make two horizontal pager of accompanist library to work synchronously?

What I am trying to achieve is if there are two horizontal pagers, then on swiping top one to left then the bottom horizontal pager should swipe to right and vice-versa, have tried using pagerState scrollBy method but not getting desired output
First of all, you need to determine which pager is scrolled and which one should follow it. This can be done with isScrollInProgress, I use it inside derivedStateOf to avoid unnecessary re-compositions.
Then I run LaunchedEffect. I'm passing pair of pager states in needed order, or null, as a key, so LaunchedEffect will be re-launched when scrolling stops/starts. Using snapshotFlow, it is possible to track changes in the result of a block whose calculations depend on state changes.
PagerState has scroll position information in the properties currentPage and currentPageOffset. scrollToPage takes only 0..1 values for page offset, but currentPageOffset can be less than zero when scrolling backward.
Suppose currentPage = 2 and currentPageOffset = -0.1. In this case, I will get 1.9 in pagePart, and I need to split it back to get 1 and 0.9. To do this I use divideAndRemainder: it will return a list of the form listOf(1.0, 0.9).
Column {
val count = 10
val firstPagerState = rememberPagerState()
val secondPagerState = rememberPagerState()
val scrollingFollowingPair by remember {
derivedStateOf {
if (firstPagerState.isScrollInProgress) {
firstPagerState to secondPagerState
} else if (secondPagerState.isScrollInProgress) {
secondPagerState to firstPagerState
} else null
}
}
LaunchedEffect(scrollingFollowingPair) {
val (scrollingState, followingState) = scrollingFollowingPair ?: return#LaunchedEffect
snapshotFlow { scrollingState.currentPage + scrollingState.currentPageOffset }
.collect { pagePart ->
val divideAndRemainder = BigDecimal.valueOf(pagePart.toDouble())
.divideAndRemainder(BigDecimal.ONE)
followingState.scrollToPage(
divideAndRemainder[0].toInt(),
divideAndRemainder[1].toFloat(),
)
}
}
HorizontalPager(
count = count,
state = firstPagerState,
modifier = Modifier.weight(1f)
) {
Text(it.toString())
}
HorizontalPager(
count = count,
state = secondPagerState,
modifier = Modifier.weight(1f)
) {
Text(it.toString())
}
}
Result:

Android Compose remember affects other remember change

I have the next hierarchy:
WalletDetailsScreen
WalletDetailsView
SubWalletView
DefaultOutlinedButton
1st remember domainsVisible is declared in WalletDetailsScreen. Callback is propagated to DefaultOutlinedButton's onClick.
2nd remember copyToClipboardClicked is declared in SubWalletView.
What happens:
User opens the screen.
User taps copy button at first (SubWalletView). (2nd remember)
User taps DefaultOutlinedButton then. 1st remember is changed AND 2ND ONE IS CHANGED AS WELL!
Code:
#Composable
fun WalletDetailsScreen(
snackbarController: SnackbarController,
wallet: Wallet,
onNavIconClicked: () -> Unit
) {
// CHANGING THIS REMEMBER CHANGES 2ND ONE (BUT ONLY IF 2ND WAS FIRED AT LEAST ONCE)
val domainsVisible = rememberMutableStateOf(key = "domains_visible_btn", value = false)
WalletDetailsView(
snackbarController = snackbarController,
wallet = wallet,
domainsVisible = domainsVisible.value,
domainsCount = 0
) {
domainsVisible.toggle()
}
}
#Composable
private fun WalletDetailsView(
snackbarController: SnackbarController,
wallet: Wallet,
domainsVisible: Boolean,
domainsCount: Int,
onDomainsVisibilityClicked: () -> Unit
) {
Column {
wallet.subWallets.forEach { subWallet ->
SubWalletView(snackbarController = snackbarController, subWallet = subWallet)
}
// 1st REMEMBER IS CHANGED HERE
DefaultOutlinedButton(text = text, onClick = onDomainsVisibilityClicked)
}
}
#Composable
private fun SubWalletView(
snackbarController: SnackbarController,
subWallet: SubWallet
) {
// 2ND REMEMBER
val copyToClipboardClicked = rememberMutableStateOf(key = "copy_btn", value = false)
if (copyToClipboardClicked.value) {
CopyToClipboard(text = subWallet.address)
}
// 2ND REMEMBER IS CHANGED HERE
Box(
modifier = Modifier
.clickable { copyToClipboardClicked.toggle() }
.padding(start = 15.dp, top = 5.dp, bottom = 5.dp, end = 5.dp)
) {
// just icon here
}
}
Helpers:
#Composable
fun <T> rememberMutableStateOf(
key: String,
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
) = remember(key) { mutableStateOf(value, policy) }
fun MutableState<Boolean>.toggle() {
value = !value
}
I've tried to add keys to remember but it hasn't helped. Any ideas why changing one remember affects another? This shouldn't happen.
Finally, I figured out what's going on.
Second remember isn't changed actually.
But I rely on it to show shackbar:
if (copyToClipboardClicked.value) {
CopyToClipboard(text = subWallet.address)
ShowSnackbar(...)
copyToClipboardClicked.toggle() // <--- WE NEED THIS
}
And the missed part is that I need to switch flag off. I hadn't done it and that's why the if was triggered on each recomposition.

Bad behavior from Jetpack compose HorizontalPager

I have a problem with the HorizontalPager of the new framework Jetpack Compose. When I associate the HorizontalPager with a TabRow, everything works fine until I reach the last element of my TabRow (Title3) (by clicking the tab row or swipping till the last page). When I am on the last page, if I swip for 1 nanometer to the left the pager switch from last page (Title3) to the previous one (Title2). It is not like other switch between pages, this one is instantaneous when others are continue. I hope I am enough clear to be understandable. Here is the code I use:
val tabTitles = listOf("Title1", "Title2", "Title3")
val pagerState = rememberPagerState(pageCount = tabTitles.size)
val coroutineScope = rememberCoroutineScope()
Column {
TabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(Modifier.pagerTabIndicatorOffset(pagerState, tabPositions))
}) {
tabTitles.forEachIndexed { index, title ->
Tab(
text = { Text(title) },
selected = pagerState.currentPage == index,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } }
)
}
}
HorizontalPager(state = pagerState) { page ->
LazyVerticalGrid(
cells = GridCells.Fixed(2),
contentPadding = PaddingValues(4.dp)
) {
when (page) {
0 -> // Create a list of card for each item
1 -> // Create a list of card for each item
2 -> // Create a list of card for each item
}
}
}
}
I hope someone can help me even if this is a new framework :/
I am open to other solution to implement this kind of pager without the HorizontalPager if the problem come from it and not from my code.
Thanks in advance.
Edit: It was a bug from google accompanist library on version 0.9.0, I solved my problem using the newest version (0.10.0).

How to disable and enable scrolling in LazyColumn/LazyRow in Jetpack Compose?

I want to dynamically enable and disable scrolling programmatically in a LazyColumn.
There don't seem to be any relevant functions on LazyListState or relevant parameters on LazyColumn itself. How can I achieve this in Compose?
There's not (currently) a built-in way to do this, which is a reasonable feature request.
However, the scroll API is flexible enough that we can add it ourselves. Basically, we create a never-ending fake scroll at MutatePriority.PreventUserInput to prevent scrolling, and then use a do-nothing scroll at the same priority to cancel the first "scroll" and re-enable scrolling.
Here are two utility functions on LazyListState to disable/re-enable scrolling, and a demo of them both in action (some imports will be required, but Android Studio should suggest them for you).
Note that because we're taking control of scrolling to do this, calling reenableScrolling will also cancel any ongoing scrolls or flings (that is, you should only call it when scrolling is disabled and you want to re-enable it, not just to confirm that it's enabled).
fun LazyListState.disableScrolling(scope: CoroutineScope) {
scope.launch {
scroll(scrollPriority = MutatePriority.PreventUserInput) {
// Await indefinitely, blocking scrolls
awaitCancellation()
}
}
}
fun LazyListState.reenableScrolling(scope: CoroutineScope) {
scope.launch {
scroll(scrollPriority = MutatePriority.PreventUserInput) {
// Do nothing, just cancel the previous indefinite "scroll"
}
}
}
#Composable
fun StopScrollDemo() {
val scope = rememberCoroutineScope()
val state = rememberLazyListState()
Column {
Row {
Button(onClick = { state.disableScrolling(scope) }) { Text("Disable") }
Button(onClick = { state.reenableScrolling(scope) }) { Text("Re-enable") }
}
LazyColumn(Modifier.fillMaxWidth(), state = state) {
items((1..100).toList()) {
Text("$it", fontSize = 24.sp)
}
}
}
}
Since 1.2.0-alpha01 userScrollEnabled was added to LazyColumn, LazyRow, and LazyVerticalGrid
Answer for 1.1.0 and earlier versions:
#Ryan's solution will also disable programmatically-called scrolling.
Here's a solution proposed by a maintainer in this feature request. It'll disable scrolling, allow programmatic scrolling as well as children view touches.
private val VerticalScrollConsumer = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource) = available.copy(x = 0f)
override suspend fun onPreFling(available: Velocity) = available.copy(x = 0f)
}
private val HorizontalScrollConsumer = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource) = available.copy(y = 0f)
override suspend fun onPreFling(available: Velocity) = available.copy(y = 0f)
}
fun Modifier.disabledVerticalPointerInputScroll(disabled: Boolean = true) =
if (disabled) this.nestedScroll(VerticalScrollConsumer) else this
fun Modifier.disabledHorizontalPointerInputScroll(disabled: Boolean = true) =
if (disabled) this.nestedScroll(HorizontalScrollConsumer) else this
Usage:
LazyColumn(
modifier = Modifier.disabledVerticalPointerInputScroll()
) {
// ...
}
NestedScrollConnection allows you to consume any scroll applied to a lazy column or row. When true, all of the available scroll is consumed. If false, none is consumed and scrolling happens normally. With this information, you can see how this can be extended for slow/fast scrolls by returning the offset multiple by some factor.
fun Modifier.scrollEnabled(
enabled: Boolean,
) = nestedScroll(
connection = object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset = if(enabled) Offset.Zero else available
}
)
it can be used like this:
LazyColumn(
modifier = Modifier.scrollEnabled(
enabled = enabled, //provide a mutable state boolean here
)
){
...
However, this does block programmatic scrolls.

Categories

Resources