23

I have a single vertical nestedscrollview that contains a bunch of recyclerview with a horizontal layoutmanager setup. The idea is pretty similar to how the new google play store looks. I'm able to make it functional but it isn't smooth at all. Here are the problems:

1) The horizontal recyclerview item fails to intercept the touch event most of the times even though i tap right on it. The scroll view seems to take precedence for most of the motions. It's hard for me to get a hook onto the horizontal motion. This UX is frustrating as I need to try a few times before it works. If you check the play store, it is able to intercept the touch event really well and it just works well. I noticed in the play store the way they set it up is many horizontal recyclerviews inside one vertical recyclerview. No scrollview.

2) The height of the horizontal recyclerviews have to be manually set and there is no easy way to calculate the height of the children elements.

Here is the layout I'm using:

<android.support.v4.widget.NestedScrollView
    android:id="@+id/scroll"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:background="@color/dark_bgd"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:id="@+id/main_content_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="gone"
            tools:visibility="gone"
            android:orientation="vertical">                

                <android.support.v7.widget.RecyclerView
                    android:id="@+id/starring_list"
                    android:paddingLeft="@dimen/spacing_major"
                    android:paddingRight="@dimen/spacing_major"
                    android:layout_width="match_parent"
                    android:layout_height="180dp" />

This UI pattern is very basic and most likely used in many different apps. I've read many SO's where ppl say it's a bad idea to put a list within a list, but it is a very common and modern UI pattern used all over the place.Think of netflix like interface with a series of horizontal scroll lists inside a vertical list. Isn't there a smooth way to accomplish this?

Example image from the store:

Google Play Store

falc0nit3
  • 999
  • 2
  • 8
  • 16
  • 1
    Weird thing is that I have the opposite issue: I want the NestedScrollView to allow to scroll vertically, but the horizontal RecyclerView takes the touch events so when I start the vertical scrolling on it, it doesn't allow it. Also, btw, if you have just a few items to show, you can use a simple HorizontalScrollView with LinearLayout. – android developer Dec 30 '15 at 08:10

3 Answers3

23

So the smooth scrolling issue is fixed now. It was caused by a bug in the NestedScrollView in the Design Support Library (currently 23.1.1).

You can read about the issue and the simple fix here: https://code.google.com/p/android/issues/detail?id=194398

In short, after you performed a fling, the nestedscrollview didn't register a complete on the scroller component and so it needed an additional 'ACTION_DOWN' event to release the parent nestedscrollview from intercepting(eating up) the subsequent events. So what happened was if you tried scrolling your child list(or viewpager), after a fling, the first touch releases the parent NSV bind and the subsequent touches would work. That was making the UX really bad.

Essentially need to add this line on the ACTION_DOWN event of the NSV:

computeScroll();

Here is what I'm using:

public class MyNestedScrollView extends NestedScrollView {
private int slop;
private float mInitialMotionX;
private float mInitialMotionY;

public MyNestedScrollView(Context context) {
    super(context);
    init(context);
}

private void init(Context context) {
    ViewConfiguration config = ViewConfiguration.get(context);
    slop = config.getScaledEdgeSlop();
}

public MyNestedScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

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


private float xDistance, yDistance, lastX, lastY;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final float x = ev.getX();
    final float y = ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            xDistance = yDistance = 0f;
            lastX = ev.getX();
            lastY = ev.getY();

            // This is very important line that fixes 
           computeScroll();


            break;
        case MotionEvent.ACTION_MOVE:
            final float curX = ev.getX();
            final float curY = ev.getY();
            xDistance += Math.abs(curX - lastX);
            yDistance += Math.abs(curY - lastY);
            lastX = curX;
            lastY = curY;

            if (xDistance > yDistance) {
                return false;
            }
    }


    return super.onInterceptTouchEvent(ev);
}

}

Use this class in place of your nestedscrollview in the xml file, and the child lists should intercept and handle the touch events properly.

Phew, there are actually quite a few bugs like these that makes me want to ditch the design support library altogether and revisit it when its more mature.

falc0nit3
  • 999
  • 2
  • 8
  • 16
  • `slop`, `mInitialMotionX`, `mInitialMotionY`, `x` and `y` are not being used in `MyNestedScrollView`. – Rany Albeg Wein Apr 06 '18 at 00:29
  • 1
    This solution does not work anymore in support lib 26.x.x and above! Still marked as a bug in https://code.google.com/p/android/issues/detail?id=194398 – Sjd May 17 '18 at 12:00
