NestedScrollView and Horizontal RecyclerView Smooth Scrolling

♀尐吖头ヾ 提交于 2019-11-28 05:02:28

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.

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

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!