summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> 2023-07-24 15:31:22 +0000
committer Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> 2023-07-24 15:31:22 +0000
commitec606cbaaffe47975b90ac4fe0c5d7aa7b7fdf08 (patch)
treee916c03691007ef24d7bc427e8ee7e606866297b
parent4cb5d79088819e8795d4004ebe5c3367bb2025ed (diff)
parentef900a3efad12e10ca8c561a6d8a3f3cb5df6d59 (diff)
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>
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt357
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt348
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt270
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)
- }
-}