@@ -4,6 +4,9 @@ import android.os.Bundle
44import androidx.activity.compose.BackHandler
55import androidx.activity.compose.setContent
66import androidx.activity.enableEdgeToEdge
7+ import androidx.compose.animation.core.FastOutSlowInEasing
8+ import androidx.compose.animation.core.animate
9+ import androidx.compose.animation.core.tween
710import androidx.compose.foundation.layout.Arrangement
811import androidx.compose.foundation.layout.Column
912import androidx.compose.foundation.layout.ColumnScope
@@ -17,10 +20,20 @@ import androidx.compose.runtime.LaunchedEffect
1720import androidx.compose.runtime.collectAsState
1821import androidx.compose.runtime.getValue
1922import androidx.compose.runtime.key
23+ import androidx.compose.runtime.mutableFloatStateOf
24+ import androidx.compose.runtime.mutableStateOf
25+ import androidx.compose.runtime.remember
2026import androidx.compose.runtime.rememberCoroutineScope
27+ import androidx.compose.runtime.setValue
2128import androidx.compose.runtime.snapshotFlow
2229import androidx.compose.ui.Alignment
2330import androidx.compose.ui.Modifier
31+ import androidx.compose.ui.geometry.Offset
32+ import androidx.compose.ui.graphics.graphicsLayer
33+ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
34+ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
35+ import androidx.compose.ui.input.nestedscroll.nestedScroll
36+ import androidx.compose.ui.unit.Velocity
2437import androidx.lifecycle.Lifecycle
2538import androidx.lifecycle.compose.LifecycleEventEffect
2639import com.raival.compose.file.explorer.App.Companion.globalClass
@@ -44,6 +57,8 @@ import com.raival.compose.file.explorer.screen.main.ui.Toolbar
4457import com.raival.compose.file.explorer.theme.FileExplorerTheme
4558import kotlinx.coroutines.launch
4659import java.io.File
60+ import kotlin.math.abs
61+ import kotlin.math.exp
4762
4863class MainActivity : BaseActivity () {
4964 private val HOME_SCREEN_SHORTCUT_EXTRA_KEY = " filePath"
@@ -167,13 +182,128 @@ class MainActivity : BaseActivity() {
167182 }
168183 }
169184
185+ var overscrollAmount by remember { mutableFloatStateOf(0f ) }
186+ val threshold = 100f
187+ val animationScope = rememberCoroutineScope()
188+ var isAnimatingBack by remember { mutableStateOf(false ) }
189+
190+ fun applyExponentialTension (current : Float , addition : Float , threshold : Float ): Float {
191+ return if (current < threshold) {
192+ current + addition
193+ } else {
194+ val excess = current - threshold
195+ val decayFactor =
196+ exp(- excess / threshold * 2f ) // Adjust multiplier for steepness
197+ current + (addition * decayFactor)
198+ }
199+ }
200+
201+ val nestedScrollConnection = remember {
202+ object : NestedScrollConnection {
203+ override fun onPreScroll (
204+ available : Offset ,
205+ source : NestedScrollSource
206+ ): Offset {
207+ // Smoothly animate overscroll back to zero when scrolling in opposite direction
208+ if (available.x > 0 && overscrollAmount > 0 && ! isAnimatingBack) {
209+ isAnimatingBack = true
210+ animationScope.launch {
211+ animate(
212+ initialValue = overscrollAmount,
213+ targetValue = 0f ,
214+ animationSpec = tween(
215+ durationMillis = 150 ,
216+ easing = FastOutSlowInEasing
217+ )
218+ ) { value, _ ->
219+ overscrollAmount = value
220+ }
221+ isAnimatingBack = false
222+ }
223+ }
224+ return Offset .Zero
225+ }
226+
227+ override fun onPostScroll (
228+ consumed : Offset ,
229+ available : Offset ,
230+ source : NestedScrollSource
231+ ): Offset {
232+ // Check if we're on the last page and there's leftover scroll
233+ val isLastPage = pagerState.currentPage == pagerState.pageCount - 1
234+ if (isLastPage && available.x < 0 && source == NestedScrollSource .UserInput ) {
235+ val availableAmount = abs(available.x)
236+
237+ overscrollAmount = applyExponentialTension(
238+ overscrollAmount,
239+ availableAmount,
240+ threshold
241+ )
242+ return Offset (available.x, 0f ) // Consume the scroll
243+ }
244+ return Offset .Zero
245+ }
246+
247+ override suspend fun onPreFling (available : Velocity ): Velocity {
248+ val isLastPage = pagerState.currentPage == pagerState.pageCount - 1
249+
250+ if (isLastPage && overscrollAmount > 0 && ! isAnimatingBack) {
251+ // Trigger action when releasing overscroll
252+ if (overscrollAmount > threshold) {
253+ manager.addTabAndSelect(HomeTab ())
254+ }
255+
256+ // Animate overscroll back to 0 to prevent page jumping
257+ // This helps avoid the pager shooting to previous page
258+ // Animate overscroll back to 0 to prevent page jumping
259+ isAnimatingBack = true
260+ animationScope.launch {
261+ animate(
262+ initialValue = overscrollAmount,
263+ targetValue = 0f ,
264+ animationSpec = tween(
265+ durationMillis = 200 ,
266+ easing = FastOutSlowInEasing
267+ )
268+ ) { value, _ ->
269+ overscrollAmount = value
270+ }
271+ isAnimatingBack = false
272+ }
273+
274+ // Don't consume velocity if we're not overscrolling significantly
275+ // This prevents interfering with normal pager fling behavior
276+ return if (overscrollAmount > 10f ) {
277+ Velocity (
278+ available.x * 0.3f ,
279+ available.y
280+ ) // Reduce horizontal velocity
281+ } else {
282+ Velocity .Zero
283+ }
284+ }
285+
286+ overscrollAmount = 0f
287+ return Velocity .Zero
288+ }
289+ }
290+ }
291+
170292 HorizontalPager (
171293 state = pagerState,
172- modifier = Modifier .weight(1f ),
294+ modifier = Modifier
295+ .weight(1f )
296+ .nestedScroll(nestedScrollConnection),
173297 key = { state.tabs[it].id }
174298 ) { index ->
175299 key(index) {
176- Column (modifier = Modifier .weight(1f )) {
300+ Column (
301+ modifier = Modifier
302+ .weight(1f )
303+ .graphicsLayer {
304+ translationX = - overscrollAmount
305+ }
306+ ) {
177307 if (state.tabs.isNotEmpty()) {
178308 val currentTab = state.tabs[index]
179309 when (currentTab) {
0 commit comments