ListView in BottomSheet
Asked Answered
H

9

25

I ran into a problem where I had a simple ListView in a BottomSheet and ListView had enough items to fill the screen and scroll even more.

When I scroll down, everything seems to work however when I tried to scroll back up, it was scrolling the BottomSheet itself and closing the view instead of just scrolling the ListView.

I was able to find a solution after a while and since I couldn't find it anywhere here, I figured I would post it here.

Hendecahedron answered 13/11, 2016 at 5:47 Comment(0)
H
31

The solution is to extend the ListView like this:

public class BottomSheetListView extends ListView {
    public BottomSheetListView (Context context, AttributeSet p_attrs) {
        super (context, p_attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (canScrollVertically(this)) {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
        return super.onTouchEvent(ev);
    }

    public boolean canScrollVertically (AbsListView view) {
        boolean canScroll = false;

        if (view !=null && view.getChildCount ()> 0) {
            boolean isOnTop = view.getFirstVisiblePosition() != 0 || view.getChildAt(0).getTop() != 0;
            boolean isAllItemsVisible = isOnTop && view.getLastVisiblePosition() == view.getChildCount();

            if (isOnTop || isAllItemsVisible) {
                canScroll = true;
            }
        }

        return  canScroll;
    }
}

Then in your layout file bottom_sheet_view.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.mypackage.name.BottomSheetListView
        android:id="@+id/listViewBtmSheet"
        android:divider="@color/colorPrimary"
        android:dividerHeight="1dp"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="0dp" />

</LinearLayout>

Then finally, in your Activity/Fragment:

BottomSheetDialog dialog = new BottomSheetDialog(context);
dialog.setContentView(R.layout.bottom_sheet_view);

BottomSheetListView listView = (BottomSheetListView) dialog.findViewById(R.id.listViewBtmSheet);
// apply some adapter - add some data to listview

dialog.show();

This will provide a BottomSheet that is fully working with ListView scroll.

Hendecahedron answered 13/11, 2016 at 5:47 Comment(9)
hey buddy, nice solution, but the list items are not clickable, any hack for it?Malapropos
@Malapropos items are clickable for me. Do you have ImageView or Button inside the list item layout? If so, you need add android:descendantFocusability="blocksDescendants" to the root element of the list item layoutPindaric
I did it without writing android:descendantFocusability="blocksDescendants". But it needs little more improvement. I posted the code on commentMalapropos
list items are not clickable for me also. any solution?Woodruff
@th3pat3l Hey!! Anyone find a solution? Even I am not able to click listview item. I have an ImageView inside listview. Please help if possible. Thanks.Exotoxin
Helpful! Any solution to achieve this without extending listview?Zambia
Great, thanks! Test with ExpandableListView and working. @PratikSaluja If you don't want to extend a class you could set an OnTouchListener in the List View and works properly too. By the way here: Manage touch events in a ViewGroup explains that returning false in onInterceptTouchEvent let the child views handle the events. May be could be the solution for items not clickable issueAlexandrina
hi, great answer. Can you please guide me as to how to do the same for recyclerview. Your early response will be very much appreciatedSumptuary
Great! Tks verry muchDibru
C
17

There is a better approach if you don't want to extend the ListView:

//in onCreate

_listView.setOnTouchListener(new ListView.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        // Disallow NestedScrollView to intercept touch events.
                        v.getParent().requestDisallowInterceptTouchEvent(true);
                        break;

                    case MotionEvent.ACTION_UP:
                        // Allow NestedScrollView to intercept touch events.
                        v.getParent().requestDisallowInterceptTouchEvent(false);
                        break;
                }

                // Handle ListView touch events.
                v.onTouchEvent(event);
                return true;
            }
        });
Chancey answered 9/9, 2017 at 9:1 Comment(0)
G
11

Starting from version 22.1.0, you might want to try setNestedScrollingEnabled=true

If this property is set to true the view will be permitted to initiate nested scrolling operations with a compatible parent view in the current hierarchy. If this view does not implement nested scrolling this will have no effect. Disabling nested scrolling while a nested scroll is in progress has the effect of stopping the nested scroll.

Reference to Google API

Gerius answered 24/9, 2019 at 11:33 Comment(2)
It should be the accepted answer!Actress
This is the correct answer for recent library.Feaze
R
8

This is the correct list view custom class with touch event hadnling

