mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Migrate to M3 pull-to-refresh (#10164)
This commit is contained in:
		| @@ -73,7 +73,7 @@ fun ExtensionScreen( | ||||
|     PullRefresh( | ||||
|         refreshing = state.isRefreshing, | ||||
|         onRefresh = onRefresh, | ||||
|         enabled = !state.isLoading, | ||||
|         enabled = { !state.isLoading }, | ||||
|     ) { | ||||
|         when { | ||||
|             state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|   | ||||
| @@ -19,6 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| import androidx.compose.ui.zIndex | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import kotlinx.collections.immutable.ImmutableList | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| @@ -70,6 +71,7 @@ fun TabbedScreen( | ||||
|         ) { | ||||
|             PrimaryTabRow( | ||||
|                 selectedTabIndex = state.currentPage, | ||||
|                 modifier = Modifier.zIndex(1f), | ||||
|             ) { | ||||
|                 tabs.forEachIndexed { index, tab -> | ||||
|                     Tab( | ||||
|   | ||||
| @@ -93,7 +93,7 @@ fun LibraryContent( | ||||
|                     isRefreshing = false | ||||
|                 } | ||||
|             }, | ||||
|             enabled = notSelectionMode, | ||||
|             enabled = { notSelectionMode }, | ||||
|         ) { | ||||
|             LibraryPager( | ||||
|                 state = pagerState, | ||||
|   | ||||
| @@ -7,7 +7,9 @@ import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.PrimaryScrollableTabRow | ||||
| import androidx.compose.material3.Tab | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.zIndex | ||||
| import eu.kanade.presentation.category.visualName | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.presentation.core.components.material.TabText | ||||
| @@ -19,7 +21,9 @@ internal fun LibraryTabs( | ||||
|     getNumberOfMangaForCategory: (Category) -> Int?, | ||||
|     onTabItemClick: (Int) -> Unit, | ||||
| ) { | ||||
|     Column { | ||||
|     Column( | ||||
|         modifier = Modifier.zIndex(1f), | ||||
|     ) { | ||||
|         PrimaryScrollableTabRow( | ||||
|             selectedTabIndex = pagerState.currentPage, | ||||
|             edgePadding = 0.dp, | ||||
|   | ||||
| @@ -364,8 +364,8 @@ private fun MangaScreenSmallImpl( | ||||
|         PullRefresh( | ||||
|             refreshing = state.isRefreshingData, | ||||
|             onRefresh = onRefresh, | ||||
|             enabled = !isAnySelected, | ||||
|             indicatorPadding = WindowInsets.systemBars.only(WindowInsetsSides.Top).asPaddingValues(), | ||||
|             enabled = { !isAnySelected }, | ||||
|             indicatorPadding = PaddingValues(top = topPadding), | ||||
|         ) { | ||||
|             val layoutDirection = LocalLayoutDirection.current | ||||
|             VerticalFastScroller( | ||||
| @@ -529,97 +529,98 @@ fun MangaScreenLargeImpl( | ||||
|  | ||||
|     val insetPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues() | ||||
|     var topBarHeight by remember { mutableIntStateOf(0) } | ||||
|     PullRefresh( | ||||
|         refreshing = state.isRefreshingData, | ||||
|         onRefresh = onRefresh, | ||||
|         enabled = !isAnySelected, | ||||
|         indicatorPadding = PaddingValues( | ||||
|             start = insetPadding.calculateStartPadding(layoutDirection), | ||||
|             top = with(density) { topBarHeight.toDp() }, | ||||
|             end = insetPadding.calculateEndPadding(layoutDirection), | ||||
|         ), | ||||
|     ) { | ||||
|         val chapterListState = rememberLazyListState() | ||||
|  | ||||
|         val internalOnBackPressed = { | ||||
|             if (isAnySelected) { | ||||
|                 onAllChapterSelected(false) | ||||
|             } else { | ||||
|                 onBackClicked() | ||||
|             } | ||||
|     val chapterListState = rememberLazyListState() | ||||
|  | ||||
|     val internalOnBackPressed = { | ||||
|         if (isAnySelected) { | ||||
|             onAllChapterSelected(false) | ||||
|         } else { | ||||
|             onBackClicked() | ||||
|         } | ||||
|         BackHandler(onBack = internalOnBackPressed) | ||||
|     } | ||||
|     BackHandler(onBack = internalOnBackPressed) | ||||
|  | ||||
|         Scaffold( | ||||
|             topBar = { | ||||
|                 val selectedChapterCount = remember(chapters) { | ||||
|                     chapters.count { it.selected } | ||||
|     Scaffold( | ||||
|         topBar = { | ||||
|             val selectedChapterCount = remember(chapters) { | ||||
|                 chapters.count { it.selected } | ||||
|             } | ||||
|             MangaToolbar( | ||||
|                 modifier = Modifier.onSizeChanged { topBarHeight = it.height }, | ||||
|                 title = state.manga.title, | ||||
|                 titleAlphaProvider = { if (isAnySelected) 1f else 0f }, | ||||
|                 backgroundAlphaProvider = { 1f }, | ||||
|                 hasFilters = state.filterActive, | ||||
|                 onBackClicked = internalOnBackPressed, | ||||
|                 onClickFilter = onFilterButtonClicked, | ||||
|                 onClickShare = onShareClicked, | ||||
|                 onClickDownload = onDownloadActionClicked, | ||||
|                 onClickEditCategory = onEditCategoryClicked, | ||||
|                 onClickRefresh = onRefresh, | ||||
|                 onClickMigrate = onMigrateClicked, | ||||
|                 actionModeCounter = selectedChapterCount, | ||||
|                 onSelectAll = { onAllChapterSelected(true) }, | ||||
|                 onInvertSelection = { onInvertSelection() }, | ||||
|             ) | ||||
|         }, | ||||
|         bottomBar = { | ||||
|             Box( | ||||
|                 modifier = Modifier.fillMaxWidth(), | ||||
|                 contentAlignment = Alignment.BottomEnd, | ||||
|             ) { | ||||
|                 val selectedChapters = remember(chapters) { | ||||
|                     chapters.filter { it.selected } | ||||
|                 } | ||||
|                 MangaToolbar( | ||||
|                     modifier = Modifier.onSizeChanged { topBarHeight = it.height }, | ||||
|                     title = state.manga.title, | ||||
|                     titleAlphaProvider = { if (isAnySelected) 1f else 0f }, | ||||
|                     backgroundAlphaProvider = { 1f }, | ||||
|                     hasFilters = state.filterActive, | ||||
|                     onBackClicked = internalOnBackPressed, | ||||
|                     onClickFilter = onFilterButtonClicked, | ||||
|                     onClickShare = onShareClicked, | ||||
|                     onClickDownload = onDownloadActionClicked, | ||||
|                     onClickEditCategory = onEditCategoryClicked, | ||||
|                     onClickRefresh = onRefresh, | ||||
|                     onClickMigrate = onMigrateClicked, | ||||
|                     actionModeCounter = selectedChapterCount, | ||||
|                     onSelectAll = { onAllChapterSelected(true) }, | ||||
|                     onInvertSelection = { onInvertSelection() }, | ||||
|                 SharedMangaBottomActionMenu( | ||||
|                     selected = selectedChapters, | ||||
|                     onMultiBookmarkClicked = onMultiBookmarkClicked, | ||||
|                     onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, | ||||
|                     onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, | ||||
|                     onDownloadChapter = onDownloadChapter, | ||||
|                     onMultiDeleteClicked = onMultiDeleteClicked, | ||||
|                     fillFraction = 0.5f, | ||||
|                 ) | ||||
|             }, | ||||
|             bottomBar = { | ||||
|                 Box( | ||||
|                     modifier = Modifier.fillMaxWidth(), | ||||
|                     contentAlignment = Alignment.BottomEnd, | ||||
|                 ) { | ||||
|                     val selectedChapters = remember(chapters) { | ||||
|                         chapters.filter { it.selected } | ||||
|                     } | ||||
|                     SharedMangaBottomActionMenu( | ||||
|                         selected = selectedChapters, | ||||
|                         onMultiBookmarkClicked = onMultiBookmarkClicked, | ||||
|                         onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, | ||||
|                         onMarkPreviousAsReadClicked = onMarkPreviousAsReadClicked, | ||||
|                         onDownloadChapter = onDownloadChapter, | ||||
|                         onMultiDeleteClicked = onMultiDeleteClicked, | ||||
|                         fillFraction = 0.5f, | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|             snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, | ||||
|             floatingActionButton = { | ||||
|                 val isFABVisible = remember(chapters) { | ||||
|                     chapters.fastAny { !it.chapter.read } && !isAnySelected | ||||
|                 } | ||||
|                 AnimatedVisibility( | ||||
|                     visible = isFABVisible, | ||||
|                     enter = fadeIn(), | ||||
|                     exit = fadeOut(), | ||||
|                 ) { | ||||
|                     ExtendedFloatingActionButton( | ||||
|                         text = { | ||||
|                             val isReading = remember(state.chapters) { | ||||
|                                 state.chapters.fastAny { it.chapter.read } | ||||
|                             } | ||||
|                             Text( | ||||
|                                 text = stringResource( | ||||
|                                     if (isReading) MR.strings.action_resume else MR.strings.action_start, | ||||
|                                 ), | ||||
|                             ) | ||||
|                         }, | ||||
|                         icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, | ||||
|                         onClick = onContinueReading, | ||||
|                         expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|         ) { contentPadding -> | ||||
|             } | ||||
|         }, | ||||
|         snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, | ||||
|         floatingActionButton = { | ||||
|             val isFABVisible = remember(chapters) { | ||||
|                 chapters.fastAny { !it.chapter.read } && !isAnySelected | ||||
|             } | ||||
|             AnimatedVisibility( | ||||
|                 visible = isFABVisible, | ||||
|                 enter = fadeIn(), | ||||
|                 exit = fadeOut(), | ||||
|             ) { | ||||
|                 ExtendedFloatingActionButton( | ||||
|                     text = { | ||||
|                         val isReading = remember(state.chapters) { | ||||
|                             state.chapters.fastAny { it.chapter.read } | ||||
|                         } | ||||
|                         Text( | ||||
|                             text = stringResource( | ||||
|                                 if (isReading) MR.strings.action_resume else MR.strings.action_start, | ||||
|                             ), | ||||
|                         ) | ||||
|                     }, | ||||
|                     icon = { Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null) }, | ||||
|                     onClick = onContinueReading, | ||||
|                     expanded = chapterListState.isScrollingUp() || chapterListState.isScrolledToEnd(), | ||||
|                 ) | ||||
|             } | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         PullRefresh( | ||||
|             refreshing = state.isRefreshingData, | ||||
|             onRefresh = onRefresh, | ||||
|             enabled = { !isAnySelected }, | ||||
|             indicatorPadding = PaddingValues( | ||||
|                 start = insetPadding.calculateStartPadding(layoutDirection), | ||||
|                 top = with(density) { topBarHeight.toDp() }, | ||||
|                 end = insetPadding.calculateEndPadding(layoutDirection), | ||||
|             ), | ||||
|         ) { | ||||
|             TwoPanelBox( | ||||
|                 modifier = Modifier.padding( | ||||
|                     start = contentPadding.calculateStartPadding(layoutDirection), | ||||
|   | ||||
| @@ -104,7 +104,7 @@ fun UpdateScreen( | ||||
|                             isRefreshing = false | ||||
|                         } | ||||
|                     }, | ||||
|                     enabled = !state.selectionMode, | ||||
|                     enabled = { !state.selectionMode }, | ||||
|                     indicatorPadding = contentPadding, | ||||
|                 ) { | ||||
|                     FastScrollLazyColumn( | ||||
|   | ||||
| @@ -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, | ||||
|     ) | ||||
|  | ||||
|     Box(Modifier.pullRefresh(state, enabled)) { | ||||
|         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, | ||||
|             ) | ||||
|     if (state.isRefreshing) { | ||||
|         LaunchedEffect(true) { | ||||
|             onRefresh() | ||||
|         } | ||||
|     } | ||||
|     LaunchedEffect(refreshing) { | ||||
|         if (refreshing && !state.isRefreshing) { | ||||
|             state.startRefreshAnimated() | ||||
|         } else if (!refreshing && state.isRefreshing) { | ||||
|             state.endRefreshAnimated() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Box(modifier.nestedScroll(state.nestedScrollConnection)) { | ||||
|         content() | ||||
|  | ||||
|         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 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user