Skip to content
This repository has been archived by the owner on Nov 10, 2022. It is now read-only.

Commit

Permalink
Add snapIndex lambda parameter (#15)
Browse files Browse the repository at this point in the history
* Add `snapIndex` lambda parameter

This block which returns the index which the apps wishes to snap to.
The block is provided with the SnapperLayoutInfo and the index which Snapper
has determined is the correct target index. Callers can override this value as
they see fit.

A common use case could be rounding up/down to achieve groupings of items, which was
the intention of #12

* Add test for snapIndex

* Improve kdoc

* Expand snapIndex test

* Add snapIndex scenario with opposite directions

* Make snapIndex non-nullable

* Split out maximumFlingDistance to a deprecated overload

Had to remove the default value to avoid overload ambiguity.

* Re-add default value for maximumFlingDistance

This maintains binary compatibility.

* Remove extra comment

* Add `startIndex` to SnapIndex

Also added some docs for the new parameter
  • Loading branch information
chrisbanes authored Feb 11, 2022
1 parent 6bfa7ea commit 4d27add
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 103 deletions.
53 changes: 42 additions & 11 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,34 +102,65 @@ LazyColumn(
}
```

## Customization of the target index

## Controlling the maximum fling distance
The `snapIndex` parameter allows customization of the index which Snapper which fling to
after a user has started a fling.

The `maximumFlingDistance` parameter allows customization of the maximum distance that a user can
fling (and snap to).
The block is given the [SnapperLayoutInfo][snapperlayoutinfo], the index where the fling started, and
with the index which Snapper has determined is the correct index to fling, without the layout limits.
The block should return the index which Snapper should fling and snap to.

Apps can provide a block which will be called once a fling has been started. The block is given
the [SnapperLayoutInfo][snapperlayoutinfo], if it needs to use the layout to determine
the distance.
The following are some examples of what you can achieve with `snapIndex`.

The following example sets the maximum fling distance to 3x the container width.
### Controlling the maximum fling distance

The following example sets the `snapIndex` so that the user can only fling up a maximum of 3 items:

``` kotlin
val MaxItemFling = 3

LazyRow(
state = lazyListState,
flingBehavior = rememberSnapperFlingBehavior(
lazyListState = lazyListState,
maximumFlingDistance = { layoutInfo ->
val scrollLength = layoutInfo.endScrollOffset - layoutInfo.startScrollOffset
// Allow the user to scroll 3x the LazyRow 'width'
scrollLength * 3f
snapIndex = { layoutInfo, startIndex, targetIndex ->
targetIndex.coerceIn(startIndex - MaxItemFling, startIndex + MaxItemFling)
}
),
) {
// content
}
```

### Snapping groups

The `snapIndex` parameter can also be used to achieve snapping to 'groups' of items.

The following example provide a `snapIndex` block which snaps flings to groups of 3 items:

``` kotlin
val GroupSize = 3

LazyRow(
state = lazyListState,
flingBehavior = rememberSnapperFlingBehavior(
lazyListState = lazyListState,
snapIndex = { _, _, targetIndex ->
val mod = targetIndex % GroupSize
if (mod > (GroupSize / 2)) {
// Round up towards infinity
GroupSize + targetIndex - mod
} else {
// Round down towards zero
targetIndex - mod
}
),
) {
// content
}
```

## Animation specs

SnapperFlingBehavior allows setting of two different animation specs: `decayAnimationSpec` and `springAnimationSpec`.
Expand Down
21 changes: 16 additions & 5 deletions lib/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package dev.chrisbanes.snapper {

public final class LazyListKt {
method @androidx.compose.runtime.Composable @dev.chrisbanes.snapper.ExperimentalSnapperApi public static dev.chrisbanes.snapper.LazyListSnapperLayoutInfo rememberLazyListSnapperLayoutInfo(androidx.compose.foundation.lazy.LazyListState lazyListState, optional kotlin.jvm.functions.Function2<? super dev.chrisbanes.snapper.SnapperLayoutInfo,? super dev.chrisbanes.snapper.SnapperLayoutItemInfo,java.lang.Integer> snapOffsetForItem, optional float endContentPadding);
method @androidx.compose.runtime.Composable @dev.chrisbanes.snapper.ExperimentalSnapperApi public static dev.chrisbanes.snapper.SnapperFlingBehavior rememberSnapperFlingBehavior(androidx.compose.foundation.lazy.LazyListState lazyListState, optional kotlin.jvm.functions.Function2<? super dev.chrisbanes.snapper.SnapperLayoutInfo,? super dev.chrisbanes.snapper.SnapperLayoutItemInfo,java.lang.Integer> snapOffsetForItem, optional float endContentPadding, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec, optional kotlin.jvm.functions.Function1<? super dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> maximumFlingDistance);
method @androidx.compose.runtime.Composable @dev.chrisbanes.snapper.ExperimentalSnapperApi public static dev.chrisbanes.snapper.SnapperFlingBehavior rememberSnapperFlingBehavior(androidx.compose.foundation.lazy.LazyListState lazyListState, optional kotlin.jvm.functions.Function2<? super dev.chrisbanes.snapper.SnapperLayoutInfo,? super dev.chrisbanes.snapper.SnapperLayoutItemInfo,java.lang.Integer> snapOffsetForItem, optional float endContentPadding, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec, kotlin.jvm.functions.Function3<? super dev.chrisbanes.snapper.SnapperLayoutInfo,? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> snapIndex);
method @androidx.compose.runtime.Composable @dev.chrisbanes.snapper.ExperimentalSnapperApi public static dev.chrisbanes.snapper.SnapperFlingBehavior rememberSnapperFlingBehavior(androidx.compose.foundation.lazy.LazyListState lazyListState, optional kotlin.jvm.functions.Function2<? super dev.chrisbanes.snapper.SnapperLayoutInfo,? super dev.chrisbanes.snapper.SnapperLayoutItemInfo,java.lang.Integer> snapOffsetForItem, optional float endContentPadding, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec);
method @Deprecated @androidx.compose.runtime.Composable @dev.chrisbanes.snapper.ExperimentalSnapperApi public static dev.chrisbanes.snapper.SnapperFlingBehavior rememberSnapperFlingBehavior(androidx.compose.foundation.lazy.LazyListState lazyListState, optional kotlin.jvm.functions.Function2<? super dev.chrisbanes.snapper.SnapperLayoutInfo,? super dev.chrisbanes.snapper.SnapperLayoutItemInfo,java.lang.Integer> snapOffsetForItem, optional float endContentPadding, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec, optional kotlin.jvm.functions.Function1<? super dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> maximumFlingDistance);
}

@dev.chrisbanes.snapper.ExperimentalSnapperApi public final class LazyListSnapperLayoutInfo extends dev.chrisbanes.snapper.SnapperLayoutInfo {
Expand All @@ -18,10 +20,12 @@ package dev.chrisbanes.snapper {
method public dev.chrisbanes.snapper.SnapperLayoutItemInfo? getCurrentItem();
method public int getEndScrollOffset();
method public int getStartScrollOffset();
method public int getTotalItemsCount();
method public kotlin.sequences.Sequence<dev.chrisbanes.snapper.SnapperLayoutItemInfo> getVisibleItems();
property public dev.chrisbanes.snapper.SnapperLayoutItemInfo? currentItem;
property public int endScrollOffset;
property public int startScrollOffset;
property public int totalItemsCount;
property public kotlin.sequences.Sequence<dev.chrisbanes.snapper.SnapperLayoutItemInfo> visibleItems;
}

Expand All @@ -36,22 +40,27 @@ package dev.chrisbanes.snapper {
}

@dev.chrisbanes.snapper.ExperimentalSnapperApi public final class SnapperFlingBehavior implements androidx.compose.foundation.gestures.FlingBehavior {
ctor public SnapperFlingBehavior(dev.chrisbanes.snapper.SnapperLayoutInfo layoutInfo, optional kotlin.jvm.functions.Function1<? super dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> maximumFlingDistance, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec);
ctor public SnapperFlingBehavior(dev.chrisbanes.snapper.SnapperLayoutInfo layoutInfo, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec, optional kotlin.jvm.functions.Function3<? super dev.chrisbanes.snapper.SnapperLayoutInfo,? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> snapIndex);
ctor @Deprecated public SnapperFlingBehavior(dev.chrisbanes.snapper.SnapperLayoutInfo layoutInfo, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec, optional kotlin.jvm.functions.Function1<? super dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> maximumFlingDistance);
method public Integer? getAnimationTarget();
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float> p);
property public final Integer? animationTarget;
}

@dev.chrisbanes.snapper.ExperimentalSnapperApi public final class SnapperFlingBehaviorDefaults {
method public kotlin.jvm.functions.Function1<dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> getMaximumFlingDistance();
method @Deprecated public kotlin.jvm.functions.Function1<dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> getMaximumFlingDistance();
method public kotlin.jvm.functions.Function3<dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Integer,java.lang.Integer,java.lang.Integer> getSnapIndex();
method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getSpringAnimationSpec();
property public final kotlin.jvm.functions.Function1<dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> MaximumFlingDistance;
property @Deprecated public final kotlin.jvm.functions.Function1<dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> MaximumFlingDistance;
property public final kotlin.jvm.functions.Function3<dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Integer,java.lang.Integer,java.lang.Integer> SnapIndex;
property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> SpringAnimationSpec;
field public static final dev.chrisbanes.snapper.SnapperFlingBehaviorDefaults INSTANCE;
}

public final class SnapperFlingBehaviorKt {
method @androidx.compose.runtime.Composable @dev.chrisbanes.snapper.ExperimentalSnapperApi public static dev.chrisbanes.snapper.SnapperFlingBehavior rememberSnapperFlingBehavior(dev.chrisbanes.snapper.SnapperLayoutInfo layoutInfo, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec, optional kotlin.jvm.functions.Function1<? super dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> maximumFlingDistance);
method @androidx.compose.runtime.Composable @dev.chrisbanes.snapper.ExperimentalSnapperApi public static dev.chrisbanes.snapper.SnapperFlingBehavior rememberSnapperFlingBehavior(dev.chrisbanes.snapper.SnapperLayoutInfo layoutInfo, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec, kotlin.jvm.functions.Function3<? super dev.chrisbanes.snapper.SnapperLayoutInfo,? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> snapIndex);
method @androidx.compose.runtime.Composable @dev.chrisbanes.snapper.ExperimentalSnapperApi public static inline dev.chrisbanes.snapper.SnapperFlingBehavior rememberSnapperFlingBehavior(dev.chrisbanes.snapper.SnapperLayoutInfo layoutInfo, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec);
method @Deprecated @androidx.compose.runtime.Composable @dev.chrisbanes.snapper.ExperimentalSnapperApi public static dev.chrisbanes.snapper.SnapperFlingBehavior rememberSnapperFlingBehavior(dev.chrisbanes.snapper.SnapperLayoutInfo layoutInfo, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> springAnimationSpec, optional kotlin.jvm.functions.Function1<? super dev.chrisbanes.snapper.SnapperLayoutInfo,java.lang.Float> maximumFlingDistance);
}

@dev.chrisbanes.snapper.ExperimentalSnapperApi public abstract class SnapperLayoutInfo {
Expand All @@ -63,10 +72,12 @@ package dev.chrisbanes.snapper {
method public abstract dev.chrisbanes.snapper.SnapperLayoutItemInfo? getCurrentItem();
method public abstract int getEndScrollOffset();
method public abstract int getStartScrollOffset();
method public abstract int getTotalItemsCount();
method public abstract kotlin.sequences.Sequence<dev.chrisbanes.snapper.SnapperLayoutItemInfo> getVisibleItems();
property public abstract dev.chrisbanes.snapper.SnapperLayoutItemInfo? currentItem;
property public abstract int endScrollOffset;
property public abstract int startScrollOffset;
property public abstract int totalItemsCount;
property public abstract kotlin.sequences.Sequence<dev.chrisbanes.snapper.SnapperLayoutItemInfo> visibleItems;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ import org.junit.runners.Parameterized
*/
@RunWith(Parameterized::class)
class InstrumentedSnapperFlingLazyColumnTest(
maxScrollDistanceDp: Float,
snapIndexDelta: Int,
contentPadding: PaddingValues,
itemSpacingDp: Int,
reverseLayout: Boolean,
) : BaseSnapperFlingLazyColumnTest(
maxScrollDistanceDp,
snapIndexDelta,
contentPadding,
itemSpacingDp,
reverseLayout,
Expand All @@ -44,23 +44,14 @@ class InstrumentedSnapperFlingLazyColumnTest(
*/
@JvmStatic
@Parameterized.Parameters(
name = "maxScrollDistanceDp={0}," +
name = "snapIndexDelta={0}," +
"contentPadding={1}," +
"itemSpacing={2}," +
"reverseLayout={3}"
)
fun data() = parameterizedParams()
// maxScrollDistanceDp
.combineWithParameters(
// We add 4dp on to cater for item spacing
1 * (ItemSize.value + 4),
4 * (ItemSize.value + 4),
// We also test without adding the item spacing. Key use cases like
// Accompanist Pager do not add the item spacing so we need to ensure things
// work as expected without it.
1 * ItemSize.value,
4 * ItemSize.value,
)
// snapIndexDelta
.combineWithParameters(1, 4)
// contentPadding
.combineWithParameters(
PaddingValues(bottom = 32.dp), // Alignment.Top
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ import org.junit.runners.Parameterized
*/
@RunWith(Parameterized::class)
class InstrumentedSnapperFlingLazyRowTest(
maxScrollDistanceDp: Float,
snapIndexDelta: Int,
contentPadding: PaddingValues,
itemSpacingDp: Int,
layoutDirection: LayoutDirection,
reverseLayout: Boolean,
) : BaseSnapperFlingLazyRowTest(
maxScrollDistanceDp,
snapIndexDelta,
contentPadding,
itemSpacingDp,
layoutDirection,
Expand All @@ -44,24 +44,15 @@ class InstrumentedSnapperFlingLazyRowTest(
companion object {
@JvmStatic
@Parameterized.Parameters(
name = "maxScrollDistanceDp={0}," +
name = "snapIndexDelta={0}," +
"contentPadding={1}," +
"itemSpacing={2}," +
"layoutDirection={3}," +
"reverseLayout={4}"
)
fun data() = parameterizedParams()
// maxScrollDistanceDp
.combineWithParameters(
// We add 4dp on to cater for item spacing
1 * (ItemSize.value + 4),
4 * (ItemSize.value + 4),
// We also test without adding the item spacing. Key use cases like
// Accompanist Pager do not add the item spacing so we need to ensure things
// work as expected without it.
1 * ItemSize.value,
4 * ItemSize.value,
)
// snapIndexDelta
.combineWithParameters(1, 4)
// contentPadding
.combineWithParameters(
PaddingValues(end = 32.dp), // Alignment.Start
Expand Down
Loading

0 comments on commit 4d27add

Please sign in to comment.