List view snap to item
Asked Answered
H

7

19

I'm creating a list of pictures using a ListView and the photos are of a size that would fit 2 to 3 photos on the screen.

The problem that I'm having is that I would like to when the user stops scrolling that the first item of the visible list would snap to the top of screen, for example, if the scroll ends and small part of the first picture displayed, we scroll the list down so the picture is always fully displayed, if mostly of the picture is displayed, we scroll the list up so the next picture is fully visible.

Is there a way to achieve this in android with the listview?

Halvorson answered 13/12, 2010 at 18:36 Comment(0)
H
26

I've found a way to do this just listening to scroll and change the position when the scroll ended by implementing ListView.OnScrollListener

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
    switch (scrollState) {
    case OnScrollListener.SCROLL_STATE_IDLE:
        if (scrolling){
            // get first visible item
            View itemView = view.getChildAt(0);
            int top = Math.abs(itemView.getTop()); // top is a negative value
            int bottom = Math.abs(itemView.getBottom());
            if (top >= bottom){
                ((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition()+1, 0);
            } else {
                ((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition(), 0);
            }
        }
        scrolling = false;
        break;
    case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
    case OnScrollListener.SCROLL_STATE_FLING:
        Log.i("TEST", "SCROLLING");
        scrolling = true;
        break;
    }
}

The change is not so smooth but it works.

Halvorson answered 17/12, 2010 at 11:42 Comment(4)
If you want it to be smooth try using ((ListView)view).smoothScrollToPosition(pos);Bibliofilm
Two problems with doing that: 1) smoothScrollToPosition will cause the above logic to detect another scroll and go into a jittery loop -- you can avoid this by removing the case for SCROLL_STATE_FLING but I'm not sure if that's a good fix. 2) smoothScrollToPosition doesn't align things correctly, at least with my lists. It scrolls the specified item's top to maybe 15 pixels above the top of the listview, instead of aligning things correctly like setSelection/setSelectionFromTop do.Electrothermal
Should be 'ScrollState.SCROLL_STATE_IDLE' etc, not 'OnScrollListener.SCROLL_STATE_IDLE'Engobe
thanks for the snippet. Is there an issue with alignment of the last item in the list. So when the last item of the list is visible we should align to it, not the first visible item.Intimidate
A
5

Utilizing a couple ideas from @nininho's solution, I got my listview to snap to the item with a smooth scroll instead of abruptly going to it. One caveat is that I've only tested this solution on a Moto X in a basic ListView with text, but it works very well on the device. Nevertheless, I'm confident about this solution, and encourage you to provide feedback.

listview.setOnScrollListener(new OnScrollListener() {
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            // TODO Auto-generated method stub
            if (scrollState == SCROLL_STATE_IDLE) {
                View itemView = view.getChildAt(0);
                int top = Math.abs(itemView.getTop());
                int bottom = Math.abs(itemView.getBottom());
                int scrollBy = top >= bottom ? bottom : -top;
                if (scrollBy == 0) {
                    return;
                }
                smoothScrollDeferred(scrollBy, (ListView)view);
            }
        }

        private void smoothScrollDeferred(final int scrollByF,
                final ListView viewF) {
            final Handler h = new Handler();
            h.post(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    viewF.smoothScrollBy(scrollByF, 200);
                }
            });
        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
                int visibleItemCount, int totalItemCount) {
            // TODO Auto-generated method stub

        }
    });

The reason I defer the smooth scrolling is because in my testing, directly calling the smoothScrollBy method in the state changed callback had problems actually scrolling. Also, I don't foresee a fully-tested, robust solution holding very much state, and in my solution below, I hold no state at all. This solution is not yet in the Google Play Store, but should serve as a good starting point.

Accipitrine answered 7/12, 2014 at 0:13 Comment(1)
When top >= bottom, scrollBy equals (N: cols in GridView, N=1 for ListView) view.getChildAt(view.getFirstVisiblePosition()+N).getY().Connatural
L
2

Using @nininho 's solution,

In the onScrollStateChanged when the state changes to SCROLL_STATE_IDLE, remember the position to snap and raise a flag:

snapTo = view.getFirstVisiblePosition();
shouldSnap = true;

Then, override the computeScroll() method:

@Override
public void computeScroll() {
    super.computeScroll();
    if(shouldSnap){
        this.smoothScrollToPositionFromTop(snapTo, 0);
        shouldSnap = false;
    }
}
Lucilucia answered 28/2, 2014 at 6:21 Comment(0)
E
1

You can do a much more smooth scrolling if you use RecyclerView. The OnScrollListener is way better.

I have made an example here: https://github.com/plattysoft/SnappingList

Embolectomy answered 17/6, 2015 at 10:14 Comment(2)
You don't have any comments in you code? Can you point out where in the sample your snap occurs?Osset
There is a link on the readme to a blog post that explains all the involved parts in detail, it's not just one line of code.Embolectomy
F
1

Well.. I know 10 years have past since this question was asked, but now we can use LinearSnapHelper:

new LinearSnapHelper().attachToRecyclerView(recyclerView);

Source: https://proandroiddev.com/android-recyclerview-snaphelper-19eaa9598da6

Faizabad answered 4/12, 2020 at 10:41 Comment(1)
By far, I think this is the most official solution. Thank youSight
S
0

Apart from trying the code above one thing you should make sure of is that your listView have a height that can fit exact number of items you want to be displayed. e.g If you want 4 items to be displayed after snap effect and your row height (defined in its layout) should be 1/4 of the total height of the list.

Selfconfidence answered 3/12, 2012 at 6:46 Comment(0)
A
0

Note that after the smoothScrollBy() call, getFirstVisiblePosition() may point to the list item ABOVE the topmost one in the listview. This is especially true when view.getChildAt(0).getBottom() == 0. I had to call view.setSelection(view.getFirstVisiblePosition() + 1) to remedy this odd behavior.

Agnomen answered 17/12, 2015 at 15:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.