A snapping fling behavior for Jetpack Compose

Overview

Maven Central Build status

Snapper is a library which brings snapping to the Compose scrolling layouts (currently only LazyColumn and LazyRow).

Check out the website for more information: https://chrisbanes.github.io/snapper

License

Copyright 2021 Chris Banes
 
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

    https://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.
Comments
  • rememberSnapperFlingBehavior for scrollable Row

    rememberSnapperFlingBehavior for scrollable Row

    We have horizontally scrollable container/content. It contains about 1 to 3 (at most) containers. The design requires to have the boxes the same height, so we use intrinsic height sizing (and of course, the width is also the same). Using intrinsic is forbidden in LazyRow, so we are currently using normal scrollable Row.

    image

    It could be probably done via custom SnapperLayoutInfo, yet it seems like enormous and non trivial work/amount of code.

    Do you have any idea how to make it easier?

    Solution I'd like I am not much sure, maybe something like this?

    val state = rememberScrollState()
    val flingBehavior = rememberSnapperFlingBehavior(state, itemWidth = 200.dp)
    Row(
        Modifier
            .horizontalScroll(
                state = state,
                flingBehavior = flingBehavior,
            )
    )
    

    Alternatives I've considered

    • having custom snapping code (what we have now)
    • implementing SnapperLayoutInfo
    enhancement 
    opened by hrach 12
  • Updating to 0.3.0 causes java.lang.NoSuchMethodError

    Updating to 0.3.0 causes java.lang.NoSuchMethodError

    Describe the bug

    The app crashes when trying to call the rememberSnapperFlingBehavior() function

    To Reproduce

    Using rememberSnapperFlingBehavior() instantly crashes my app since i have updated to the 0.3.0 version

    Expected behavior

    No crash occurs

    Environment:

    • Android OS version: android 13
    • Device: Pixel 5
    • Library version: 0.3.0

    Exception Message:

    java.lang.NoSuchMethodError: No static method rememberSnapperFlingBehavior-TN_CM5M(Landroidx/compose/foundation/lazy/LazyListState;Lkotlin/jvm/functions/Function2;FLandroidx/compose/animation/core/DecayAnimationSpec;Landroidx/compose/animation/core/AnimationSpec;Landroidx/compose/runtime/Composer;II)Ldev/chrisbanes/snapper/SnapperFlingBehavior; in class Ldev/chrisbanes/snapper/LazyListKt; or its super classes (declaration of 'dev.chrisbanes.snapper.LazyListKt' appears in ,,,

    opened by joseffilzmaier 8
  • The same index for two last items

    The same index for two last items

    Describe the bug

    Hi, Thanks for library. It is great!

    I've got issues when trying to get current item. LayoutInfo's return the same currentItem.index for two latest items. For example, when I got 4 items, after scroll one by one (horizontally) layoutInfo.currentItem?.index should returns 0, 1, 2, 3 index values, however it returns 0, 1, 2, 2 values.

    Here is simplified implementation to reproduce issue:

    @OptIn(ExperimentalSnapperApi::class)
    @Composable
    fun TestCode(titles: List<String>) {
        val contentPadding = PaddingValues(start = 16.dp,
                                           end = 16.dp)
        val lazyListState = rememberLazyListState()
        val layoutInfo = rememberLazyListSnapperLayoutInfo(lazyListState,
                                                           snapOffsetForItem = SnapOffsets.Center,
                                                           endContentPadding = contentPadding.calculateEndPadding(LayoutDirection.Ltr))
    
        LaunchedEffect(lazyListState.isScrollInProgress) {
            if (!lazyListState.isScrollInProgress) {
                val index = layoutInfo.currentItem?.index
                // index will never be equals to last item index.
                // two last items will returns the same index
            }
        }
    
        LazyRow(
            state = lazyListState,
            contentPadding = contentPadding,
            flingBehavior = rememberSnapperFlingBehavior(layoutInfo),
            horizontalArrangement = Arrangement.spacedBy(24.dp),
            modifier = Modifier.fillMaxWidth()
        ) {
            items(titles) { title ->
                TextField(text = title)
            }
        }
    }
    

    Environment:

    • Android OS 11, 12
    • OnePlus7T PRO, Android Emulator
    • Library version: both 0.1.0 and 0.2.0
    opened by brystel 7
  • LazyList doesn't show the item defined by `initialFirstVisibleItemIndex` in LazyListState

    LazyList doesn't show the item defined by `initialFirstVisibleItemIndex` in LazyListState

    Describe the bug

    LazyList with snapper doesn't scroll to the item defined by initialFirstVisibleItemIndex.

    To Reproduce

    Steps to reproduce the behavior:

    val lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = 3)
    
    val flingBehavior = rememberSnapperFlingBehavior(
        lazyListState = lazyListState,
        endContentPadding = contentPadding.calculateEndPadding(LayoutDirection.Ltr),
    )
    
    LazyRow(
        state = lazyListState,
        flingBehavior = flingBehavior
    ) {
        items(5) {
            // show item here
        }
    }
    

    Expected behavior

    On the initial display (or first launch), the lazylist should auto-scroll to 3rd item. The lazylist shouldn't scroll after display but rather scroll to the correct initial item index before displaying.

    Actual behavior

    On initial display, the lazylist shows 1st item as the 1st visible item.

    To work this issue around, I am using a side affect to scroll to the initial page when the screen is shown

    val lazyListState = rememberLazyListState(initialFirstVisibleItemIndex = 3)
    
    // workaround
    LaunchedEffect(Unit) {
        lazyListState.animateScrollToItem(initialPage)
    }
    
    val flingBehavior = rememberSnapperFlingBehavior(
        lazyListState = lazyListState,
        endContentPadding = contentPadding.calculateEndPadding(LayoutDirection.Ltr),
    )
    
    LazyRow(
        state = lazyListState,
        flingBehavior = flingBehavior
    ) {
        items(5) {
            // show item here
        }
    }
    

    Screenshots?

    If applicable, add screenshots to help explain your problem.

    Environment:

    • Android OS version: 13
    • Device: Google Pixel 4a
    • Library version: 0.2.2

    Additional context

    Add any other context about the problem here.

    Stale 
    opened by iamutkarshtiwari 6
  • Snapper breaks when a lazy list has a large number of items

    Snapper breaks when a lazy list has a large number of items

    Describe the bug

    Trying to snap when item indexes are large numbers, snapper snaps back to the current item

    To Reproduce

    Minimal reproducible sample:

    @OptIn(ExperimentalSnapperApi::class)
    @Composable
    fun SnapperBug() {
        val modifier = Modifier
            .fillMaxWidth()
            .aspectRatio(1f)
        val lazyListState = rememberLazyListState(
            initialFirstVisibleItemIndex = Int.MAX_VALUE / 2
        )
        LazyRow(
            modifier = modifier,
            state = lazyListState,
            flingBehavior = rememberSnapperFlingBehavior(lazyListState)
        ) {
            items(
                count = Int.MAX_VALUE,
                key = { it }
            ) { index ->
                Box(modifier) {
                    Text(
                        index.toString(),
                        modifier = Modifier.align(Alignment.Center)
                    )
                }
            }
        }
    }
    

    Steps to reproduce:

    1. Load the above Composable
    2. Scroll once, note the first scroll is successful
    3. Try to scroll again, note snapper now snaps back to the initial item immediately.

    Expected behavior

    Snapper continues to snap across the entire range of items

    Screenshots?

    I can attach a video if necessary

    Environment:

    • Android OS version: 12L (API32)
    • Device: Resizable Emulator
    • Library version: 0.1.2
    • Compose version: 1.2.0-alpha02 (Also reproducible with Compose 1.1.0-rc01)

    Additional context

    If you significantly lower the initial page in the above sample, snapper starts to work normally

    opened by boswelja 6
  • Add support for compose-jb

    Add support for compose-jb

    Is your feature request related to a problem? Please describe. I want to use this library with pager from accompanist in my Compose for Desktop app.

    Describe the solution you'd like Add support for compose-jb to be able to use this as a multiplatform library, the code looks to not depend on android only code so it should be possible.

    Describe alternatives you've considered If not, I can fork this and make a new version that supports Compose for Desktop.

    Additional context I have migrated all my android app from xml + fragments to compose only to make my app work for desktop as well and it works well, but I need a Pager and I can't use accompanist because it supports Android only.

    enhancement Stale 
    opened by dragossusi 6
  • add ability to snap to n items

    add ability to snap to n items

    Hi @chrisbanes . I need a feature to snap n items, not only one. I'd appreciate if you'll look on my changes to support this case or at least to say if snapper is going to support this case.

    opened by ahamula 5
  • Using unsafe nullable type ScrollScope.performFling function

    Using unsafe nullable type ScrollScope.performFling function

    Describe the bug

    Using unsafe nullable type in the ScrollScope.performFling function, which causes crash

      override suspend fun ScrollScope.performFling(
            initialVelocity: Float
        ): Float {
            // If we're at the start/end of the scroll range, we don't snap and assume the user
            // wanted to scroll here.
            if (!layoutInfo.canScrollTowardsStart() || !layoutInfo.canScrollTowardsEnd()) {
                return initialVelocity
            }
    
            SnapperLog.d { "performFling. initialVelocity: $initialVelocity" }
    
            val maxFlingDistance = maximumFlingDistance(layoutInfo)
            require(maxFlingDistance > 0) {
                "Distance returned by maximumFlingDistance should be greater than 0"
            }
    
            val targetIndex = layoutInfo.determineTargetIndex(
                velocity = initialVelocity,
                decayAnimationSpec = decayAnimationSpec,
                maximumFlingDistance = maxFlingDistance,
            ).let { target ->
                // Let the snapIndex block transform the value
                val start = layoutInfo.**currentItem!!**.index.let { index ->
                    // If the user is flinging towards the index 0, we assume that the start item is
                    // actually the next item (towards infinity).
                    if (initialVelocity < 0) index + 1 else index
                }
                snapIndex(layoutInfo, start, target)
            }.also {
                require(it in 0 until layoutInfo.totalItemsCount)
            }
    
            return flingToIndex(index = targetIndex, initialVelocity = initialVelocity)
        }
    

    Additional context

    This unsafe nullable type is used in two other places as well ->

    • SemanticsNodeInteraction.swipeAcrossCenterWithVelocity func
    • ScrollScope.flingToIndex func
    opened by sepidevatankhah 4
  • Allow configuring the minimum velocity to scroll past an item

    Allow configuring the minimum velocity to scroll past an item

    The documentation states:

    decayAnimationSpec is the main spec used for flinging, and is used when the fling has enough velocity to scroll past the current item.

    However, how do you actually configure what is 'enough' velocity? I've noticed that the default velocity doesn't fit my needs very well. Arpit Shukla described this very well 10 months ago:

    Sometimes I gently swipe to the left or right and expect that I will move to the next page, but that doesn't happen, the page moves a little and then comes back. Then I realize that I need to put more effort and swipe a little more :sweat_smile:. I was thinking if it is possible to change that threshold velocity which is required to switch pages. I checked few other apps which provide a pager with a tab layout and there I can change pages with the slightest swipe.

    Attaching a video to give more context to the behavior I was referring to.

    https://user-images.githubusercontent.com/25990014/192085480-0d8cf1a2-4adb-4ad0-b04b-64f11eb8ee98.mp4

    Perhaps it is configurable, but I cannot find how.

    enhancement Stale 
    opened by natanfudge 3
  • [Idea] Support non-lazy layouts

    [Idea] Support non-lazy layouts

    Is your feature request related to a problem? Please describe. Sometimes due to a design request (like having all items in a carousel be the same height (using intrinsics) I cannot use lazy*, but I would love to have snapping behavior.

    enhancement 
    opened by ColtonIdle 3
  • Add `snapIndex` lambda parameter

    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.

    A very simple version could look this this:

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

    You could also look at the layout.currentItem to determine the fling direction to influence which direction to round.

    opened by chrisbanes 3
  • Selecting an item programmatically

    Selecting an item programmatically

    How do I scroll to an item programmatically?

    Also, how do I scroll "past" the beginning or end so that the first or last items of the list can also be in the center and be "selected"

    I am using this for a selectable list of items. Items can be selected either by:

    • tapping on them (triggering a scroll to put the selected item in the center)
    • scrolling until the item is in the center
    • Programmatically by updating the selected index from another function

    Would this be possible?

    opened by slickorange 0
  • [WIP] Try out compiling with Compose Multiplatform

    [WIP] Try out compiling with Compose Multiplatform

    It compiles but doesn't work on desktop due to https://github.com/JetBrains/compose-jb/issues/1423. Will need to wait for that to be fixed before continuing.

    #5

    opened by chrisbanes 0
Releases(v0.3.0)
  • v0.3.0(Aug 23, 2022)

    What's Changed

    • Update to Compose 1.2.0 by @chrisbanes in https://github.com/chrisbanes/snapper/pull/31
    • Modernize CI by @chrisbanes in https://github.com/chrisbanes/snapper/pull/33

    Full Changelog: https://github.com/chrisbanes/snapper/compare/v0.2.2...v0.3.0

    Source code(tar.gz)
    Source code(zip)
  • v0.2.2(Jun 16, 2022)

    What's Changed

    • Fix target overscroll on page skips by @chippmann in https://github.com/chrisbanes/snapper/pull/28
    • Back currentItem with a derived state by @chrisbanes in https://github.com/chrisbanes/snapper/pull/29
    • Compile against SDK 33 by @chrisbanes in https://github.com/chrisbanes/snapper/pull/30

    New Contributors

    • @chippmann made their first contribution in https://github.com/chrisbanes/snapper/pull/28

    Full Changelog: https://github.com/chrisbanes/snapper/compare/v0.2.1...v0.2.2

    Source code(tar.gz)
    Source code(zip)
  • v0.2.1(May 4, 2022)

    What's Changed

    • Remove unsafe nullable refs by @chrisbanes in https://github.com/chrisbanes/snapper/pull/23
    • Bump up to Compose version 1.1.1 by @chrisbanes in https://github.com/chrisbanes/snapper/pull/25

    Full Changelog: https://github.com/chrisbanes/snapper/compare/v0.2.0...v0.2.1

    Source code(tar.gz)
    Source code(zip)
  • v0.2.0(Feb 12, 2022)

    What's Changed

    • Enable Kotlin's Explicit API mode by @chrisbanes in https://github.com/chrisbanes/snapper/pull/11
    • Fix float inaccuracies at high values by @chrisbanes in https://github.com/chrisbanes/snapper/pull/14
    • Update dependencies by @chrisbanes in https://github.com/chrisbanes/snapper/pull/16
    • Update to Compose 1.1 and Kotlin 1.6.10 by @chrisbanes in https://github.com/chrisbanes/snapper/pull/17
    • Add snapIndex lambda parameter by @chrisbanes in https://github.com/chrisbanes/snapper/pull/15

    Full Changelog: https://github.com/chrisbanes/snapper/compare/v0.1.2...v0.2.0

    Source code(tar.gz)
    Source code(zip)
  • v0.1.2(Jan 27, 2022)

    What's Changed

    • Tidy up target index calculation by @chrisbanes in https://github.com/chrisbanes/snapper/pull/10

    Full Changelog: https://github.com/chrisbanes/snapper/compare/v0.1.1...v0.1.2

    Source code(tar.gz)
    Source code(zip)
  • v0.1.1(Jan 12, 2022)

    What's Changed

    • Tidy up logging by @chrisbanes in https://github.com/chrisbanes/snapper/pull/9

    Full Changelog: https://github.com/chrisbanes/snapper/compare/v0.1.0...v0.1.1

    Source code(tar.gz)
    Source code(zip)
  • v0.1.0(Oct 18, 2021)

    First release!

    What's Changed

    • Generalize the layout interface for Snapper by @chrisbanes in https://github.com/chrisbanes/snapper/pull/3
    • Upgrade the docs! by @chrisbanes in https://github.com/chrisbanes/snapper/pull/4

    Full Changelog: https://github.com/chrisbanes/snapper/commits/v0.1.0

    Source code(tar.gz)
    Source code(zip)
Zoom Modifiers, zoomable image and layouts with limit pan bounds, fling and moving back to valid bounds and callbacks that return current transformation or visible image section

Zoom Modifiers, zoomable image and layouts with limit pan bounds, fling and moving back to valid bounds and callbacks that return current transformation or visible image section

Smart Tool Factory 20 Dec 13, 2022
Jetpack Compose Boids | Flocking Insect 🐜. bird or Fish simulation using Jetpack Compose Desktop 🚀, using Canvas API 🎨

?? ?? ?? Compose flocking Ants(boids) ?? ?? ?? Jetpack compose Boids | Flocking Insect. bird or Fish simulation using Jetpack Compose Desktop ?? , usi

Chetan Gupta 38 Sep 25, 2022
A collection of animations, compositions, UIs using Jetpack Compose. You can say Jetpack Compose cookbook or play-ground if you want!

Why Not Compose! A collection of animations, compositions, UIs using Jetpack Compose. You can say Jetpack Compose cookbook or play-ground if you want!

Md. Mahmudul Hasan Shohag 186 Jan 1, 2023
Learn Jetpack Compose for Android by Examples. Learn how to use Jetpack Compose for Android App Development. Android’s modern toolkit for building native UI.

Learn Jetpack Compose for Android by Examples. Learn how to use Jetpack Compose for Android App Development. Android’s modern toolkit for building native UI.

MindOrks 382 Jan 5, 2023
This is a sample app(For beginners - App #2) built using Jetpack Compose. It demonstrates the concept of State Hoisting in Jetpack Compose.

JetBMICalculator This is a sample app(For beginners - App #2) built using Jetpack Compose. It demonstrates the concept of State Hoisting in Jetpack Co

BHAVNA THACKER 3 Dec 31, 2022
Jetpack-Compose-Demo - Instagram Profile UI using Jetpack Compose

Jetpack-Compose-Demo Instagram Profile UI using Jetpack Compose

omar 1 Aug 11, 2022
Jetpack-compose-animations-examples - Cool animations implemented with Jetpack compose

Jetpack-compose-animations-examples This repository consists of 4 animations: St

Canopas Software 180 Jan 2, 2023
Compose-navigation - Set of utils to help with integrating Jetpack Compose and Jetpack's Navigation

Jetpack Compose Navigation Set of utils to help with integrating Jetpack Compose

Adam Kobus 5 Apr 5, 2022
Jetpack-compose-uis - A collection of some UIs using Jetpack Compose. built using Katalog

Jetpack Compose UIs This is a collection of some UIs using Jetpack Compose. It i

Mori Atsushi 3 Dec 15, 2022
A simple authentication application using Jetpack compose to illustrate signin and sign up using Mvvm, Kotlin and jetpack compose

Authentication A simple authentication application using Jetpack compose to illustrate signin and sign up using Mvvm, Kotlin and jetpack compose Scree

Felix Kariuki 5 Dec 29, 2022
An application that i developed with a aim of learning Jetpack compose and many other jetpack libraries

An application that i developed with a aim of learning Jetpack compose and many other jetpack libraries, The application make use of jikan Api which displays a list of animations,there more details and even trailers of the animations.

Odhiambo Brandy 10 Nov 23, 2022
A Kotlin library to use Jetpack Compose in Android and iOS. Allow to write UI for both in Kotin. Still experimental as many compose features are not yet available.

Multiplatform Compose A Kotlin library to use Jetpack Compose in Android and iOS. Allow to write UI for both in Kotin. Still experimental as many comp

Clément Beffa 548 Jan 7, 2023
K5-compose is a sketchy port of p5.js for Jetpack Compose

k5-compose k5-compose is a sketchy port of P5.js for Jetpack Compose Desktop. This library provides you a playground to play with your sketches so you

Nikhil Chaudhari 176 Nov 22, 2022
Jetpack Compose based project, used to stress-testing compose features / integrations and explore non-trivial functionality

Project containing Jetpack Compose samples For pagination & network images it uses CATAAS. Known issues Navigation-Compose Issue with fast tapping on

Denis Rudenko 59 Dec 14, 2022
Pokedex Compose is an independent re-write of a demo application by the name of Pokedex, but written in jetpack compose.

Pokedex Compose Pokedex Compose is an independent re-write of a similar project by the name of Pokedex. I am recreating the UI but I am doing it using

Jose Patino 4 May 1, 2022
Compose-Instagram-Profile-UI - Instagram profile screen UI using android jetpack compose

Compose-Intsgram-Profile-UI Instagram profile screen UI using android jetpack co

TILLERN 1 Mar 8, 2022
Luis David Orellana 3 Jun 20, 2022
Lock Screen-Compose - Lock Screen with Jetpack Compose

Lock_Screen-Compose 此專案為Jetpack Compose練習題.

Kanneki Chen 0 Jan 26, 2022
Compose-Ratingbar-library - A simple implementation for rating bar in Jetpack Compose

Compose-Ratingbar-library - A simple implementation for rating bar in Jetpack Compose

Mahmoud Hussein 14 Dec 21, 2022