| 
					
				 | 
			
			
				@@ -1,17 +1,33 @@ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 package tachiyomi.presentation.core.components.material 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.animation.core.animate 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import androidx.compose.foundation.layout.Box 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import androidx.compose.foundation.layout.PaddingValues 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import androidx.compose.foundation.layout.padding 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-import androidx.compose.material.pullrefresh.PullRefreshIndicator 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-import androidx.compose.material.pullrefresh.pullRefresh 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-import androidx.compose.material.pullrefresh.rememberPullRefreshState 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-import androidx.compose.material3.MaterialTheme 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.material3.pulltorefresh.PullToRefreshContainer 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.material3.pulltorefresh.PullToRefreshState 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import androidx.compose.runtime.Composable 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.runtime.LaunchedEffect 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.runtime.getValue 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.runtime.mutableFloatStateOf 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.runtime.mutableStateOf 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.runtime.remember 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.runtime.saveable.Saver 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.runtime.saveable.rememberSaveable 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.runtime.setValue 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import androidx.compose.ui.Alignment 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import androidx.compose.ui.Modifier 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-import androidx.compose.ui.draw.clipToBounds 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.ui.geometry.Offset 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.ui.input.nestedscroll.NestedScrollSource 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.ui.input.nestedscroll.nestedScroll 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.ui.platform.LocalDensity 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.ui.unit.Dp 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.ui.unit.LayoutDirection 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import androidx.compose.ui.unit.Velocity 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 import androidx.compose.ui.unit.dp 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import kotlin.math.abs 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import kotlin.math.pow 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 /** 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  * @param refreshing Whether the layout is currently refreshing 
			 | 
		
	
	
		
			
				| 
					
				 | 
			
			
				@@ -19,38 +35,239 @@ import androidx.compose.ui.unit.dp 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  * @param enabled Whether the the layout should react to swipe gestures or not. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  * @param indicatorPadding Content padding for the indicator, to inset the indicator in if required. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  * @param content The content containing a vertically scrollable composable. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- * 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				- * Code reference: [Accompanist SwipeRefresh](https://github.com/google/accompanist/blob/677bc4ca0ee74677a8ba73793d04d85fe4ab55fb/swiperefresh/src/main/java/com/google/accompanist/swiperefresh/SwipeRefresh.kt#L265-L283) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  */ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 @Composable 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 fun PullRefresh( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     refreshing: Boolean, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    enabled: () -> Boolean, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     onRefresh: () -> Unit, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    enabled: Boolean, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    modifier: Modifier = Modifier, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     indicatorPadding: PaddingValues = PaddingValues(0.dp), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     content: @Composable () -> Unit, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 ) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    val state = rememberPullRefreshState( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        refreshing = refreshing, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        onRefresh = onRefresh, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    val state = rememberPullToRefreshState( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        extraVerticalOffset = indicatorPadding.calculateTopPadding(), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        enabled = enabled, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    if (state.isRefreshing) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        LaunchedEffect(true) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            onRefresh() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    LaunchedEffect(refreshing) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if (refreshing && !state.isRefreshing) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            state.startRefreshAnimated() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } else if (!refreshing && state.isRefreshing) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            state.endRefreshAnimated() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-    Box(Modifier.pullRefresh(state, enabled)) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    Box(modifier.nestedScroll(state.nestedScrollConnection)) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         content() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				  
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        Box( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            Modifier 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                .padding(indicatorPadding) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                .matchParentSize() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                .clipToBounds(), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-        ) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            PullRefreshIndicator( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                refreshing = refreshing, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                state = state, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                modifier = Modifier.align(Alignment.TopCenter), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                backgroundColor = MaterialTheme.colorScheme.primary, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-                contentColor = MaterialTheme.colorScheme.onPrimary, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				-            ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        val contentPadding = remember(indicatorPadding) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            object : PaddingValues { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    indicatorPadding.calculateLeftPadding(layoutDirection) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                override fun calculateTopPadding(): Dp = 0.dp 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    indicatorPadding.calculateRightPadding(layoutDirection) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                override fun calculateBottomPadding(): Dp = 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    indicatorPadding.calculateBottomPadding() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				         } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        PullToRefreshContainer( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            state = state, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            modifier = Modifier 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                .align(Alignment.TopCenter) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                .padding(contentPadding), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+@Composable 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+private fun rememberPullToRefreshState( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    extraVerticalOffset: Dp, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    positionalThreshold: Dp = 64.dp, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    enabled: () -> Boolean = { true }, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+): PullToRefreshStateImpl { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    val density = LocalDensity.current 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    val extraVerticalOffsetPx = with(density) { extraVerticalOffset.toPx() } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    val positionalThresholdPx = with(density) { positionalThreshold.toPx() } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return rememberSaveable( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        extraVerticalOffset, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        positionalThresholdPx, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        enabled, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        saver = PullToRefreshStateImpl.Saver( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            extraVerticalOffset = extraVerticalOffsetPx, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            positionalThreshold = positionalThresholdPx, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            enabled = enabled, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    ) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        PullToRefreshStateImpl( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            initialRefreshing = false, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            extraVerticalOffset = extraVerticalOffsetPx, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            positionalThreshold = positionalThresholdPx, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            enabled = enabled, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+/** 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ * Creates a [PullToRefreshState]. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ * 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ * @param positionalThreshold The positional threshold, in pixels, in which a refresh is triggered 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ * @param extraVerticalOffset Extra vertical offset, in pixels, for the "refreshing" state 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ * @param initialRefreshing The initial refreshing value of [PullToRefreshState] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ * @param enabled a callback used to determine whether scroll events are to be handled by this 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ * [PullToRefreshState] 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ */ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+private class PullToRefreshStateImpl( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    initialRefreshing: Boolean, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    private val extraVerticalOffset: Float, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    override val positionalThreshold: Float, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    enabled: () -> Boolean, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+) : PullToRefreshState { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    override val progress get() = adjustedDistancePulled / positionalThreshold 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    override var verticalOffset by mutableFloatStateOf(0f) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    override var isRefreshing by mutableStateOf(initialRefreshing) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    override fun startRefresh() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        isRefreshing = true 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        verticalOffset = positionalThreshold + extraVerticalOffset 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    suspend fun startRefreshAnimated() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        isRefreshing = true 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        animateTo(positionalThreshold + extraVerticalOffset) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				     } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    override fun endRefresh() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        verticalOffset = 0f 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        isRefreshing = false 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    suspend fun endRefreshAnimated() { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        animateTo(0f) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        isRefreshing = false 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    override var nestedScrollConnection = object : NestedScrollConnection { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        override fun onPreScroll( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            available: Offset, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            source: NestedScrollSource, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ): Offset = when { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            !enabled() -> Offset.Zero 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // Swiping up 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            source == NestedScrollSource.Drag && available.y < 0 -> { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                consumeAvailableOffset(available) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            else -> Offset.Zero 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        override fun onPostScroll( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            consumed: Offset, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            available: Offset, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            source: NestedScrollSource, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ): Offset = when { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            !enabled() -> Offset.Zero 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // Swiping down 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            source == NestedScrollSource.Drag && available.y > 0 -> { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                consumeAvailableOffset(available) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            else -> Offset.Zero 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        override suspend fun onPreFling(available: Velocity): Velocity { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            return Velocity(0f, onRelease(available.y)) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /** Helper method for nested scroll connection */ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    fun consumeAvailableOffset(available: Offset): Offset { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        val y = if (isRefreshing) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            0f 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            val newOffset = (distancePulled + available.y).coerceAtLeast(0f) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            val dragConsumed = newOffset - distancePulled 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            distancePulled = newOffset 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            dragConsumed 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return Offset(0f, y) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /** Helper method for nested scroll connection. Calls onRefresh callback when triggered */ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    suspend fun onRelease(velocity: Float): Float { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if (isRefreshing) return 0f // Already refreshing, do nothing 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // Trigger refresh 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if (adjustedDistancePulled > positionalThreshold) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            startRefreshAnimated() 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            animateTo(0f) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        val consumed = when { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // We are flinging without having dragged the pull refresh (for example a fling inside 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // a list) - don't consume 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            distancePulled == 0f -> 0f 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // If the velocity is negative, the fling is upwards, and we don't want to prevent the 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // the list from scrolling 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            velocity < 0f -> 0f 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // We are showing the indicator, and the fling is downwards - consume everything 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            else -> velocity 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        distancePulled = 0f 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        return consumed 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    suspend fun animateTo(offset: Float) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        animate(initialValue = verticalOffset, targetValue = offset) { value, _ -> 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            verticalOffset = value 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    /** Provides custom vertical offset behavior for [PullToRefreshContainer] */ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    fun calculateVerticalOffset(): Float = when { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        // If drag hasn't gone past the threshold, the position is the adjustedDistancePulled. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        adjustedDistancePulled <= positionalThreshold -> adjustedDistancePulled 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        else -> { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // How far beyond the threshold pull has gone, as a percentage of the threshold. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            val overshootPercent = abs(progress) - 1.0f 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // Limit the overshoot to 200%. Linear between 0 and 200. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            val linearTension = overshootPercent.coerceIn(0f, 2f) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // Non-linear tension. Increases with linearTension, but at a decreasing rate. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            val tensionPercent = linearTension - linearTension.pow(2) / 4 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            // The additional offset beyond the threshold. 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            val extraOffset = positionalThreshold * tensionPercent 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            positionalThreshold + extraOffset 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    companion object { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        /** The default [Saver] for [PullToRefreshStateImpl]. */ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        fun Saver( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            extraVerticalOffset: Float, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            positionalThreshold: Float, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            enabled: () -> Boolean, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ) = Saver<PullToRefreshStateImpl, Boolean>( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            save = { it.isRefreshing }, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            restore = { isRefreshing -> 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                PullToRefreshStateImpl( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    initialRefreshing = isRefreshing, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    extraVerticalOffset = extraVerticalOffset, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    positionalThreshold = positionalThreshold, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                    enabled = enabled, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+                ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            }, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    private var distancePulled by mutableFloatStateOf(0f) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    private val adjustedDistancePulled: Float get() = distancePulled * 0.5f 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				 } 
			 |