ListView内部的水平RecyclerView不能滚动得非常流畅

时间:2015-12-24 06:54:09

标签: android listview scroll android-recyclerview user-experience

我有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;
                }
            });

但它不是很有效。事实上,我没有注意到很多改进。这是因为只有当动作完全水平时才会调用此动作事件 - 这种情况已经正常工作。然后我尝试这样做:

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后,它无法正常工作。而这也未能产生任何明显的改善。我也注意到这些方法在某种程度上也依赖于压力。当我用轻微的压力轻扫时,什么也没发生。同样的事情在同一条道路上完成,压力越大,就会产生滚动。

即使滑动的路径不是水平直线,或者即使动作是圆形的,有没有办法使滚动变得平滑?任何帮助将受到高度赞赏。

4 个答案:

答案 0 :(得分:7)

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());
    }
}

答案 1 :(得分:5)

问题可能是触摸事件无法确定去哪里。您可能希望使用onInterceptTouchEvent和TouchSlop来确定是否应该从子行中窃取触摸事件(回收器视图)。

onInterceptTouchEvent非常重要,因此ListView可以决定是否应该窃取来自儿童的触摸事件。

触摸坡度非常重要,因为它定义了在我们采取行动之前需要多少运动(在这种情况下是从孩子身上窃取事件)。这是因为只需用手指触摸屏幕(没有明显/预期的移动)就会生成MotionEvent.ACTION_MOVE事件(您可以通过在MOTION_MOVE下添加日志来自行验证,以打印出getX和getY事件。

如果您仍然遇到问题,请确认回收者视图的子项也不期待运动事件。如果是这些孩子,他们可能需要使用requestDisallowInterceptTouchEvent来阻止recyclerview和listview拦截他们的活动。

此代码基于Android文档中的代码,您可以查看原始代码并阅读有关触摸事件的更多信息here

     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;
}

答案 2 :(得分:1)

IIRC你可以覆盖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;
    }
}

答案 3 :(得分:1)

你试过verticalRow.setNestedScrollingEnabled(false); //这里verticalRow是recylerView实例

相关问题