ListView中的水平RecyclerView滑动不太流畅

14

我有一个ListView,其中每个项目都是一个水平的RecyclerView。我面临的问题是,水平滚动不够流畅。如果我以完全水平的方向滚动/滑动,那么它就正常工作,但如果滚动略微偏离水平(比如与水平线成10度角),由于父级ListView,它会将其视为上下滚动而不是左右滚动。我尝试了这种方法:

   recyclerView.setOnTouchListener(new FlingRecyclerView.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    int action = event.getAction();
                    switch (action) {
                        case MotionEvent.ACTION_DOWN:
                            // Disallow ScrollView to intercept touch events.
                            v.getParent().requestDisallowInterceptTouchEvent(true);
                            Log.i("Scroll down", "Intercept true");
                            break;

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

                        case MotionEvent.ACTION_MOVE:
                            // Allow ScrollView to intercept touch events.
                            v.getParent().requestDisallowInterceptTouchEvent(true);
                            break;
                    }

                    // Handle HorizontalScrollView touch events.
                    v.onTouchEvent(event);
                    return true;
                }
            });

但这方法并不是很有效。事实上,我几乎没有注意到任何改善。这是因为当运动完全水平时,只有这个motionEvent被调用-那种情况已经很好了。然后我尝试做这个:

class MyGestureDetector extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            try {
                Log.i("TAG", "VelocityX " + Float.toString(velocityX) + "VelocityY " + Float.toString(velocityY));
                Log.i("TAG", "e1X " + Float.toString(e1.getX()) + "e1Y " + Float.toString(e1.getY()) + "e2X " + Float.toString(e2.getX()) + "e2Y " + Float.toString(e2.getY()));
                // downward swipe
                if (e2.getY() - e1.getY() > SWIPE_MAX_OFF_PATH && Math.abs(velocityY) > SWIPE_THRESHOLD_VELOCITY) {
                    Toast.makeText(TestActivity.this, "Downward Swipe", Toast.LENGTH_SHORT).show();
                    Log.i("TAG", "Downward swipe");
                }
                else if (e1.getY() - e2.getY() > SWIPE_MAX_OFF_PATH && Math.abs(velocityY) > SWIPE_THRESHOLD_VELOCITY) {
                    Toast.makeText(TestActivity.this, "Upward Swipe", Toast.LENGTH_SHORT).show();
                    Log.i("TAG", "Upward swipe");
                }
                    // right to left swipe
                else if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    Toast.makeText(TestActivity.this, "Left Swipe", Toast.LENGTH_SHORT).show();
                    Log.i("TAG", "Left swipe");
                }  else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    Toast.makeText(TestActivity.this, "Right Swipe", Toast.LENGTH_SHORT).show();
                    Log.i("TAG", "Right swipe");
                }
            } catch (Exception e) {
                // nothing
            }
            return false;
        }

    }

我注意到它仅在我松开手指时才注册事件,而不是在我开始滑动时。它不会立即在我触摸 RecyclerView 时起作用。但这也没有产生任何明显的改进。我还注意到这些方法在某种程度上也取决于压力。当我轻轻地滑动时,什么也没发生。而同样的事情,如果加大压力并按同样的路径滑动,就会产生滚动。

是否有办法使滚动平稳,即使滑动的路径不是水平直线,甚至如果运动是圆形的?感激不尽任何帮助。


你使用RecyclerView嵌套在ListView中只是为了实现水平滚动,这样做能够达到什么效果? - Brendon
@Brendon 我的布局是一个动态信息流。它可以垂直滚动,ListView 中的每个项目都是一个画廊。而画廊中有一些帖子可以水平滚动。 - Amit Tiwari
所以你只需要横向滚动是吗?只需在它内部膨胀视图时使用水平滚动,它就会平稳完美地滚动。尝试以另一种方式实现这一点。 - Brendon
我以前使用过ScrollView。毫无疑问,滚动很流畅。但HorizontalScrollView没有视图回收机制,因此我经常遇到内存不足的问题。 - Amit Tiwari
重复:https://dev59.com/XHE85IYBdhLWcg3wtV71 - Dean Wild
4个回答

8

Jim Baca帮我找到了正确的方向,我的问题是父视图(垂直滚动)正在从子视图(水平滚动)中窃取滚动。以下方法适用于我:

/**
 * This class is a workaround for when a parent RecyclerView with vertical scroll
 * is a bit too sensitive and steals onTouchEvents from horizontally scrolling child views
 */
public class NestedScrollingParentRecyclerView extends RecyclerView {
    private boolean mChildIsScrolling = false;
    private int mTouchSlop;
    private float mOriginalX;
    private float mOriginalY;

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

    public NestedScrollingParentRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public NestedScrollingParentRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    private void init(Context context) {
        ViewConfiguration vc = ViewConfiguration.get(context);
        mTouchSlop = vc.getScaledTouchSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the scroll
            mChildIsScrolling = false;
            return false; // Let child handle touch event
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                mChildIsScrolling = false;
                setOriginalMotionEvent(ev);
            }
            case MotionEvent.ACTION_MOVE: {
                if (mChildIsScrolling) {
                    // Child is scrolling so let child handle touch event
                    return false;
                }

                // If the user has dragged her finger horizontally more than
                // the touch slop, then child view is scrolling

                final int xDiff = calculateDistanceX(ev);
                final int yDiff = calculateDistanceY(ev);

                // Touch slop should be calculated using ViewConfiguration
                // constants.
                if (xDiff > mTouchSlop && xDiff > yDiff) {
                    mChildIsScrolling = true;
                    return false;
                }
            }
        }

        // In general, we don't want to intercept touch events. They should be
        // handled by the child view.  Be safe and leave it up to the original definition
        return super.onInterceptTouchEvent(ev);
    }

    public void setOriginalMotionEvent(MotionEvent ev) {
        mOriginalX = ev.getX();
        mOriginalY = ev.getY();
    }

    public int calculateDistanceX(MotionEvent ev) {
        return (int) Math.abs(mOriginalX - ev.getX());
    }

    public int calculateDistanceY(MotionEvent ev) {
        return (int) Math.abs(mOriginalY - ev.getY());
    }
}

