【问题标题】:Nested RecyclerView. How to prevent parent RecyclerView from getting scrolled while child RecyclerView is scrolling?嵌套的 RecyclerView。如何防止父 RecyclerView 在子 RecyclerView 滚动时滚动?
【发布时间】:2016-07-17 03:10:02
【问题描述】:

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

  1. setNestedScrollingEnabled(false) 在父 recyclerview
  2. 在子recyclerviewonTouch() 中,我通过调用requestdisallowinterceptTouchevent(false) 禁用父recyclerview 上的触摸事件

以上解决方案都不能完美解决问题。任何帮助表示赞赏

【问题讨论】:

    标签: android android-recyclerview nestedrecyclerview


    【解决方案1】:

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

    所以你可以试试这样的:

    像这样定义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
    

    现在对于子 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;
        }
    }
    

    并将其设置为layoutManger 给孩子RecyclerView

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

    所以基本上父RecyclerView 只听水平滚动和孩子RecyclerView 只听垂直滚动。

    除此之外,如果您还想处理对角滑动(几乎不偏向垂直或水平),您可以在 parent 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。看起来它节省了我的精力。
    • 惊人的答案,就我而言,我不需要使用整个解决方案,只需要使用 SimpleOnGestureListener 部分,它可以完美运行。谢谢。
    • 确实,当使用LinearLayoutManager 时,限制对角滚动就足够了。
    • 不需要扩展 LinearLayoutManager 因为明确禁用水平/垂直滚动,因为您可以看到 canScrollHorizontally / canScrollVertically 方法的实现它已经在使用您在构造函数中提供的方向。
    【解决方案2】:

    父recyclerview上的setNestedScrollingEnabled(false)

    您可以尝试在 child RecyclerView 上使用setNestedScrollingEnabled(false)(如果有)。 RecyclerView 的嵌套滚动特性是一个孩子的特性(这就是它实现NestedScrollingChild 的原因)。

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

    这应该可以,但你应该做的是requestDisallowInterceptTouchEvent(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);
        }
    });
    

    【讨论】:

      【解决方案3】:

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

      我没有让孩子告诉父母何时停止查看事件,而是让父母决定何时忽略(基于方向)。这种方法需要一个自定义视图,但这可能需要更多的工作。下面是我创建的将用作外部/父视图的内容。

      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()
      • 感谢这种有价值的方法。在父母中处理这种事情的方法要容易得多。我成功地使用您修改后的逻辑在水平视图页面中创建了一个垂直视图。
      【解决方案4】:

      现在您可以尝试 android:nestedScrollingEnabled,因为 Google 修复了使用 nestedScrollingEnabled (Issue 197932) 的崩溃问题

      【讨论】:

        【解决方案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);
        

        【讨论】:

        • 这是如何工作的?你能解释一下吗?
        • 当嵌套的RecyclerView被滚动时,不允许拦截父RecyclerView的触摸事件。
        【解决方案6】:

        试试这个。对于我的用例,它已经奏效了:

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

        【讨论】:

          【解决方案7】:

          试试下面的代码,希望能成功。

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

          【讨论】:

            【解决方案8】:

            我对单元格使用卡片视图,并在子适配器中使用子单元格 itemView setOnClickListener 停用父回收器视图滚动。

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

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

            【讨论】:

              【解决方案9】:

              尝试下面的代码滚动内部 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;
                      }
              
                  });
              

              【讨论】:

                【解决方案10】:

                IMO,您可以在外部 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

                希望对你有帮助!

                【讨论】:

                  【解决方案11】:

                  使用此代码关闭 recyclerview 上的滚动:

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

                  【讨论】:

                    【解决方案12】:

                    你应该这样做:

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

                    希望这会对你有所帮助。

                    【讨论】:

                    • 如果在onInterceptTouchEvent中返回false,onTouchEvent永远不会被调用
                    【解决方案13】:

                    问题存在于 Android 对 RecyclerView 的 onInterceptTouchEvent() 方法的实现中。 这个博客指出了这个问题并修复了它 - http://nerds.headout.com/fix-horizontal-scrolling-in-your-android-app/。 唯一的区别是父级垂直滚动,子级水平滚动。但该解决方案需要注意它也应该适用于您的情况。

                    【讨论】:

                    • 以防万一有人想知道,链接的文章现在给出了 404...但不要惊慌,它已在中国某处被镜像:tuicool.com/articles/ENbiqi
                    【解决方案14】:

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

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

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

                    【讨论】:

                      猜你喜欢
                      • 2017-04-12
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2018-09-14
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多