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