5
问题可能是触摸事件不确定该去哪里。您可能想要使用onInterceptTouchEvent和TouchSlop来确定是否应从子行(回收站视图)中窃取触摸事件。
onInterceptTouchEvent很重要,因此ListView可以决定是否从子项中窃取触摸事件。
触摸差距很重要,因为它定义了在采取措施之前需要多少移动量(在这种情况下,从子代窃取事件)。这是因为仅用手指触摸屏幕(没有明显/预期的移动)将生成MotionEvent.ACTION_MOVE事件(您可以通过在MOTION_MOVE下添加日志记录以打印getX和getY事件来自行验证这一点。
如果仍然遇到问题,请验证recyclerview的子项是否也期望运动事件。如果这些子项是,则可能需要使用requestDisallowInterceptTouchEvent来防止recyclerview和listview拦截其事件。
这段代码基于在android文档中找到的代码,你可以在此处查看原始代码并了解更多关于触摸事件的信息。
     MotionEvent mOriginal;
     mIsScrolling = false;
     // put these two lines in your constructor
     ViewConfiguration vc = ViewConfiguration.get(view.getContext());
     mTouchSlop = vc.getScaledTouchSlop();


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    /*
     * This method JUST determines whether we want to intercept the motion.
     * If we return true, onTouchEvent will be called and we do the actual
     * scrolling there.
     */


    final int action = MotionEventCompat.getActionMasked(ev);

    // Always handle the case of the touch gesture being complete.
    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        // Release the scroll.

        mIsScrolling = false;
        return false; // Do not intercept touch event, let the child handle it
    }

    switch (action) {
        case MotionEvent.ACTION_DOWN:  {
            mIsScrolling = false;
            setOriginalMotionEvent(ev);
        }
        case MotionEvent.ACTION_MOVE: {
            if (mIsScrolling) {
                // We're currently scrolling, so yes, intercept the 
                // touch event!
                return true;
            }

            // If the user has dragged her finger horizontally more than 
            // the touch slop, start the scroll

            final int xDiff = calculateDistanceX(ev);
            final int yDiff = calculateDistanceY(ev);

            // Touch slop should be calculated using ViewConfiguration 
            // constants.
            if (yDiff > mTouchSlop &&  yDiff > xDiff) { 
                // Start scrolling!
                mIsScrolling = true;
                return true;
            }
            break;
        }
        ...
    }

    // In general, we don't want to intercept touch events. They should be 
    // handled by the child view.  Be safe and leave it up to the original definition
    return super.onInterceptTouchEvent(ev);
}

public void setOriginalMotionEvent(MotionEvent ev){
     mOriginal = ev.clone();
}

public int calculateDistanceX(ev){
    int distance = Math.abs(mOriginal.getX() - ev.getX());
    return distance;
}

public int calculateDistanceY(ev){
    int distance = Math.abs(mOriginal.getY() - ev.getY());
    return distance;
}

1

我记得你可以在ListView中重写onInterceptTouchEvent方法。

private Point movementStart;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean superResult = super.onInterceptTouchEvent(ev);
    if(movementStart == null) {
        movementStart = new Point((int)ev.getX(), (int)ev.getY());
    }
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            return false;
        case MotionEvent.ACTION_MOVE:
            //if movement is horizontal than don't intercept it
            if(Math.abs(movementStart.x - ev.getX()) > Math.abs(movementStart.y - ev.getY())){
                return false;
            }
                break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            movementStart = null;
            break;
    }
    return superResult;
}

您可以通过以下方式扩展上面的代码:

private Point movementStart;
private boolean disallowIntercept = false;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean superResult = super.onInterceptTouchEvent(ev);
    if(movementStart == null) {
        movementStart = new Point((int)ev.getX(), (int)ev.getY());
    }
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            return false;
        case MotionEvent.ACTION_MOVE:
            //if movement is horizontal than don't intercept it
            if(Math.abs(movementStart.x - ev.getX()) > Math.abs(movementStart.y - ev.getY())){
                disallowIntercept = true;
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            movementStart = null;
            disallowIntercept = false;
            break;
    }

    if(disallowIntercept) {
        return false;
    } else {
        return superResult;
    }
}

当我为ListView覆盖上述方法时,RecyclerView的水平滚动是完美的。但是,在RecyclerView上向上或向下滚动(从RecyclerView内部开始触摸)不会垂直滚动ListView。只有当触摸开始在ListView的某个地方,例如两个水平RecyclerView之间的一些间隙时,ListView才会垂直滚动。 - Amit Tiwari
我编辑了我的答案,并添加了调用super.onInterceptTouchEvent。ListView的行为与ScrollView不同,我在内存中有ScrollView.onInterceptTouchEvent :) - Drake29a
它在大多数情况下都能正常工作,但不够流畅。有时候ListView就是不会移动。 - Amit Tiwari

1

你尝试过使用 verticalRow.setNestedScrollingEnabled(false); 吗? //这里的 verticalRow 是一个 RecyclerView 实例


我也遇到了这个问题,我的观察是setNestedScrollingEnabled(false)可以解决父级ScrollView/RecyclerView的平稳滚动,但无法解决子级ScrollView/RecyclerView的平稳滚动。 - user1354603

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接