From 8e1b5b4803b0da76f0ed5091d3fb1b6c3b2359f5 Mon Sep 17 00:00:00 2001
From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
Date: Tue, 20 Dec 2022 21:16:43 +0700
Subject: [PATCH] Pager: Bring back previous snapping behavior (#8776)

New default LazyList snap behavior is optimized for non-pager use.
---
 .../kanade/presentation/components/Pager.kt   | 81 ++++++++++++++++++-
 1 file changed, 79 insertions(+), 2 deletions(-)

diff --git a/app/src/main/java/eu/kanade/presentation/components/Pager.kt b/app/src/main/java/eu/kanade/presentation/components/Pager.kt
index 285abac68..9e21256e3 100644
--- a/app/src/main/java/eu/kanade/presentation/components/Pager.kt
+++ b/app/src/main/java/eu/kanade/presentation/components/Pager.kt
@@ -1,5 +1,8 @@
 package eu.kanade.presentation.components
 
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
 import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -7,6 +10,7 @@ import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListLayoutInfo
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.runtime.Composable
@@ -14,6 +18,7 @@ import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.runtime.saveable.rememberSaveable
@@ -21,7 +26,10 @@ import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMaxBy
+import androidx.compose.ui.util.fastSumBy
 import kotlinx.coroutines.flow.distinctUntilChanged
 
 @Composable
@@ -79,7 +87,7 @@ private fun Pager(
             horizontalAlignment = horizontalAlignment,
             verticalArrangement = Arrangement.aligned(verticalAlignment),
             userScrollEnabled = userScrollEnabled,
-            flingBehavior = rememberSnapFlingBehavior(lazyListState = state.lazyListState),
+            flingBehavior = rememberLazyListSnapFlingBehavior(lazyListState = state.lazyListState),
         ) {
             items(
                 count = count,
@@ -102,7 +110,7 @@ private fun Pager(
             verticalAlignment = verticalAlignment,
             horizontalArrangement = Arrangement.aligned(horizontalAlignment),
             userScrollEnabled = userScrollEnabled,
-            flingBehavior = rememberSnapFlingBehavior(lazyListState = state.lazyListState),
+            flingBehavior = rememberLazyListSnapFlingBehavior(lazyListState = state.lazyListState),
         ) {
             items(
                 count = count,
@@ -180,3 +188,72 @@ class PagerState(
         )
     }
 }
+
+// https://android.googlesource.com/platform/frameworks/support/+/refs/changes/78/2160778/35/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
+private fun lazyListSnapLayoutInfoProvider(
+    lazyListState: LazyListState,
+    positionInLayout: (layoutSize: Float, itemSize: Float) -> Float = { layoutSize, itemSize ->
+        layoutSize / 2f - itemSize / 2f
+    },
+) = object : SnapLayoutInfoProvider {
+
+    private val layoutInfo: LazyListLayoutInfo
+        get() = lazyListState.layoutInfo
+
+    // Single page snapping is the default
+    override fun Density.calculateApproachOffset(initialVelocity: Float): Float = 0f
+
+    override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
+        var lowerBoundOffset = Float.NEGATIVE_INFINITY
+        var upperBoundOffset = Float.POSITIVE_INFINITY
+
+        layoutInfo.visibleItemsInfo.fastForEach { item ->
+            val offset =
+                calculateDistanceToDesiredSnapPosition(layoutInfo, item, positionInLayout)
+
+            // Find item that is closest to the center
+            if (offset <= 0 && offset > lowerBoundOffset) {
+                lowerBoundOffset = offset
+            }
+
+            // Find item that is closest to center, but after it
+            if (offset >= 0 && offset < upperBoundOffset) {
+                upperBoundOffset = offset
+            }
+        }
+
+        return lowerBoundOffset.rangeTo(upperBoundOffset)
+    }
+
+    override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) {
+        if (visibleItemsInfo.isNotEmpty()) {
+            visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat()
+        } else {
+            0f
+        }
+    }
+}
+
+@Composable
+private fun rememberLazyListSnapFlingBehavior(lazyListState: LazyListState): FlingBehavior {
+    val snappingLayout = remember(lazyListState) { lazyListSnapLayoutInfoProvider(lazyListState) }
+    return rememberSnapFlingBehavior(snappingLayout)
+}
+
+private fun calculateDistanceToDesiredSnapPosition(
+    layoutInfo: LazyListLayoutInfo,
+    item: LazyListItemInfo,
+    positionInLayout: (layoutSize: Float, itemSize: Float) -> Float,
+): Float {
+    val containerSize =
+        with(layoutInfo) { singleAxisViewportSize - beforeContentPadding - afterContentPadding }
+
+    val desiredDistance =
+        positionInLayout(containerSize.toFloat(), item.size.toFloat())
+
+    val itemCurrentPosition = item.offset
+    return itemCurrentPosition - desiredDistance
+}
+
+private val LazyListLayoutInfo.singleAxisViewportSize: Int
+    get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width