Merge "Remove Pager from PlatformComposeCore (1/2)" into udc-qpr-dev am: ef900a3efa
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/24146484 Change-Id: Id52c5d9a27f071d90c5e3685f77672f4e04ec5ad Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -1,357 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.android.compose.pager
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.AnimationSpec
|
|
||||||
import androidx.compose.animation.core.DecayAnimationSpec
|
|
||||||
import androidx.compose.animation.rememberSplineBasedDecay
|
|
||||||
import androidx.compose.foundation.gestures.FlingBehavior
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
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.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
|
|
||||||
/** Library-wide switch to turn on debug logging. */
|
|
||||||
internal const val DebugLog = false
|
|
||||||
|
|
||||||
@RequiresOptIn(message = "Accompanist Pager is experimental. The API may be changed in the future.")
|
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class ExperimentalPagerApi
|
|
||||||
|
|
||||||
/** Contains the default values used by [HorizontalPager] and [VerticalPager]. */
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
object PagerDefaults {
|
|
||||||
/**
|
|
||||||
* Remember the default [FlingBehavior] that represents the scroll curve.
|
|
||||||
*
|
|
||||||
* @param state The [PagerState] to update.
|
|
||||||
* @param decayAnimationSpec The decay animation spec to use for decayed flings.
|
|
||||||
* @param snapAnimationSpec The animation spec to use when snapping.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun flingBehavior(
|
|
||||||
state: PagerState,
|
|
||||||
decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
|
|
||||||
snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
|
|
||||||
): FlingBehavior =
|
|
||||||
rememberSnappingFlingBehavior(
|
|
||||||
lazyListState = state.lazyListState,
|
|
||||||
decayAnimationSpec = decayAnimationSpec,
|
|
||||||
snapAnimationSpec = snapAnimationSpec,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Deprecated(
|
|
||||||
"Replaced with PagerDefaults.flingBehavior()",
|
|
||||||
ReplaceWith("PagerDefaults.flingBehavior(state, decayAnimationSpec, snapAnimationSpec)")
|
|
||||||
)
|
|
||||||
@Composable
|
|
||||||
fun rememberPagerFlingConfig(
|
|
||||||
state: PagerState,
|
|
||||||
decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
|
|
||||||
snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
|
|
||||||
): FlingBehavior = flingBehavior(state, decayAnimationSpec, snapAnimationSpec)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A horizontally scrolling layout that allows users to flip between items to the left and right.
|
|
||||||
*
|
|
||||||
* @param count the number of pages.
|
|
||||||
* @param modifier the modifier to apply to this layout.
|
|
||||||
* @param state the state object to be used to control or observe the pager's state.
|
|
||||||
* @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
|
|
||||||
* composed from the end to the start and [PagerState.currentPage] == 0 will mean the first item
|
|
||||||
* is located at the end.
|
|
||||||
* @param itemSpacing horizontal spacing to add between items.
|
|
||||||
* @param flingBehavior logic describing fling behavior.
|
|
||||||
* @param key the scroll position will be maintained based on the key, which means if you add/remove
|
|
||||||
* items before the current visible item the item with the given key will be kept as the first
|
|
||||||
* visible one.
|
|
||||||
* @param content a block which describes the content. Inside this block you can reference
|
|
||||||
* [PagerScope.currentPage] and other properties in [PagerScope].
|
|
||||||
* @sample com.google.accompanist.sample.pager.HorizontalPagerSample
|
|
||||||
*/
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
@Composable
|
|
||||||
fun HorizontalPager(
|
|
||||||
count: Int,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
state: PagerState = rememberPagerState(),
|
|
||||||
reverseLayout: Boolean = false,
|
|
||||||
itemSpacing: Dp = 0.dp,
|
|
||||||
flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
|
|
||||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
|
||||||
key: ((page: Int) -> Any)? = null,
|
|
||||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
|
||||||
content: @Composable PagerScope.(page: Int) -> Unit,
|
|
||||||
) {
|
|
||||||
Pager(
|
|
||||||
count = count,
|
|
||||||
state = state,
|
|
||||||
modifier = modifier,
|
|
||||||
isVertical = false,
|
|
||||||
reverseLayout = reverseLayout,
|
|
||||||
itemSpacing = itemSpacing,
|
|
||||||
verticalAlignment = verticalAlignment,
|
|
||||||
flingBehavior = flingBehavior,
|
|
||||||
key = key,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A vertically scrolling layout that allows users to flip between items to the top and bottom.
|
|
||||||
*
|
|
||||||
* @param count the number of pages.
|
|
||||||
* @param modifier the modifier to apply to this layout.
|
|
||||||
* @param state the state object to be used to control or observe the pager's state.
|
|
||||||
* @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
|
|
||||||
* composed from the bottom to the top and [PagerState.currentPage] == 0 will mean the first item
|
|
||||||
* is located at the bottom.
|
|
||||||
* @param itemSpacing vertical spacing to add between items.
|
|
||||||
* @param flingBehavior logic describing fling behavior.
|
|
||||||
* @param key the scroll position will be maintained based on the key, which means if you add/remove
|
|
||||||
* items before the current visible item the item with the given key will be kept as the first
|
|
||||||
* visible one.
|
|
||||||
* @param content a block which describes the content. Inside this block you can reference
|
|
||||||
* [PagerScope.currentPage] and other properties in [PagerScope].
|
|
||||||
* @sample com.google.accompanist.sample.pager.VerticalPagerSample
|
|
||||||
*/
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
@Composable
|
|
||||||
fun VerticalPager(
|
|
||||||
count: Int,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
state: PagerState = rememberPagerState(),
|
|
||||||
reverseLayout: Boolean = false,
|
|
||||||
itemSpacing: Dp = 0.dp,
|
|
||||||
flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
|
|
||||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
|
||||||
key: ((page: Int) -> Any)? = null,
|
|
||||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
|
||||||
content: @Composable PagerScope.(page: Int) -> Unit,
|
|
||||||
) {
|
|
||||||
Pager(
|
|
||||||
count = count,
|
|
||||||
state = state,
|
|
||||||
modifier = modifier,
|
|
||||||
isVertical = true,
|
|
||||||
reverseLayout = reverseLayout,
|
|
||||||
itemSpacing = itemSpacing,
|
|
||||||
horizontalAlignment = horizontalAlignment,
|
|
||||||
flingBehavior = flingBehavior,
|
|
||||||
key = key,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
@Composable
|
|
||||||
internal fun Pager(
|
|
||||||
count: Int,
|
|
||||||
modifier: Modifier,
|
|
||||||
state: PagerState,
|
|
||||||
reverseLayout: Boolean,
|
|
||||||
itemSpacing: Dp,
|
|
||||||
isVertical: Boolean,
|
|
||||||
flingBehavior: FlingBehavior,
|
|
||||||
key: ((page: Int) -> Any)?,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
|
||||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
|
||||||
content: @Composable PagerScope.(page: Int) -> Unit,
|
|
||||||
) {
|
|
||||||
require(count >= 0) { "pageCount must be >= 0" }
|
|
||||||
|
|
||||||
// Provide our PagerState with access to the SnappingFlingBehavior animation target
|
|
||||||
// TODO: can this be done in a better way?
|
|
||||||
state.flingAnimationTarget = { (flingBehavior as? SnappingFlingBehavior)?.animationTarget }
|
|
||||||
|
|
||||||
LaunchedEffect(count) {
|
|
||||||
state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once a fling (scroll) has finished, notify the state
|
|
||||||
LaunchedEffect(state) {
|
|
||||||
// When a 'scroll' has finished, notify the state
|
|
||||||
snapshotFlow { state.isScrollInProgress }
|
|
||||||
.filter { !it }
|
|
||||||
.collect { state.onScrollFinished() }
|
|
||||||
}
|
|
||||||
|
|
||||||
val pagerScope = remember(state) { PagerScopeImpl(state) }
|
|
||||||
|
|
||||||
// We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
|
|
||||||
// as normal
|
|
||||||
val consumeFlingNestedScrollConnection =
|
|
||||||
ConsumeFlingNestedScrollConnection(
|
|
||||||
consumeHorizontal = !isVertical,
|
|
||||||
consumeVertical = isVertical,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isVertical) {
|
|
||||||
LazyColumn(
|
|
||||||
state = state.lazyListState,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(itemSpacing, verticalAlignment),
|
|
||||||
horizontalAlignment = horizontalAlignment,
|
|
||||||
flingBehavior = flingBehavior,
|
|
||||||
reverseLayout = reverseLayout,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
count = count,
|
|
||||||
key = key,
|
|
||||||
) { page ->
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
// We don't any nested flings to continue in the pager, so we add a
|
|
||||||
// connection which consumes them.
|
|
||||||
// See: https://github.com/google/accompanist/issues/347
|
|
||||||
.nestedScroll(connection = consumeFlingNestedScrollConnection)
|
|
||||||
// Constraint the content to be <= than the size of the pager.
|
|
||||||
.fillParentMaxHeight()
|
|
||||||
.wrapContentSize()
|
|
||||||
) {
|
|
||||||
pagerScope.content(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LazyRow(
|
|
||||||
state = state.lazyListState,
|
|
||||||
verticalAlignment = verticalAlignment,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
|
|
||||||
flingBehavior = flingBehavior,
|
|
||||||
reverseLayout = reverseLayout,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
count = count,
|
|
||||||
key = key,
|
|
||||||
) { page ->
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
// We don't any nested flings to continue in the pager, so we add a
|
|
||||||
// connection which consumes them.
|
|
||||||
// See: https://github.com/google/accompanist/issues/347
|
|
||||||
.nestedScroll(connection = consumeFlingNestedScrollConnection)
|
|
||||||
// Constraint the content to be <= than the size of the pager.
|
|
||||||
.fillParentMaxWidth()
|
|
||||||
.wrapContentSize()
|
|
||||||
) {
|
|
||||||
pagerScope.content(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ConsumeFlingNestedScrollConnection(
|
|
||||||
private val consumeHorizontal: Boolean,
|
|
||||||
private val consumeVertical: Boolean,
|
|
||||||
) : NestedScrollConnection {
|
|
||||||
override fun onPostScroll(
|
|
||||||
consumed: Offset,
|
|
||||||
available: Offset,
|
|
||||||
source: NestedScrollSource
|
|
||||||
): Offset =
|
|
||||||
when (source) {
|
|
||||||
// We can consume all resting fling scrolls so that they don't propagate up to the
|
|
||||||
// Pager
|
|
||||||
NestedScrollSource.Fling -> available.consume(consumeHorizontal, consumeVertical)
|
|
||||||
else -> Offset.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
|
||||||
// We can consume all post fling velocity on the main-axis
|
|
||||||
// so that it doesn't propagate up to the Pager
|
|
||||||
return available.consume(consumeHorizontal, consumeVertical)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Offset.consume(
|
|
||||||
consumeHorizontal: Boolean,
|
|
||||||
consumeVertical: Boolean,
|
|
||||||
): Offset =
|
|
||||||
Offset(
|
|
||||||
x = if (consumeHorizontal) this.x else 0f,
|
|
||||||
y = if (consumeVertical) this.y else 0f,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun Velocity.consume(
|
|
||||||
consumeHorizontal: Boolean,
|
|
||||||
consumeVertical: Boolean,
|
|
||||||
): Velocity =
|
|
||||||
Velocity(
|
|
||||||
x = if (consumeHorizontal) this.x else 0f,
|
|
||||||
y = if (consumeVertical) this.y else 0f,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Scope for [HorizontalPager] content. */
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
@Stable
|
|
||||||
interface PagerScope {
|
|
||||||
/** Returns the current selected page */
|
|
||||||
val currentPage: Int
|
|
||||||
|
|
||||||
/** The current offset from the start of [currentPage], as a ratio of the page width. */
|
|
||||||
val currentPageOffset: Float
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
private class PagerScopeImpl(
|
|
||||||
private val state: PagerState,
|
|
||||||
) : PagerScope {
|
|
||||||
override val currentPage: Int
|
|
||||||
get() = state.currentPage
|
|
||||||
override val currentPageOffset: Float
|
|
||||||
get() = state.currentPageOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the offset for the given [page] from the current scroll position. This is useful when
|
|
||||||
* using the scroll position to apply effects or animations to items.
|
|
||||||
*
|
|
||||||
* The returned offset can positive or negative, depending on whether which direction the [page] is
|
|
||||||
* compared to the current scroll position.
|
|
||||||
*
|
|
||||||
* @sample com.google.accompanist.sample.pager.HorizontalPagerWithOffsetTransition
|
|
||||||
*/
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
fun PagerScope.calculateCurrentOffsetForPage(page: Int): Float {
|
|
||||||
return (currentPage + currentPageOffset) - page
|
|
||||||
}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.android.compose.pager
|
|
||||||
|
|
||||||
import androidx.annotation.FloatRange
|
|
||||||
import androidx.annotation.IntRange
|
|
||||||
import androidx.compose.animation.core.AnimationSpec
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.foundation.MutatePriority
|
|
||||||
import androidx.compose.foundation.gestures.ScrollScope
|
|
||||||
import androidx.compose.foundation.gestures.ScrollableState
|
|
||||||
import androidx.compose.foundation.interaction.InteractionSource
|
|
||||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.Saver
|
|
||||||
import androidx.compose.runtime.saveable.listSaver
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Deprecated(
|
|
||||||
"Replaced with rememberPagerState(initialPage) and count parameter on Pager composables",
|
|
||||||
ReplaceWith("rememberPagerState(initialPage)"),
|
|
||||||
level = DeprecationLevel.ERROR,
|
|
||||||
)
|
|
||||||
@Suppress("UNUSED_PARAMETER", "NOTHING_TO_INLINE")
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
@Composable
|
|
||||||
inline fun rememberPagerState(
|
|
||||||
@IntRange(from = 0) pageCount: Int,
|
|
||||||
@IntRange(from = 0) initialPage: Int = 0,
|
|
||||||
@FloatRange(from = 0.0, to = 1.0) initialPageOffset: Float = 0f,
|
|
||||||
@IntRange(from = 1) initialOffscreenLimit: Int = 1,
|
|
||||||
infiniteLoop: Boolean = false
|
|
||||||
): PagerState {
|
|
||||||
return rememberPagerState(initialPage = initialPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a [PagerState] that is remembered across compositions.
|
|
||||||
*
|
|
||||||
* Changes to the provided values for [initialPage] will **not** result in the state being recreated
|
|
||||||
* or changed in any way if it has already been created.
|
|
||||||
*
|
|
||||||
* @param initialPage the initial value for [PagerState.currentPage]
|
|
||||||
*/
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
@Composable
|
|
||||||
fun rememberPagerState(
|
|
||||||
@IntRange(from = 0) initialPage: Int = 0,
|
|
||||||
): PagerState =
|
|
||||||
rememberSaveable(saver = PagerState.Saver) {
|
|
||||||
PagerState(
|
|
||||||
currentPage = initialPage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A state object that can be hoisted to control and observe scrolling for [HorizontalPager].
|
|
||||||
*
|
|
||||||
* In most cases, this will be created via [rememberPagerState].
|
|
||||||
*
|
|
||||||
* @param currentPage the initial value for [PagerState.currentPage]
|
|
||||||
*/
|
|
||||||
@ExperimentalPagerApi
|
|
||||||
@Stable
|
|
||||||
class PagerState(
|
|
||||||
@IntRange(from = 0) currentPage: Int = 0,
|
|
||||||
) : ScrollableState {
|
|
||||||
// Should this be public?
|
|
||||||
internal val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
|
|
||||||
|
|
||||||
private var _currentPage by mutableStateOf(currentPage)
|
|
||||||
|
|
||||||
private val currentLayoutPageInfo: LazyListItemInfo?
|
|
||||||
get() =
|
|
||||||
lazyListState.layoutInfo.visibleItemsInfo
|
|
||||||
.asSequence()
|
|
||||||
.filter { it.offset <= 0 && it.offset + it.size > 0 }
|
|
||||||
.lastOrNull()
|
|
||||||
|
|
||||||
private val currentLayoutPageOffset: Float
|
|
||||||
get() =
|
|
||||||
currentLayoutPageInfo?.let { current ->
|
|
||||||
// We coerce since itemSpacing can make the offset > 1f.
|
|
||||||
// We don't want to count spacing in the offset so cap it to 1f
|
|
||||||
(-current.offset / current.size.toFloat()).coerceIn(0f, 1f)
|
|
||||||
}
|
|
||||||
?: 0f
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [InteractionSource] that will be used to dispatch drag events when this list is being
|
|
||||||
* dragged. If you want to know whether the fling (or animated scroll) is in progress, use
|
|
||||||
* [isScrollInProgress].
|
|
||||||
*/
|
|
||||||
val interactionSource: InteractionSource
|
|
||||||
get() = lazyListState.interactionSource
|
|
||||||
|
|
||||||
/** The number of pages to display. */
|
|
||||||
@get:IntRange(from = 0)
|
|
||||||
val pageCount: Int by derivedStateOf { lazyListState.layoutInfo.totalItemsCount }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The index of the currently selected page. This may not be the page which is currently
|
|
||||||
* displayed on screen.
|
|
||||||
*
|
|
||||||
* To update the scroll position, use [scrollToPage] or [animateScrollToPage].
|
|
||||||
*/
|
|
||||||
@get:IntRange(from = 0)
|
|
||||||
var currentPage: Int
|
|
||||||
get() = _currentPage
|
|
||||||
internal set(value) {
|
|
||||||
if (value != _currentPage) {
|
|
||||||
_currentPage = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current offset from the start of [currentPage], as a ratio of the page width.
|
|
||||||
*
|
|
||||||
* To update the scroll position, use [scrollToPage] or [animateScrollToPage].
|
|
||||||
*/
|
|
||||||
val currentPageOffset: Float by derivedStateOf {
|
|
||||||
currentLayoutPageInfo?.let {
|
|
||||||
// The current page offset is the current layout page delta from `currentPage`
|
|
||||||
// (which is only updated after a scroll/animation).
|
|
||||||
// We calculate this by looking at the current layout page + it's offset,
|
|
||||||
// then subtracting the 'current page'.
|
|
||||||
it.index + currentLayoutPageOffset - _currentPage
|
|
||||||
}
|
|
||||||
?: 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The target page for any on-going animations. */
|
|
||||||
private var animationTargetPage: Int? by mutableStateOf(null)
|
|
||||||
|
|
||||||
internal var flingAnimationTarget: (() -> Int?)? by mutableStateOf(null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The target page for any on-going animations or scrolls by the user. Returns the current page
|
|
||||||
* if a scroll or animation is not currently in progress.
|
|
||||||
*/
|
|
||||||
val targetPage: Int
|
|
||||||
get() =
|
|
||||||
animationTargetPage
|
|
||||||
?: flingAnimationTarget?.invoke()
|
|
||||||
?: when {
|
|
||||||
// If a scroll isn't in progress, return the current page
|
|
||||||
!isScrollInProgress -> currentPage
|
|
||||||
// If the offset is 0f (or very close), return the current page
|
|
||||||
currentPageOffset.absoluteValue < 0.001f -> currentPage
|
|
||||||
// If we're offset towards the start, guess the previous page
|
|
||||||
currentPageOffset < -0.5f -> (currentPage - 1).coerceAtLeast(0)
|
|
||||||
// If we're offset towards the end, guess the next page
|
|
||||||
else -> (currentPage + 1).coerceAtMost(pageCount - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated(
|
|
||||||
"Replaced with animateScrollToPage(page, pageOffset)",
|
|
||||||
ReplaceWith("animateScrollToPage(page = page, pageOffset = pageOffset)")
|
|
||||||
)
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
suspend fun animateScrollToPage(
|
|
||||||
@IntRange(from = 0) page: Int,
|
|
||||||
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
|
|
||||||
animationSpec: AnimationSpec<Float> = spring(),
|
|
||||||
initialVelocity: Float = 0f,
|
|
||||||
skipPages: Boolean = true,
|
|
||||||
) {
|
|
||||||
animateScrollToPage(page = page, pageOffset = pageOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animate (smooth scroll) to the given page to the middle of the viewport.
|
|
||||||
*
|
|
||||||
* Cancels the currently running scroll, if any, and suspends until the cancellation is
|
|
||||||
* complete.
|
|
||||||
*
|
|
||||||
* @param page the page to animate to. Must be between 0 and [pageCount] (inclusive).
|
|
||||||
* @param pageOffset the percentage of the page width to offset, from the start of [page]. Must
|
|
||||||
* be in the range 0f..1f.
|
|
||||||
*/
|
|
||||||
suspend fun animateScrollToPage(
|
|
||||||
@IntRange(from = 0) page: Int,
|
|
||||||
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
|
|
||||||
) {
|
|
||||||
requireCurrentPage(page, "page")
|
|
||||||
requireCurrentPageOffset(pageOffset, "pageOffset")
|
|
||||||
try {
|
|
||||||
animationTargetPage = page
|
|
||||||
|
|
||||||
if (pageOffset <= 0.005f) {
|
|
||||||
// If the offset is (close to) zero, just call animateScrollToItem and we're done
|
|
||||||
lazyListState.animateScrollToItem(index = page)
|
|
||||||
} else {
|
|
||||||
// Else we need to figure out what the offset is in pixels...
|
|
||||||
|
|
||||||
var target =
|
|
||||||
lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == page }
|
|
||||||
|
|
||||||
if (target != null) {
|
|
||||||
// If we have access to the target page layout, we can calculate the pixel
|
|
||||||
// offset from the size
|
|
||||||
lazyListState.animateScrollToItem(
|
|
||||||
index = page,
|
|
||||||
scrollOffset = (target.size * pageOffset).roundToInt()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// If we don't, we use the current page size as a guide
|
|
||||||
val currentSize = currentLayoutPageInfo!!.size
|
|
||||||
lazyListState.animateScrollToItem(
|
|
||||||
index = page,
|
|
||||||
scrollOffset = (currentSize * pageOffset).roundToInt()
|
|
||||||
)
|
|
||||||
|
|
||||||
// The target should be visible now
|
|
||||||
target = lazyListState.layoutInfo.visibleItemsInfo.first { it.index == page }
|
|
||||||
|
|
||||||
if (target.size != currentSize) {
|
|
||||||
// If the size we used for calculating the offset differs from the actual
|
|
||||||
// target page size, we need to scroll again. This doesn't look great,
|
|
||||||
// but there's not much else we can do.
|
|
||||||
lazyListState.animateScrollToItem(
|
|
||||||
index = page,
|
|
||||||
scrollOffset = (target.size * pageOffset).roundToInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// We need to manually call this, as the `animateScrollToItem` call above will happen
|
|
||||||
// in 1 frame, which is usually too fast for the LaunchedEffect in Pager to detect
|
|
||||||
// the change. This is especially true when running unit tests.
|
|
||||||
onScrollFinished()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantly brings the item at [page] to the middle of the viewport.
|
|
||||||
*
|
|
||||||
* Cancels the currently running scroll, if any, and suspends until the cancellation is
|
|
||||||
* complete.
|
|
||||||
*
|
|
||||||
* @param page the page to snap to. Must be between 0 and [pageCount] (inclusive).
|
|
||||||
*/
|
|
||||||
suspend fun scrollToPage(
|
|
||||||
@IntRange(from = 0) page: Int,
|
|
||||||
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
|
|
||||||
) {
|
|
||||||
requireCurrentPage(page, "page")
|
|
||||||
requireCurrentPageOffset(pageOffset, "pageOffset")
|
|
||||||
try {
|
|
||||||
animationTargetPage = page
|
|
||||||
|
|
||||||
// First scroll to the given page. It will now be laid out at offset 0
|
|
||||||
lazyListState.scrollToItem(index = page)
|
|
||||||
|
|
||||||
// If we have a start spacing, we need to offset (scroll) by that too
|
|
||||||
if (pageOffset > 0.0001f) {
|
|
||||||
scroll { currentLayoutPageInfo?.let { scrollBy(it.size * pageOffset) } }
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// We need to manually call this, as the `scroll` call above will happen in 1 frame,
|
|
||||||
// which is usually too fast for the LaunchedEffect in Pager to detect the change.
|
|
||||||
// This is especially true when running unit tests.
|
|
||||||
onScrollFinished()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun onScrollFinished() {
|
|
||||||
// Then update the current page to our layout page
|
|
||||||
currentPage = currentLayoutPageInfo?.index ?: 0
|
|
||||||
// Clear the animation target page
|
|
||||||
animationTargetPage = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun scroll(
|
|
||||||
scrollPriority: MutatePriority,
|
|
||||||
block: suspend ScrollScope.() -> Unit
|
|
||||||
) = lazyListState.scroll(scrollPriority, block)
|
|
||||||
|
|
||||||
override fun dispatchRawDelta(delta: Float): Float {
|
|
||||||
return lazyListState.dispatchRawDelta(delta)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isScrollInProgress: Boolean
|
|
||||||
get() = lazyListState.isScrollInProgress
|
|
||||||
|
|
||||||
override fun toString(): String =
|
|
||||||
"PagerState(" +
|
|
||||||
"pageCount=$pageCount, " +
|
|
||||||
"currentPage=$currentPage, " +
|
|
||||||
"currentPageOffset=$currentPageOffset" +
|
|
||||||
")"
|
|
||||||
|
|
||||||
private fun requireCurrentPage(value: Int, name: String) {
|
|
||||||
if (pageCount == 0) {
|
|
||||||
require(value == 0) { "$name must be 0 when pageCount is 0" }
|
|
||||||
} else {
|
|
||||||
require(value in 0 until pageCount) { "$name[$value] must be >= 0 and < pageCount" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requireCurrentPageOffset(value: Float, name: String) {
|
|
||||||
if (pageCount == 0) {
|
|
||||||
require(value == 0f) { "$name must be 0f when pageCount is 0" }
|
|
||||||
} else {
|
|
||||||
require(value in 0f..1f) { "$name must be >= 0 and <= 1" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** The default [Saver] implementation for [PagerState]. */
|
|
||||||
val Saver: Saver<PagerState, *> =
|
|
||||||
listSaver(
|
|
||||||
save = {
|
|
||||||
listOf<Any>(
|
|
||||||
it.currentPage,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
restore = {
|
|
||||||
PagerState(
|
|
||||||
currentPage = it[0] as Int,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.android.compose.pager
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.AnimationSpec
|
|
||||||
import androidx.compose.animation.core.AnimationState
|
|
||||||
import androidx.compose.animation.core.DecayAnimationSpec
|
|
||||||
import androidx.compose.animation.core.animateDecay
|
|
||||||
import androidx.compose.animation.core.animateTo
|
|
||||||
import androidx.compose.animation.core.calculateTargetValue
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.animation.rememberSplineBasedDecay
|
|
||||||
import androidx.compose.foundation.gestures.FlingBehavior
|
|
||||||
import androidx.compose.foundation.gestures.ScrollScope
|
|
||||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/** Default values used for [SnappingFlingBehavior] & [rememberSnappingFlingBehavior]. */
|
|
||||||
internal object SnappingFlingBehaviorDefaults {
|
|
||||||
/** TODO */
|
|
||||||
val snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = 600f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and remember a snapping [FlingBehavior] to be used with [LazyListState].
|
|
||||||
*
|
|
||||||
* @param lazyListState The [LazyListState] to update.
|
|
||||||
* @param decayAnimationSpec The decay animation spec to use for decayed flings.
|
|
||||||
* @param snapAnimationSpec The animation spec to use when snapping.
|
|
||||||
*
|
|
||||||
* TODO: move this to a new module and make it public
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
internal fun rememberSnappingFlingBehavior(
|
|
||||||
lazyListState: LazyListState,
|
|
||||||
decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
|
|
||||||
snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
|
|
||||||
): SnappingFlingBehavior =
|
|
||||||
remember(lazyListState, decayAnimationSpec, snapAnimationSpec) {
|
|
||||||
SnappingFlingBehavior(
|
|
||||||
lazyListState = lazyListState,
|
|
||||||
decayAnimationSpec = decayAnimationSpec,
|
|
||||||
snapAnimationSpec = snapAnimationSpec,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A snapping [FlingBehavior] for [LazyListState]. Typically this would be created via
|
|
||||||
* [rememberSnappingFlingBehavior].
|
|
||||||
*
|
|
||||||
* @param lazyListState The [LazyListState] to update.
|
|
||||||
* @param decayAnimationSpec The decay animation spec to use for decayed flings.
|
|
||||||
* @param snapAnimationSpec The animation spec to use when snapping.
|
|
||||||
*/
|
|
||||||
internal class SnappingFlingBehavior(
|
|
||||||
private val lazyListState: LazyListState,
|
|
||||||
private val decayAnimationSpec: DecayAnimationSpec<Float>,
|
|
||||||
private val snapAnimationSpec: AnimationSpec<Float>,
|
|
||||||
) : FlingBehavior {
|
|
||||||
/** The target item index for any on-going animations. */
|
|
||||||
var animationTarget: Int? by mutableStateOf(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
|
|
||||||
val itemInfo = currentItemInfo ?: return initialVelocity
|
|
||||||
|
|
||||||
// If the decay fling can scroll past the current item, fling with decay
|
|
||||||
return if (decayAnimationSpec.canFlingPastCurrentItem(itemInfo, initialVelocity)) {
|
|
||||||
performDecayFling(initialVelocity, itemInfo)
|
|
||||||
} else {
|
|
||||||
// Otherwise we 'spring' to current/next item
|
|
||||||
performSpringFling(
|
|
||||||
index =
|
|
||||||
when {
|
|
||||||
// If the velocity is greater than 1 item per second (velocity is px/s),
|
|
||||||
// spring
|
|
||||||
// in the relevant direction
|
|
||||||
initialVelocity > itemInfo.size -> {
|
|
||||||
(itemInfo.index + 1).coerceAtMost(
|
|
||||||
lazyListState.layoutInfo.totalItemsCount - 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
initialVelocity < -itemInfo.size -> itemInfo.index
|
|
||||||
// If the velocity is 0 (or less than the size of the item), spring to
|
|
||||||
// whichever item is closest to the snap point
|
|
||||||
itemInfo.offset < -itemInfo.size / 2 -> itemInfo.index + 1
|
|
||||||
else -> itemInfo.index
|
|
||||||
},
|
|
||||||
initialVelocity = initialVelocity,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun ScrollScope.performDecayFling(
|
|
||||||
initialVelocity: Float,
|
|
||||||
startItem: LazyListItemInfo,
|
|
||||||
): Float {
|
|
||||||
val index =
|
|
||||||
when {
|
|
||||||
initialVelocity > 0 -> startItem.index + 1
|
|
||||||
else -> startItem.index
|
|
||||||
}
|
|
||||||
val forward = index > (currentItemInfo?.index ?: return initialVelocity)
|
|
||||||
|
|
||||||
// Update the animationTarget
|
|
||||||
animationTarget = index
|
|
||||||
|
|
||||||
var velocityLeft = initialVelocity
|
|
||||||
var lastValue = 0f
|
|
||||||
AnimationState(
|
|
||||||
initialValue = 0f,
|
|
||||||
initialVelocity = initialVelocity,
|
|
||||||
)
|
|
||||||
.animateDecay(decayAnimationSpec) {
|
|
||||||
val delta = value - lastValue
|
|
||||||
val consumed = scrollBy(delta)
|
|
||||||
lastValue = value
|
|
||||||
velocityLeft = this.velocity
|
|
||||||
|
|
||||||
val current = currentItemInfo
|
|
||||||
if (current == null) {
|
|
||||||
cancelAnimation()
|
|
||||||
return@animateDecay
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!forward &&
|
|
||||||
(current.index < index || current.index == index && current.offset >= 0)
|
|
||||||
) {
|
|
||||||
// 'snap back' to the item as we may have scrolled past it
|
|
||||||
scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
|
|
||||||
cancelAnimation()
|
|
||||||
} else if (
|
|
||||||
forward &&
|
|
||||||
(current.index > index || current.index == index && current.offset <= 0)
|
|
||||||
) {
|
|
||||||
// 'snap back' to the item as we may have scrolled past it
|
|
||||||
scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
|
|
||||||
cancelAnimation()
|
|
||||||
} else if (abs(delta - consumed) > 0.5f) {
|
|
||||||
// avoid rounding errors and stop if anything is unconsumed
|
|
||||||
cancelAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
animationTarget = null
|
|
||||||
return velocityLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun ScrollScope.performSpringFling(
|
|
||||||
index: Int,
|
|
||||||
scrollOffset: Int = 0,
|
|
||||||
initialVelocity: Float = 0f,
|
|
||||||
): Float {
|
|
||||||
// If we don't have a current layout, we can't snap
|
|
||||||
val initialItem = currentItemInfo ?: return initialVelocity
|
|
||||||
|
|
||||||
val forward = index > initialItem.index
|
|
||||||
// We add 10% on to the size of the current item, to compensate for any item spacing, etc
|
|
||||||
val target = (if (forward) initialItem.size else -initialItem.size) * 1.1f
|
|
||||||
|
|
||||||
// Update the animationTarget
|
|
||||||
animationTarget = index
|
|
||||||
|
|
||||||
var velocityLeft = initialVelocity
|
|
||||||
var lastValue = 0f
|
|
||||||
AnimationState(
|
|
||||||
initialValue = 0f,
|
|
||||||
initialVelocity = initialVelocity,
|
|
||||||
)
|
|
||||||
.animateTo(
|
|
||||||
targetValue = target,
|
|
||||||
animationSpec = snapAnimationSpec,
|
|
||||||
) {
|
|
||||||
// Springs can overshoot their target, clamp to the desired range
|
|
||||||
val coercedValue =
|
|
||||||
if (forward) {
|
|
||||||
value.coerceAtMost(target)
|
|
||||||
} else {
|
|
||||||
value.coerceAtLeast(target)
|
|
||||||
}
|
|
||||||
val delta = coercedValue - lastValue
|
|
||||||
val consumed = scrollBy(delta)
|
|
||||||
lastValue = coercedValue
|
|
||||||
velocityLeft = this.velocity
|
|
||||||
|
|
||||||
val current = currentItemInfo
|
|
||||||
if (current == null) {
|
|
||||||
cancelAnimation()
|
|
||||||
return@animateTo
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrolledPastItem(initialVelocity, current, index, scrollOffset)) {
|
|
||||||
// If we've scrolled to/past the item, stop the animation. We may also need to
|
|
||||||
// 'snap back' to the item as we may have scrolled past it
|
|
||||||
scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
|
|
||||||
cancelAnimation()
|
|
||||||
} else if (abs(delta - consumed) > 0.5f) {
|
|
||||||
// avoid rounding errors and stop if anything is unconsumed
|
|
||||||
cancelAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
animationTarget = null
|
|
||||||
return velocityLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LazyListState.calculateScrollOffsetToItem(index: Int): Int {
|
|
||||||
return layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }?.offset ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private val currentItemInfo: LazyListItemInfo?
|
|
||||||
get() =
|
|
||||||
lazyListState.layoutInfo.visibleItemsInfo
|
|
||||||
.asSequence()
|
|
||||||
.filter { it.offset <= 0 && it.offset + it.size > 0 }
|
|
||||||
.lastOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrolledPastItem(
|
|
||||||
initialVelocity: Float,
|
|
||||||
currentItem: LazyListItemInfo,
|
|
||||||
targetIndex: Int,
|
|
||||||
targetScrollOffset: Int = 0,
|
|
||||||
): Boolean {
|
|
||||||
return if (initialVelocity > 0) {
|
|
||||||
// forward
|
|
||||||
currentItem.index > targetIndex ||
|
|
||||||
(currentItem.index == targetIndex && currentItem.offset <= targetScrollOffset)
|
|
||||||
} else {
|
|
||||||
// backwards
|
|
||||||
currentItem.index < targetIndex ||
|
|
||||||
(currentItem.index == targetIndex && currentItem.offset >= targetScrollOffset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun DecayAnimationSpec<Float>.canFlingPastCurrentItem(
|
|
||||||
currentItem: LazyListItemInfo,
|
|
||||||
initialVelocity: Float,
|
|
||||||
): Boolean {
|
|
||||||
val targetValue =
|
|
||||||
calculateTargetValue(
|
|
||||||
initialValue = currentItem.offset.toFloat(),
|
|
||||||
initialVelocity = initialVelocity,
|
|
||||||
)
|
|
||||||
return when {
|
|
||||||
// forward. We add 10% onto the size to cater for any item spacing
|
|
||||||
initialVelocity > 0 -> targetValue <= -(currentItem.size * 1.1f)
|
|
||||||
// backwards. We add 10% onto the size to cater for any item spacing
|
|
||||||
else -> targetValue >= (currentItem.size * 0.1f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user