嵌套的RecyclerView。如何在子RecyclerView滚动时防止父RecyclerView被滚动?

32
我正在尝试实现一个水平的RecyclerView,每个RecyclerView的项都将是具有网格布局的垂直RecyclerView。我面临的问题是,当我尝试在子RecyclerView中垂直滚动时,有时父RecyclerView会接管滚动并开始水平滚动。我尝试解决这个问题的方法包括:
  1. 在父RecyclerView上调用setNestedScrollingEnabled(false)
  2. 在子RecyclerView的onTouch()中,通过调用requestDisallowInterceptTouchEvent(false)禁用父RecyclerView上的触摸事件
以上解决方案都不能完美解决问题。如有帮助,不胜感激。
14个回答

33
问题对我来说很有趣。因此我尝试去实现并取得了如下成果(您还可以在这里看到视频,效果非常流畅)。 enter image description here 因此,您可以试试像这样做:
定义一个继承自LinearLayoutManager的CustomLinearLayoutManager,像这样:
public class CustomLinearLayoutManager extends LinearLayoutManager {

    public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
    }

    @Override
    public boolean canScrollVertically() {
        return false;
    }
}

将这个CustomLinearLayoutManager设置为您的父级RecyclerView

RecyclerView parentRecyclerView = (RecyclerView)findViewById(R.id.parent_rv);
CustomLinearLayoutManager customLayoutManager = new CustomLinearLayoutManager(this, LinearLayoutManager.HORIZONTAL,false);
parentRecyclerView.setLayoutManager(customLayoutManager);
parentRecyclerView.setAdapter(new ParentAdapter(this)); // some adapter

现在为子RecyclerView,定义一个自定义的CustomGridLayoutManager,继承GridLayoutManager

public class CustomGridLayoutManager extends GridLayoutManager {

    public CustomGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public CustomGridLayoutManager(Context context, int spanCount) {
        super(context, spanCount);
    }

    public CustomGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
        super(context, spanCount, orientation, reverseLayout);
    }

    @Override
    public boolean canScrollHorizontally() {
        return false;
    }
}

将其设置为子RecyclerViewlayoutManager:

childRecyclerView = (RecyclerView)itemView.findViewById(R.id.child_rv);
childRecyclerView.setLayoutManager(new CustomGridLayoutManager(context, 3));
childRecyclerView.setAdapter(new ChildAdapter()); // some adapter

基本上,父 RecyclerView 只监听水平滚动,而子 RecyclerView 只监听垂直滚动。

另外,如果您还想处理对角线滑动(稍微倾斜于垂直或水平),您可以在 RecylerView 中包含手势侦听器。

public class ParentRecyclerView extends RecyclerView {

    private GestureDetector mGestureDetector;

    public ParentRecyclerView(Context context) {
        super(context);
        mGestureDetector = new GestureDetector(this.getContext(), new XScrollDetector());
       // do the same in other constructors
    }

   // and override onInterceptTouchEvent

   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev) && mGestureDetector.onTouchEvent(ev);
    }

}

其中 XScrollDetector 是什么

class XScrollDetector extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return Math.abs(distanceY) < Math.abs(distanceX);
        }
}

因此,ParentRecyclerView 会请求子视图(在我们的情况下为VerticalRecyclerView)来处理滚动事件。如果子视图处理,则父视图不做任何处理,否则最终父视图来处理滚动。


扩展线性布局是一个很好的建议。我差点把整个实现都改成了NestedScrollList。看起来它为我省了这个功夫。 - wtk
2
很棒的答案,在我的情况下,我不需要使用整个解决方案,只需要SimpleOnGestureListener部分就可以完美地工作。谢谢。 - Pedro Okawa
实际上,当使用LinearLayoutManager时,仅限制对角线滚动就足够了。 - Micha
不需要扩展LinearLayoutManager,因为你可以看到canScrollHorizontally / canScrollVertically方法的实现已经使用了你在构造函数中提供的方向来明确禁用水平/垂直滚动。 - Shahab Rauf

8
你可以尝试在子RecyclerView上使用setNestedScrollingEnabled(false),因为RecyclerView的嵌套滚动性质是作为子视图(这就是为什么它实现了NestedScrollingChild接口)。在子RecyclerView的onTouch()方法中,你可以通过调用requestDisallowInterceptTouchEvent(true)来禁用父RecyclerView的触摸事件,而不是false。如果你需要自定义RecyclerView,你可以重写onTouchEvent方法。
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_UP) {
        // ensure we release the disallow request when the finger is lifted
        getParent().requestDisallowInterceptTouchEvent(false);
    } else {
        getParent().requestDisallowInterceptTouchEvent(true);
    }
    // Call the super class to ensure touch handling
    return super.onTouchEvent(event);
}

或者,通过外部的触摸监听器,
child.setOnTouchListener(new View.OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (v.getId() == child.getId()) {
            if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_UP) {
                // ensure we release the disallow request when the finger is lifted
                child.getParent().requestDisallowInterceptTouchEvent(false);
            } else {
                child.getParent().requestDisallowInterceptTouchEvent(true);
            }
        }
        // Call the super class to ensure touch handling
        return super.onTouchEvent(event);
    }
});

8

我曾在一个类似的项目中解决了这个问题,方法与你(和其他人)截然不同。

与其让子元素告诉父元素何时停止查看事件,我让父元素根据方向来决定何时忽略它们。但这种方法需要自定义视图,稍微费点功夫。以下是我创建的作为外部/父级视图使用的代码。

public class DirectionalRecyclerView extends RecyclerView {

