1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.foundation.pager
18 
19 import androidx.annotation.FloatRange
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.DecayAnimationSpec
22 import androidx.compose.animation.core.Spring
23 import androidx.compose.animation.core.VisibilityThreshold
24 import androidx.compose.animation.core.spring
25 import androidx.compose.animation.rememberSplineBasedDecay
26 import androidx.compose.foundation.OverscrollEffect
27 import androidx.compose.foundation.gestures.FlingBehavior
28 import androidx.compose.foundation.gestures.Orientation
29 import androidx.compose.foundation.gestures.TargetedFlingBehavior
30 import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
31 import androidx.compose.foundation.gestures.snapping.SnapPosition
32 import androidx.compose.foundation.gestures.snapping.calculateFinalSnappingBound
33 import androidx.compose.foundation.gestures.snapping.snapFlingBehavior
34 import androidx.compose.foundation.internal.requirePrecondition
35 import androidx.compose.foundation.layout.PaddingValues
36 import androidx.compose.foundation.rememberOverscrollEffect
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.remember
39 import androidx.compose.ui.Alignment
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.geometry.Offset
42 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
43 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
44 import androidx.compose.ui.platform.LocalDensity
45 import androidx.compose.ui.platform.LocalLayoutDirection
46 import androidx.compose.ui.semantics.pageDown
47 import androidx.compose.ui.semantics.pageLeft
48 import androidx.compose.ui.semantics.pageRight
49 import androidx.compose.ui.semantics.pageUp
50 import androidx.compose.ui.semantics.semantics
51 import androidx.compose.ui.unit.Dp
52 import androidx.compose.ui.unit.Velocity
53 import androidx.compose.ui.unit.dp
54 import kotlin.math.abs
55 import kotlin.math.roundToInt
56 import kotlin.math.sign
57 import kotlinx.coroutines.CancellationException
58 import kotlinx.coroutines.CoroutineScope
59 import kotlinx.coroutines.launch
60 
61 /**
62  * A Pager that scrolls horizontally. Pages are lazily placed in accordance to the available
63  * viewport size. By definition, pages in a [Pager] have the same size, defined by [pageSize] and
64  * use a snap animation (provided by [flingBehavior] to scroll pages into a specific position). You
65  * can use [beyondViewportPageCount] to place more pages before and after the visible pages.
66  *
67  * If you need snapping with pages of different size, you can use a [snapFlingBehavior] with a
68  * [SnapLayoutInfoProvider] adapted to a LazyList.
69  *
70  * @param state The state to control this pager
71  * @param modifier A modifier instance to be applied to this Pager outer layout
72  * @param contentPadding a padding around the whole content. This will add padding for the content
73  *   after it has been clipped, which is not possible via [modifier] param. You can use it to add a
74  *   padding before the first page or after the last one. Use [pageSpacing] to add spacing between
75  *   the pages.
76  * @param pageSize Use this to change how the pages will look like inside this pager.
77  * @param beyondViewportPageCount Pages to compose and layout before and after the list of visible
78  *   pages. Note: Be aware that using a large value for [beyondViewportPageCount] will cause a lot
79  *   of pages to be composed, measured and placed which will defeat the purpose of using lazy
80  *   loading. This should be used as an optimization to pre-load a couple of pages before and after
81  *   the visible ones. This does not include the pages automatically composed and laid out by the
82  *   pre-fetcher in the direction of the scroll during scroll events.
83  * @param pageSpacing The amount of space to be used to separate the pages in this Pager
84  * @param verticalAlignment How pages are aligned vertically in this Pager.
85  * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures.
86  * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is
87  *   allowed. You can still scroll programmatically using [PagerState.scroll] even when it is
88  *   disabled.
89  * @param reverseLayout reverse the direction of scrolling and layout.
90  * @param key a stable and unique key representing the item. When you specify the key the scroll
91  *   position will be maintained based on the key, which means if you add/remove items before the
92  *   current visible item the item with the given key will be kept as the first visible one. If null
93  *   is passed the position in the list will represent the key.
94  * @param pageNestedScrollConnection A [NestedScrollConnection] that dictates how this [Pager]
95  *   behaves with nested lists. The default behavior will see [Pager] to consume all nested deltas.
96  * @param snapPosition The calculation of how this Pager will perform snapping of pages. Use this to
97  *   provide different settling to different positions in the layout. This is used by [Pager] as a
98  *   way to calculate [PagerState.currentPage], currentPage is the page closest to the snap position
99  *   in the layout (e.g. if the snap position is the start of the layout, then currentPage will be
100  *   the page closest to that).
101  * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this
102  *   Pager. Note that the [OverscrollEffect.node] will be applied internally as well - you do not
103  *   need to use Modifier.overscroll separately.
104  * @param pageContent This Pager's page Composable.
105  * @sample androidx.compose.foundation.samples.SimpleHorizontalPagerSample
106  * @sample androidx.compose.foundation.samples.HorizontalPagerWithScrollableContent
107  * @see androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider for the implementation
108  *   of a [SnapLayoutInfoProvider] that uses [androidx.compose.foundation.lazy.LazyListState].
109  *
110  * Please refer to the samples to learn how to use this API.
111  */
112 @Composable
113 fun HorizontalPager(
114     state: PagerState,
115     modifier: Modifier = Modifier,
116     contentPadding: PaddingValues = PaddingValues(0.dp),
117     pageSize: PageSize = PageSize.Fill,
118     beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount,
119     pageSpacing: Dp = 0.dp,
120     verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
121     flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state),
122     userScrollEnabled: Boolean = true,
123     reverseLayout: Boolean = false,
124     key: ((index: Int) -> Any)? = null,
125     pageNestedScrollConnection: NestedScrollConnection =
126         PagerDefaults.pageNestedScrollConnection(state, Orientation.Horizontal),
127     snapPosition: SnapPosition = SnapPosition.Start,
128     overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(),
129     pageContent: @Composable PagerScope.(page: Int) -> Unit
130 ) {
131     Pager(
132         state = state,
133         modifier = modifier,
134         contentPadding = contentPadding,
135         pageSize = pageSize,
136         beyondViewportPageCount = beyondViewportPageCount,
137         pageSpacing = pageSpacing,
138         orientation = Orientation.Horizontal,
139         verticalAlignment = verticalAlignment,
140         horizontalAlignment = Alignment.CenterHorizontally,
141         flingBehavior = flingBehavior,
142         userScrollEnabled = userScrollEnabled,
143         reverseLayout = reverseLayout,
144         key = key,
145         pageNestedScrollConnection = pageNestedScrollConnection,
146         snapPosition = snapPosition,
147         overscrollEffect = overscrollEffect,
148         pageContent = pageContent
149     )
150 }
151 
152 @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN)
153 @Composable
HorizontalPagernull154 fun HorizontalPager(
155     state: PagerState,
156     modifier: Modifier = Modifier,
157     contentPadding: PaddingValues = PaddingValues(0.dp),
158     pageSize: PageSize = PageSize.Fill,
159     beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount,
160     pageSpacing: Dp = 0.dp,
161     verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
162     flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state),
163     userScrollEnabled: Boolean = true,
164     reverseLayout: Boolean = false,
165     key: ((index: Int) -> Any)? = null,
166     pageNestedScrollConnection: NestedScrollConnection =
167         PagerDefaults.pageNestedScrollConnection(state, Orientation.Horizontal),
168     snapPosition: SnapPosition = SnapPosition.Start,
169     pageContent: @Composable PagerScope.(page: Int) -> Unit
170 ) {
171     HorizontalPager(
172         state = state,
173         modifier = modifier,
174         contentPadding = contentPadding,
175         pageSize = pageSize,
176         beyondViewportPageCount = beyondViewportPageCount,
177         pageSpacing = pageSpacing,
178         verticalAlignment = verticalAlignment,
179         flingBehavior = flingBehavior,
180         userScrollEnabled = userScrollEnabled,
181         reverseLayout = reverseLayout,
182         key = key,
183         pageNestedScrollConnection = pageNestedScrollConnection,
184         snapPosition = snapPosition,
185         overscrollEffect = rememberOverscrollEffect(),
186         pageContent = pageContent
187     )
188 }
189 
190 /**
191  * A Pager that scrolls vertically. Pages are lazily placed in accordance to the available viewport
192  * size. By definition, pages in a [Pager] have the same size, defined by [pageSize] and use a snap
193  * animation (provided by [flingBehavior] to scroll pages into a specific position). You can use
194  * [beyondViewportPageCount] to place more pages before and after the visible pages.
195  *
196  * If you need snapping with pages of different size, you can use a [snapFlingBehavior] with a
197  * [SnapLayoutInfoProvider] adapted to a LazyList.
198  *
199  * @param state The state to control this pager
200  * @param modifier A modifier instance to be apply to this Pager outer layout
201  * @param contentPadding a padding around the whole content. This will add padding for the content
202  *   after it has been clipped, which is not possible via [modifier] param. You can use it to add a
203  *   padding before the first page or after the last one. Use [pageSpacing] to add spacing between
204  *   the pages.
205  * @param pageSize Use this to change how the pages will look like inside this pager.
206  * @param beyondViewportPageCount Pages to compose and layout before and after the list of visible
207  *   pages. Note: Be aware that using a large value for [beyondViewportPageCount] will cause a lot
208  *   of pages to be composed, measured and placed which will defeat the purpose of using lazy
209  *   loading. This should be used as an optimization to pre-load a couple of pages before and after
210  *   the visible ones. This does not include the pages automatically composed and laid out by the
211  *   pre-fetcher in
212  *     * the direction of the scroll during scroll events.
213  *
214  * @param pageSpacing The amount of space to be used to separate the pages in this Pager
215  * @param horizontalAlignment How pages are aligned horizontally in this Pager.
216  * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures.
217  * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is
218  *   allowed. You can still scroll programmatically using [PagerState.scroll] even when it is
219  *   disabled.
220  * @param reverseLayout reverse the direction of scrolling and layout.
221  * @param key a stable and unique key representing the item. When you specify the key the scroll
222  *   position will be maintained based on the key, which means if you add/remove items before the
223  *   current visible item the item with the given key will be kept as the first visible one. If null
224  *   is passed the position in the list will represent the key.
225  * @param pageNestedScrollConnection A [NestedScrollConnection] that dictates how this [Pager]
226  *   behaves with nested lists. The default behavior will see [Pager] to consume all nested deltas.
227  * @param snapPosition The calculation of how this Pager will perform snapping of Pages. Use this to
228  *   provide different settling to different positions in the layout. This is used by [Pager] as a
229  *   way to calculate [PagerState.currentPage], currentPage is the page closest to the snap position
230  *   in the layout (e.g. if the snap position is the start of the layout, then currentPage will be
231  *   the page closest to that).
232  * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this
233  *   Pager. Note that the [OverscrollEffect.node] will be applied internally as well - you do not
234  *   need to use Modifier.overscroll separately.
235  * @param pageContent This Pager's page Composable.
236  * @sample androidx.compose.foundation.samples.SimpleVerticalPagerSample
237  * @see androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider for the implementation
238  *   of a [SnapLayoutInfoProvider] that uses [androidx.compose.foundation.lazy.LazyListState].
239  *
240  * Please refer to the sample to learn how to use this API.
241  */
242 @Composable
VerticalPagernull243 fun VerticalPager(
244     state: PagerState,
245     modifier: Modifier = Modifier,
246     contentPadding: PaddingValues = PaddingValues(0.dp),
247     pageSize: PageSize = PageSize.Fill,
248     beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount,
249     pageSpacing: Dp = 0.dp,
250     horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
251     flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state),
252     userScrollEnabled: Boolean = true,
253     reverseLayout: Boolean = false,
254     key: ((index: Int) -> Any)? = null,
255     pageNestedScrollConnection: NestedScrollConnection =
256         PagerDefaults.pageNestedScrollConnection(state, Orientation.Vertical),
257     snapPosition: SnapPosition = SnapPosition.Start,
258     overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(),
259     pageContent: @Composable PagerScope.(page: Int) -> Unit
260 ) {
261     Pager(
262         state = state,
263         modifier = modifier,
264         contentPadding = contentPadding,
265         pageSize = pageSize,
266         beyondViewportPageCount = beyondViewportPageCount,
267         pageSpacing = pageSpacing,
268         orientation = Orientation.Vertical,
269         verticalAlignment = Alignment.CenterVertically,
270         horizontalAlignment = horizontalAlignment,
271         flingBehavior = flingBehavior,
272         userScrollEnabled = userScrollEnabled,
273         reverseLayout = reverseLayout,
274         key = key,
275         pageNestedScrollConnection = pageNestedScrollConnection,
276         snapPosition = snapPosition,
277         overscrollEffect = overscrollEffect,
278         pageContent = pageContent
279     )
280 }
281 
282 @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN)
283 @Composable
VerticalPagernull284 fun VerticalPager(
285     state: PagerState,
286     modifier: Modifier = Modifier,
287     contentPadding: PaddingValues = PaddingValues(0.dp),
288     pageSize: PageSize = PageSize.Fill,
289     beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount,
290     pageSpacing: Dp = 0.dp,
291     horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
292     flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state),
293     userScrollEnabled: Boolean = true,
294     reverseLayout: Boolean = false,
295     key: ((index: Int) -> Any)? = null,
296     pageNestedScrollConnection: NestedScrollConnection =
297         PagerDefaults.pageNestedScrollConnection(state, Orientation.Vertical),
298     snapPosition: SnapPosition = SnapPosition.Start,
299     pageContent: @Composable PagerScope.(page: Int) -> Unit
300 ) {
301     VerticalPager(
302         state = state,
303         modifier = modifier,
304         contentPadding = contentPadding,
305         pageSize = pageSize,
306         beyondViewportPageCount = beyondViewportPageCount,
307         pageSpacing = pageSpacing,
308         horizontalAlignment = horizontalAlignment,
309         flingBehavior = flingBehavior,
310         userScrollEnabled = userScrollEnabled,
311         reverseLayout = reverseLayout,
312         key = key,
313         pageNestedScrollConnection = pageNestedScrollConnection,
314         snapPosition = snapPosition,
315         overscrollEffect = rememberOverscrollEffect(),
316         pageContent = pageContent
317     )
318 }
319 
320 /** Contains the default values used by [Pager]. */
321 object PagerDefaults {
322 
323     /**
324      * A [snapFlingBehavior] that will snap pages to the start of the layout. One can use the given
325      * parameters to control how the snapping animation will happen.
326      *
327      * @param state The [PagerState] that controls the which to which this FlingBehavior will be
328      *   applied to.
329      * @param pagerSnapDistance A way to control the snapping destination for this [Pager]. The
330      *   default behavior will result in any fling going to the next page in the direction of the
331      *   fling (if the fling has enough velocity, otherwise the Pager will bounce back). Use
332      *   [PagerSnapDistance.atMost] to define a maximum number of pages this [Pager] is allowed to
333      *   fling after scrolling is finished and fling has started.
334      * @param decayAnimationSpec The animation spec used to approach the target offset. When the
335      *   fling velocity is large enough. Large enough means large enough to naturally decay. For
336      *   single page snapping this usually never happens since there won't be enough space to run a
337      *   decay animation.
338      * @param snapAnimationSpec The animation spec used to finally snap to the position. This
339      *   animation will be often used in 2 cases: 1) There was enough space to an approach
340      *   animation, the Pager will use [snapAnimationSpec] in the last step of the animation to
341      *   settle the page into position. 2) There was not enough space to run the approach animation.
342      * @param snapPositionalThreshold If the fling has a low velocity (e.g. slow scroll), this fling
343      *   behavior will use this snap threshold in order to determine if the pager should snap back
344      *   or move forward. Use a number between 0 and 1 as a fraction of the page size that needs to
345      *   be scrolled before the Pager considers it should move to the next page. For instance, if
346      *   snapPositionalThreshold = 0.35, it means if this pager is scrolled with a slow velocity and
347      *   the Pager scrolls more than 35% of the page size, then will jump to the next page, if not
348      *   it scrolls back. Note that any fling that has high enough velocity will *always* move to
349      *   the next page in the direction of the fling.
350      * @return An instance of [FlingBehavior] that will perform Snapping to the next page by
351      *   default. The animation will be governed by the post scroll velocity and the Pager will use
352      *   either [snapAnimationSpec] or [decayAnimationSpec] to approach the snapped position If a
353      *   velocity is not high enough the pager will use [snapAnimationSpec] to reach the snapped
354      *   position. If the velocity is high enough, the Pager will use the logic described in
355      *   [decayAnimationSpec] and [snapAnimationSpec].
356      * @see androidx.compose.foundation.gestures.snapping.snapFlingBehavior for more information on
357      *   what which parameter controls in the overall snapping animation.
358      *
359      * The animation specs used by the fling behavior will depend on 2 factors:
360      * 1) The gesture velocity.
361      * 2) The target page proposed by [pagerSnapDistance].
362      *
363      * If you're using single page snapping (the most common use case for [Pager]), there won't be
364      * enough space to actually run a decay animation to approach the target page, so the Pager will
365      * always use the snapping animation from [snapAnimationSpec]. If you're using multi-page
366      * snapping (this means you're abs(targetPage - currentPage) > 1) the Pager may use
367      * [decayAnimationSpec] or [snapAnimationSpec] to approach the targetPage, it will depend on the
368      * velocity generated by the triggering gesture. If the gesture has a high enough velocity to
369      * approach the target page, the Pager will use [decayAnimationSpec] followed by
370      * [snapAnimationSpec] for the final step of the animation. If the gesture doesn't have enough
371      * velocity, the Pager will use [snapAnimationSpec] + [snapAnimationSpec] in a similar fashion.
372      */
373     @Composable
flingBehaviornull374     fun flingBehavior(
375         state: PagerState,
376         pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1),
377         decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
378         snapAnimationSpec: AnimationSpec<Float> =
379             spring(
380                 stiffness = Spring.StiffnessMediumLow,
381                 visibilityThreshold = Int.VisibilityThreshold.toFloat()
382             ),
383         @FloatRange(from = 0.0, to = 1.0) snapPositionalThreshold: Float = 0.5f
384     ): TargetedFlingBehavior {
385         requirePrecondition(snapPositionalThreshold in 0f..1f) {
386             "snapPositionalThreshold should be a number between 0 and 1. " +
387                 "You've specified $snapPositionalThreshold"
388         }
389         val density = LocalDensity.current
390         val layoutDirection = LocalLayoutDirection.current
391         return remember(
392             state,
393             decayAnimationSpec,
394             snapAnimationSpec,
395             pagerSnapDistance,
396             density,
397             layoutDirection
398         ) {
399             val snapLayoutInfoProvider =
400                 SnapLayoutInfoProvider(state, pagerSnapDistance) {
401                     flingVelocity,
402                     lowerBound,
403                     upperBound ->
404                     calculateFinalSnappingBound(
405                         pagerState = state,
406                         layoutDirection = layoutDirection,
407                         snapPositionalThreshold = snapPositionalThreshold,
408                         flingVelocity = flingVelocity,
409                         lowerBoundOffset = lowerBound,
410                         upperBoundOffset = upperBound
411                     )
412                 }
413 
414             snapFlingBehavior(
415                 snapLayoutInfoProvider = snapLayoutInfoProvider,
416                 decayAnimationSpec = decayAnimationSpec,
417                 snapAnimationSpec = snapAnimationSpec
418             )
419         }
420     }
421 
422     /**
423      * The default implementation of Pager's pageNestedScrollConnection.
424      *
425      * @param state state of the pager
426      * @param orientation The orientation of the pager. This will be used to determine which
427      *   direction the nested scroll connection will operate and react on.
428      */
429     @Composable
pageNestedScrollConnectionnull430     fun pageNestedScrollConnection(
431         state: PagerState,
432         orientation: Orientation
433     ): NestedScrollConnection {
434         return remember(state, orientation) {
435             DefaultPagerNestedScrollConnection(state, orientation)
436         }
437     }
438 
439     /**
440      * The default value of beyondViewportPageCount used to specify the number of pages to compose
441      * and layout before and after the visible pages. It does not include the pages automatically
442      * composed and laid out by the pre-fetcher in the direction of the scroll during scroll events.
443      */
444     const val BeyondViewportPageCount = 0
445 }
446 
currentPageOffsetnull447 internal fun SnapPosition.currentPageOffset(
448     layoutSize: Int,
449     pageSize: Int,
450     spaceBetweenPages: Int,
451     beforeContentPadding: Int,
452     afterContentPadding: Int,
453     currentPage: Int,
454     currentPageOffsetFraction: Float,
455     pageCount: Int
456 ): Int {
457     val snapOffset =
458         position(
459             layoutSize,
460             pageSize,
461             beforeContentPadding,
462             afterContentPadding,
463             currentPage,
464             pageCount
465         )
466 
467     return (snapOffset - currentPageOffsetFraction * (pageSize + spaceBetweenPages)).roundToInt()
468 }
469 
470 private class DefaultPagerNestedScrollConnection(
471     val state: PagerState,
472     val orientation: Orientation
473 ) : NestedScrollConnection {
474 
consumeOnOrientationnull475     fun Velocity.consumeOnOrientation(orientation: Orientation): Velocity {
476         return if (orientation == Orientation.Vertical) {
477             copy(x = 0f)
478         } else {
479             copy(y = 0f)
480         }
481     }
482 
onPreScrollnull483     override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
484         return if (
485             // rounding error and drag only
486             source == NestedScrollSource.UserInput && abs(state.currentPageOffsetFraction) > 1e-6
487         ) {
488             // find the current and next page (in the direction of dragging)
489             val currentPageOffset = state.currentPageOffsetFraction * state.pageSize
490             val pageAvailableSpace = state.layoutInfo.pageSize + state.layoutInfo.pageSpacing
491             val nextClosestPageOffset =
492                 currentPageOffset + pageAvailableSpace * -sign(state.currentPageOffsetFraction)
493 
494             val minBound: Float
495             val maxBound: Float
496             // build min and max bounds in absolute coordinates for nested scroll
497             if (state.currentPageOffsetFraction > 0f) {
498                 minBound = nextClosestPageOffset
499                 maxBound = currentPageOffset
500             } else {
501                 minBound = currentPageOffset
502                 maxBound = nextClosestPageOffset
503             }
504 
505             val delta = if (orientation == Orientation.Horizontal) available.x else available.y
506             val coerced = delta.coerceIn(minBound, maxBound)
507             // dispatch and return reversed as usual
508             val consumed = -state.dispatchRawDelta(-coerced)
509             available.copy(
510                 x = if (orientation == Orientation.Horizontal) consumed else available.x,
511                 y = if (orientation == Orientation.Vertical) consumed else available.y,
512             )
513         } else {
514             Offset.Zero
515         }
516     }
517 
onPostScrollnull518     override fun onPostScroll(
519         consumed: Offset,
520         available: Offset,
521         source: NestedScrollSource
522     ): Offset {
523         if (source == NestedScrollSource.SideEffect && available.mainAxis() != 0f) {
524             throw CancellationException("Scroll cancelled")
525         }
526         return Offset.Zero
527     }
528 
onPostFlingnull529     override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
530         return available.consumeOnOrientation(orientation)
531     }
532 
mainAxisnull533     private fun Offset.mainAxis(): Float =
534         if (orientation == Orientation.Horizontal) this.x else this.y
535 }
536 
537 internal fun Modifier.pagerSemantics(
538     state: PagerState,
539     isVertical: Boolean,
540     scope: CoroutineScope,
541     userScrollEnabled: Boolean
542 ): Modifier {
543     fun performForwardPaging(): Boolean {
544         return if (state.canScrollForward) {
545             scope.launch { state.animateToNextPage() }
546             true
547         } else {
548             false
549         }
550     }
551 
552     fun performBackwardPaging(): Boolean {
553         return if (state.canScrollBackward) {
554             scope.launch { state.animateToPreviousPage() }
555             true
556         } else {
557             false
558         }
559     }
560 
561     return if (userScrollEnabled) {
562         this.then(
563             Modifier.semantics {
564                 if (isVertical) {
565                     pageUp { performBackwardPaging() }
566                     pageDown { performForwardPaging() }
567                 } else {
568                     pageLeft { performBackwardPaging() }
569                     pageRight { performForwardPaging() }
570                 }
571             }
572         )
573     } else {
574         this then Modifier
575     }
576 }
577 
debugLognull578 private inline fun debugLog(generateMsg: () -> String) {
579     if (PagerDebugConfig.MainPagerComposable) {
580         println("Pager: ${generateMsg()}")
581     }
582 }
583 
584 internal object PagerDebugConfig {
585     const val MainPagerComposable = false
586     const val PagerState = false
587     const val MeasureLogic = false
588     const val ScrollPosition = false
589     const val PagerSnapDistance = false
590     const val PagerSnapLayoutInfoProvider = false
591 }
592