public class BottomSheetListView extends ListView
{
    public BottomSheetListView(Context context, AttributeSet p_attrs)
    {
        super(context, p_attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev)
    {
        if (canScrollVertically(this))
        {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev)
    {
        if (canScrollVertically(this))
        {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
        return super.onTouchEvent(ev);
    }

    public boolean canScrollVertically(AbsListView view)
    {
        boolean canScroll = false;

        if (view != null && view.getChildCount() > 0)
        {
            boolean isOnTop = view.getFirstVisiblePosition() != 0 || view.getChildAt(0).getTop() != 0;
            boolean isAllItemsVisible = isOnTop && view.getLastVisiblePosition() == view.getChildCount();

            if (isOnTop || isAllItemsVisible)
            {
                canScroll = true;
            }
        }

        return canScroll;
    }
}
Roth answered 25/4, 2018 at 15:24 Comment(1)
Perfect Answer !!Miasma
M
5
public class BottomSheetListView extends ListView {

    public BottomSheetListView(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent motionEvent) {
        View view = (View) getChildAt(getChildCount() - 1);

        int diffBottom = (view.getBottom() - (getHeight() + getScrollY()));
        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
            if (diffBottom == 0) {
                return false;
            }
        }

         /*//Need more improvement on this logic. Do not uncomment
        int diffTop = (view.getTop() - (getHeight() + getScrollY()));
        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
            if (diffTop < 0) {
                return true;
            }
        }*/

        return super.onInterceptTouchEvent(motionEvent);
    }

    @Override
    public boolean onTouchEvent(MotionEvent motionEvent) {
        if (canScrollVertically(this)) {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
        return super.onTouchEvent(motionEvent);
    }

    public boolean canScrollVertically(AbsListView absListView) {

        boolean canScroll = false;

        if (absListView != null && absListView.getChildCount() > 0) {

            boolean isOnTop = absListView.getFirstVisiblePosition() != 0 || absListView.getChildAt(0).getTop() != 0;
            boolean isAllItemsVisible = isOnTop && getLastVisiblePosition() == absListView.getChildCount();

            if (isOnTop || isAllItemsVisible)
                canScroll = true;
        }

        return canScroll;
    }
}
Malapropos answered 5/1, 2017 at 15:31 Comment(1)
@th3pat3l pl check my comment in the code. if u can improve it then that would be great else it's ok i'll manage. i took idea from #32632047Malapropos
C
4

Just set android:nestedScrollingEnabled to true inside your layout as ListView attribute or listView.setNestedScrollingEnabled(true) in java.

Codon answered 9/8, 2020 at 9:5 Comment(0)
P
3

I know it's a bit hack but it worked for me, and even listview is selectable. As we know that when ever bottom sheet change its state it can be caught in it's listener so don't let it change the state if listview is not touching top, henceforth touch event will be passed to listview and will work as it does.

    mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback(){
@Override
public void onStateChanged(View bottomSheet, int newState) {
        if (newState == BottomSheetBehavior.STATE_DRAGGING && !listIsAtTop()){
            mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
        }
}
@Override
public void onSlide(View bottomSheet, float slideOffset) {}});

public boolean listIsAtTop()   {
    if(tripListView.getChildCount() == 0) return true;
    return (tripListView.getChildAt(0).getTop() == 0 && tripListView.getFirstVisiblePosition() ==0);
}
Pluton answered 25/11, 2017 at 10:39 Comment(0)
C
2

I Think I am little bit late but please use nested scroll enable true in your listview it will help you with that scroll down issue.

<ListView
android:id="@+id/rv_jobs_offers"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="true"
android:layout_marginTop="@dimen/dimens_20"/>
Camelot answered 14/9, 2022 at 6:41 Comment(0)
V
1

I have tried Nested ListViews that was described here, but they do not allow expand /collapse bottom sheet when touching on ListView. Below is my solution, when you scroll up and ListView cannot scroll up, bottom sheet will be expanded, when you scroll down and ListView cannot do it, BottomSheet will be collapsed.

public class NestedListView extends ListView {

    private boolean mIsBeingDragged = false;
    private float mLastMotionY;

    public NestedListView(final Context context, final AttributeSet attrs) {
        super(context, attrs);
    }

    public NestedListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                if (canScrollDown() || canScrollUp()) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            }
        }
        return super.onInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final float y = event.getRawY();

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE: {
                if (!mIsBeingDragged) {
                    final float deltaY = mLastMotionY - y;
                    mIsBeingDragged = (deltaY > 0 && canScrollDown())
                        || (deltaY < 0 && canScrollUp());

                    if (mIsBeingDragged) {
                        final ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    } else {
                        final ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(false);
                        }
                        return false;
                    }
                }
            }

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                break;
        }

        mLastMotionY = y;

        return super.onTouchEvent(event);
    }

    public boolean canScrollUp() {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return false;
        }

        final int firstPosition = getFirstVisiblePosition();
        final int firstTop = getChildAt(0).getTop();
        return firstPosition > 0 || firstTop < getListPaddingTop();
    }

    public boolean canScrollDown() {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return false;
        }

        final int firstPosition = getFirstVisiblePosition();
        final int lastBottom = getChildAt(childCount - 1).getBottom();
        final int lastPosition = firstPosition + childCount;
        return lastPosition < getCount() || lastBottom > getHeight() - getListPaddingBottom();
    }
}
Vickey answered 22/11, 2019 at 9:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.