diff options
| author | 2023-07-24 16:14:52 +0000 | |
|---|---|---|
| committer | 2023-07-24 16:14:52 +0000 | |
| commit | 6da341a9f2adfbd3a1912ca7e3276f3e865e87fd (patch) | |
| tree | 04dd12d031a35288a59517a95f339992aebd1a34 | |
| parent | 8b2888e4e71e588381ce87aa032f196943386c88 (diff) | |
| parent | ec606cbaaffe47975b90ac4fe0c5d7aa7b7fdf08 (diff) | |
Merge "Remove Pager from PlatformComposeCore (1/2)" into udc-qpr-dev am: ef900a3efa am: ec606cbaaf
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/24146484
Change-Id: I0f8fd080350df3809c9eea77e7549562166aa354
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
3 files changed, 0 insertions, 975 deletions
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt deleted file mode 100644 index a80a1f934dab..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt +++ /dev/null @@ -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 -} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt deleted file mode 100644 index 1822a68f1e77..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt +++ /dev/null @@ -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, - ) - } - ) - } -} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt deleted file mode 100644 index 98140295306a..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt +++ /dev/null @@ -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) - } -} |