1

Since falc0nit3 solution doesn't work anymore (currently the project using 28.0.0 version of support library), i have found an another one.

The background reason of the issue is still the same, scrollable view eats on down event by returning true on the second tap, where it shouldn't, because naturally second tap on the fling view stops scrolling and may be used with next move event to start opposite scroll The issue is reproduced as with NestedScrollView as with RecyclerView. My solution is to stop scrolling manually before native view will be able to intercept it in onInterceptTouchEvent. In this case it won't eat the ACTION_DOWN event, because it have been stopped already.

So, for NestedScrollView:

class NestedScrollViewFixed(context: Context, attrs: AttributeSet) :
    NestedScrollView(context, attrs) {

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            onTouchEvent(ev)
        }
        return super.onInterceptTouchEvent(ev)
    }
}

For RecyclerView:

class RecyclerViewFixed(context: Context, attrs: AttributeSet) :
    RecyclerView(context, attrs) {

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        if (e.actionMasked == MotionEvent.ACTION_DOWN) {
            this.stopScroll()
        }
        return super.onInterceptTouchEvent(e)
    }

}

Despite solution for RecyclerView looks easy to read, for NestedScrollView it's a bit complicated. Unfortunately, there is no clear way to stop scrolling manually in widget, which the only responsibility is to manage scroll (omg). I'm interesting in abortAnimatedScroll() method, but it is private. It is possible to use reflection to get around it, but for me better is to call method, which calls abortAnimatedScroll() itself. Look at onTouchEvent handling of ACTION_DOWN:

 /*
 * If being flinged and user touches, stop the fling. isFinished
 * will be false if being flinged.
 */
if (!mScroller.isFinished()) {
    Log.i(TAG, "abort animated scroll");
    abortAnimatedScroll();
}

Basically stopping fling is managed in this method, but a bit later, than we have to call it to fix the bug

Unfortunately due to this we can't just create OnTouchListener and set it outside, so only inheritance fits the requirements

Beloo
  • 9,723
  • 7
  • 40
  • 71
  • i have found a flaw in my solution, it doesn't block clicks on element when user stops fling and doesn't do vertical scroll. Still looking for improvment (google play handles it well) – Beloo Feb 04 '19 at 15:39
0

I've succeded in doing horizontal scrolling in a vertically scrolling parent with a ViewPager :

<android.support.v4.widget.NestedScrollView

    ...

    <android.support.v4.view.ViewPager
        android:id="@+id/pager_known_for"
        android:layout_width="match_parent"
        android:layout_height="350dp"
        android:minHeight="350dp"
        android:paddingLeft="24dp"
        android:paddingRight="24dp"
        android:clipToPadding="false"/>

public class UniversityKnownForPagerAdapter extends PagerAdapter {

public UniversityKnownForPagerAdapter(Context context) {
    mContext = context;
    mInflater = LayoutInflater.from(mContext);
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
    View rootView = mInflater.inflate(R.layout.card_university_demographics, container, false);

    ...

    container.addView(rootView);

    return rootView;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    container.removeView((View)object);
}

@Override
public int getCount() {
    return 4;
}

@Override
public boolean isViewFromObject(View view, Object object) {
    return (view == object);
}

Only issue : you must provide a fixed height to the view pager

Guillaume Imbert
  • 496
  • 5
  • 11
  • Thanks for your reply. I tried your solution too and got the viewpager to work and scroll, but the same scrolling problems that I mentioned above happened here too. See my answer for the fix. – falc0nit3 Dec 16 '15 at 11:18
  • Did you try with a viewpager instead of a recyclerview? – Guillaume Imbert Dec 16 '15 at 11:22
  • I did, it was still not intercepting the events too well. The problem is not with the child list implementations (Recyclerview/Viewpager/HorizontalListView etc) i think, but rather the NestedScrollView library. (as i've mentioned in my answer above) – falc0nit3 Dec 16 '15 at 11:30
  • Has your viewpager implementation been working flawlessly? I do have to mention the implementation I ended up writing uses 3 Imageviews on every viewpager item. So a swipe would swipe 3 images at a time. Look at the netflix android app for an example. – falc0nit3 Dec 16 '15 at 11:34