    private static float LOCK_DIRECTION_THRESHOLD; //The slop
    private float startX;
    private float startY;
    private LockDirection mLockDirection = null;

    public DirectionalRecyclerView(Context context) {
        super(context);
        findThreshold(context);
    }

    public DirectionalRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        findThreshold(context)
    }

    public DirectionalRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        findThreshold(context);
    }

    private void findThreshold(Context context) {
        //last number is number of dp to move before deciding that's a direction not a tap, you might want to tweak it
        LOCK_DIRECTION_THRESHOLD = context.getResources().getDisplayMetrics().density * 12;
    }

    //events start at the top of the tree and then pass down to
    //each child view until they reach where they were initiated
    //unless the parent (this) method returns true for this visitor
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX();
                startY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (mLockDirection == null) {
                    float currentX = event.getX();
                    float currentY = event.getY();
                    float diffX = Math.abs(currentX - startX);
                    float diffY = Math.abs(currentY - startY);
                    if (diffX > LOCK_DIRECTION_THRESHOLD) {
                        mLockDirection = LockDirection.HORIZONTAL;
                    } else if (diffY > LOCK_DIRECTION_THRESHOLD) {
                        mLockDirection = LockDirection.VERTICAL;
                    }
                } else {
                    //we have locked a direction, check whether we intercept
                    //the future touches in this event chain
                    //(returning true steals children's events, otherwise we'll
                    // just let the event trickle down to the child as usual)
                    return mLockDirection == LockDirection.HORIZONTAL;
                }
                break;
            case MotionEvent.ACTION_UP:
                mLockDirection = null;
                break;
        }
        //dispatch cancel, clicks etc. normally
        return super.onInterceptTouchEvent(event);
    }

    private enum LockDirection {
        HORIZONTAL,
        VERTICAL
    }

}

这对我在另一种情况下起作用 - 水平RV包含协调器,包含折叠工具栏和嵌套滚动视图。唯一需要更改的是在dispatchTouchEvent()中设置mLockDirection = null;,因为在onInterceptTouchEvent()onTouchEvent()中并不总是被调用。 - WindRider
感谢您提供这种有价值的方法。在父视图中处理这种类型的内容的方法要简单得多。我已经成功地使用了您修改后的逻辑,在水平ViewPager中创建了一个垂直视图。 - Stefan Sprenger

5

将监听器设置为嵌套的RecyclerView

 View.OnTouchListener listener = new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_MOVE
                            ) {
                        v.getParent().requestDisallowInterceptTouchEvent(true);

                    } else {
                        v.getParent().requestDisallowInterceptTouchEvent(false);

                    }
                    return false;
                }
            };

            mRecyclerView.setOnTouchListener(listener);

这是如何工作的?你能简单解释一下吗? - Salmaan
当嵌套的RecyclerView正在滚动时,它会禁止父RecyclerView的拦截触摸事件。 - Shahab Rauf

5

现在你可以尝试android:nestedScrollingEnabled了,因为Google解决了使用nestedScrollingEnabled时出现的崩溃问题 (问题编号197932)


3

试试这个。对于我的用例,它已经起作用:

nestedRecyclerView.setOnTouchListener(new View.OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return true;
    }
});

3

尝试下面的代码,希望它能够工作。

nestedRecyclerView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    int action = event.getAction();
                   switch (action) {
                  case MotionEvent.ACTION_DOWN:
                     // Disallow parent to intercept touch events.
                     v.getParent().requestDisallowInterceptTouchEvent(true);
                     break;

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

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

3

我用卡片视图作为单元格,使用子单元格的 setOnClickListener 取消父级可滚动的子适配器。

holder.itemView.setOnTouchListener { view, _ ->
    view.parent.parent.requestDisallowInterceptTouchEvent(true)
    false
}

我们之所以调用 view.parent.parent 是因为 itemView 是一个单元格布局,它的父级是我们的子级 recyclerView,同时,我们需要到达子级 recyclerView 的父级以防止父级 recyclerView 滚动。

1
尝试以下代码来滚动内部的RecyclerView。
innerRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {

        @Override
        public void onTouchEvent(RecyclerView recycler, MotionEvent event) {
            // Handle on touch events here
            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    // Disallow Parent RecyclerView to intercept touch events.
                    recycler.getParent().requestDisallowInterceptTouchEvent(true);
                    break;

                case MotionEvent.ACTION_UP:
                    // Allow Parent RecyclerView to intercept touch events.
                    recycler.getParent().requestDisallowInterceptTouchEvent(false);
                    break;
            }


        }

        @Override
        public boolean onInterceptTouchEvent(RecyclerView recycler, MotionEvent event) {
            return false;
        }

    });

1

在外部 RecyclerView 的 Adapter 中,你可以尝试以下操作:

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.cardview, parent, false);

        RVAdapter2 recyclerViewAdapter2 = new RVAdapter2();
        RecyclerView innerRV = (RecyclerView) v.findViewById(R.id.rv2);
        // Setup layout manager for items
        LinearLayoutManager layoutManager2 = new LinearLayoutManager(parent.getContext());
        // Control orientation of the items
        layoutManager2.setOrientation(LinearLayoutManager.VERTICAL);
        innerRV.setLayoutManager(layoutManager2);
        innerRV.setAdapter(recyclerViewAdapter2);

        innerRV.setOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
            }
        });

        return new MyViewHolder(v);
    }

对于API23,您也可以尝试使用innerRV.setOnScrollChangeListener,因为setOnScrollListener已被弃用。

更新:

另一个选项是使用addOnScrollListener而不是setOnScrollListener

希望这有所帮助!


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