Android 嵌套回收视图。如何防止在子RecyclerView滚动时父RecyclerView被滚动?

Android 嵌套回收视图。如何防止在子RecyclerView滚动时父RecyclerView被滚动?,android,android-recyclerview,nestedrecyclerview,Android,Android Recyclerview,Nestedrecyclerview,我正在尝试实现一个水平的recyclerview,并且recyclerview的每个项目都将是一个垂直的recyclerview,具有网格布局。我面临的问题是,当我尝试垂直滚动子recyclerview时,有时父recyclerview会滚动并开始水平滚动。我试图解决这个问题的方法是 setNestedScrollingEnabled(false)在父级recyclerview 在子对象的recyclerview的onTouch()中,我通过调用RequestDisallowWinterCept

我正在尝试实现一个水平的
recyclerview
,并且
recyclerview
的每个项目都将是一个垂直的
recyclerview
,具有网格布局。我面临的问题是,当我尝试垂直滚动子
recyclerview
时,有时父
recyclerview
会滚动并开始水平滚动。我试图解决这个问题的方法是

  • setNestedScrollingEnabled(false)
    在父级
    recyclerview
  • 在子对象的
    recyclerview
    onTouch()
    中,我通过调用
    RequestDisallowWinterCeptTouchEvent(false)

  • 上述解决方案都不能完美解决该问题。感谢您的帮助

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

    nestedRecyclerView.setOnTouchListener(new View.OnTouchListener() {
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return true;
        }
    });
    

    尝试下面的代码滚动内部回收视图

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

    在IMO中,您可以在outer RecyclerView的适配器内尝试以下操作:

        @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,您还可以尝试
    innerv.setOnScrollChangeListener
    ,因为
    setOnScrollListener
    已被弃用

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

    希望有帮助

    父recyclerview上的setNestedScrollingEnabled(false)

    您可以在子RecyclerView(如果有)上尝试设置NestedScrollingEnabled(false)。RecyclerView的nestedscroll属性与子级类似(这就是为什么它实现了
    NestedScrollingChild

    在子recyclerview的onTouch()中,我禁用了上的触摸事件 父recyclerview被调用 RequestDisallowWinterCeptTouchEvent(false)

    这应该是可行的,但您应该做的是
    requestDisallowerWinterCeptTouchEvent(true)
    ,而不是
    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);
        }
    });
    

    我在一个类似的项目中通过对您(和这里的其他人)采取相反的方法解决了这个问题

    我不是让孩子告诉家长什么时候停止看事件,而是让家长决定什么时候忽略(根据方向)。这种方法需要一个自定义视图,但这需要更多的工作。下面是我创建的用作外部/父视图的内容

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

    使用此代码关闭滚动打开
    recyclerview

    recyclerView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return true;
        }
    });
    
    childRecyclerView = (RecyclerView)itemView.findViewById(R.id.child_rv);
    childRecyclerView.setLayoutManager(new CustomGridLayoutManager(context, 3));
    childRecyclerView.setAdapter(new ChildAdapter()); // some adapter
    

    像这样扩展自定义布局管理器

     public class CustomLayoutManager extends LinearLayoutManager {
     private boolean isScrollEnabled = true;
    
     public CustomGridLayoutManager(Context context) {
     super(context);
            }
      @Override
     public boolean canScrollVertically() {
     return false;
     }
    }
    

    将布局管理器设置为“自定义布局管理器”

    您应该这样做:

    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:
    
                        recycler.getParent().requestDisallowInterceptTouchEvent(true);
    
                        break;
    
                    case MotionEvent.ACTION_UP:
    
                        recycler.getParent().requestDisallowInterceptTouchEvent(false);
    
                        break;
    
                    case MotionEvent.ACTION_MOVE:
    
                        recycler.getParent().requestDisallowInterceptTouchEvent(true);
    
                        break;
                }
    
    
            }
    
            @Override
            public boolean onInterceptTouchEvent(RecyclerView recycler, MotionEvent event) {
                return false;
            }
    
        });
    

    希望这会对您有所帮助。

    现在您可以尝试
    android:nestedScrollingEnabled
    ,因为Google使用
    nestedScrollingEnabled
    修复了一个崩溃。请尝试下面的代码,希望它能起作用

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

    这个问题对我来说似乎很有趣。所以我试着去实现,这就是我所实现的(你也可以看到),非常顺利

    因此,您可以尝试以下方法:

    public class CustomLinearLayoutManager extends LinearLayoutManager {
    
        public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
            super(context, orientation, reverseLayout);
        }
    
        @Override
        public boolean canScrollVertically() {
            return false;
        }
    }
    
    定义
    CustomLinearLayoutManager
    扩展
    LinearLayoutManager
    如下:

    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
    
    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);
        }
    
    }
    
    现在,对于child
    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;
        }
    }
    
    并将其设置为
    layoutmanager
    ,以便子级
    RecyclerView

    recyclerView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return true;
        }
    });
    
    childRecyclerView = (RecyclerView)itemView.findViewById(R.id.child_rv);
    childRecyclerView.setLayoutManager(new CustomGridLayoutManager(context, 3));
    childRecyclerView.setAdapter(new ChildAdapter()); // some adapter
    
    因此基本上,父级
    RecyclerView
    只监听水平滚动,子级
    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
    
    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);
            }
    }
    
    类XScrollDetector扩展了GestureDetector.SimpleOnGestureListener{ @凌驾 公共布尔onScroll(MotionEvent e1、MotionEvent e2、浮点距离X、浮点距离Y){ 返回Math.abs(distanceY)
    因此,
    ParentRecyclerView
    要求子视图(在我们的示例中为VerticalRecyclerView)处理滚动事件。如果子视图处理,则父视图不执行任何其他操作。父视图最终处理滚动。

    将侦听器设置为嵌套的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);
    

    这个问题存在于Android对RecyclerView的onInterceptTouchEvent()方法的实现中。 这个博客指出了这个问题,并解决了它-。
    唯一的区别是父级垂直滚动,子级水平滚动。但是解决方案也会注意到它应该适合您的情况。

    我使用了单元格的卡片视图,并使用子单元格项目视图
    setOnClickListener在子适配器中禁用了父回收器视图滚动

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

    我们正在调用
    view.parent.parent
    ,因为
    itemView
    是一个单元格布局,它的父级是我们的子级
    recyclerView
    ,而且,我们需要联系子级recyclerView的父级以防止父级
    recyclerView
    滚动。

    扩展线性布局是一个很好的建议。我几乎将整个实现更改为NestedScrollList。看起来这帮我省去了很多努力。惊人的答案,在我的例子中,我不需要使用整个解决方案,只需要SimpleNogestureListener部分,它工作得非常完美。谢谢,的确,当使用
    LinearLayoutManager
    时,限制对角线滚动就足够了。不需要扩展LinearLayoutManager,因为显式禁用水平/垂直滚动,因为您可以看到/methods的实现已经在使用构造函数中提供的方向。如果在onInterceptTouchEvent中返回false,onTouchEvent永远不会被称为Hi