Horizontal RecyclerView inside ListView not scrolling very smooth

…衆ロ難τιáo~ 提交于 2020-01-12 13:46:34

问题


I have a ListView, each of whose items is a Horizontal RecyclerView. The problem that I am facing is, the horizontal scroll is not very smooth. If I scroll/swipe in a perfectly horizontal direction, then it works fine but if the scroll is even slightly non-horizontal (say at a 10 degree angle to horizontal), it regards that as up/down scroll rather than left / right, due to the parent ListView. I tried this method:

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

But it was not very effective. In fact, I couldn't notice much improvement. That's because this motionEvent gets called only when the motion is perfectly horizontal - that case is already working fine. Then I tried doing this:

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

    }

And I noticed that it registers the events only when I release my finger, not when I start the swipe. It does not work as soon as I touch the RecyclerView. And this too, failed to produce any noticeable improvement. I also noticed one thing that these methods are somehow dependent on pressure too. When I did a swipe with a light pressure, nothing happened. And the same thing done in same path, with greater pressure does produce scroll.

Is there any way to make the scroll smooth even if the path of swipe is not horizontally straight or even if the motion is kind of circular? Any help would be highly appreciated.


回答1:


Jim Baca got me on the right track, my problem was that the parent view(vertical scroll) was stealing the scroll from child views(horizontal scroll). This worked for me:

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



回答2:


The problem is likely that the touch events aren't sure where to go. You likely want to use onInterceptTouchEvent along with TouchSlop to determine if you should steal touch events from the child rows(recycler views).

The onInterceptTouchEvent is important so that the ListView can decide if it should steal touch events from children.

The touch slop is important because it defines how much movement is required before we should take action(In this case to steal events from the child). That is because just touching the screen with your finger(with no obvious /intended movement) will generate MotionEvent.ACTION_MOVE events(You can verify this yourself by adding logging under the MOTION_MOVE to print out getX and getY events.

If you still run into problems, verify that children of the recycler view aren't expecting motion events as well. If those children are they may need to use requestDisallowInterceptTouchEvent to prevent the recyclerview and listview from intercepting their events.

This code was based on code found on the android docs you can see the original and read more about touch events 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;
}



回答3:


IIRC you can override onInterceptTouchEvent method in ListView

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

You can extend code above in this way:

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



回答4:


Did you try verticalRow.setNestedScrollingEnabled(false); //Here verticalRow is recylerView instance



来源:https://stackoverflow.com/questions/34448587/horizontal-recyclerview-inside-listview-not-scrolling-very-smooth

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