【问题标题】:Horizontal RecyclerView inside ListView not scrolling very smoothListView 内的水平 RecyclerView 滚动不流畅
【发布时间】:2015-12-24 06:54:08
【问题描述】:

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

但这不是很有效。事实上,我并没有注意到太大的改进。那是因为只有当运动完全水平时才会调用这个motionEvent——这种情况已经可以正常工作了。然后我尝试这样做:

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,它就不起作用。这也没有产生任何明显的改善。我还注意到一件事,这些方法在某种程度上也依赖于压力。当我用轻微的压力滑动时,什么也没发生。并且在相同的路径上执行相同的操作,更大的压力确实会产生滚动。

即使滑动路径不是水平直线或即使运动是圆形的,是否有任何方法可以使滚动平滑?任何帮助将不胜感激。

【问题讨论】:

  • 在 listview 中使用 recyclerview 来实现水平滚动的目的是什么?
  • @Brendon 我的布局是一个提要。它可以垂直滚动,ListView 中的每个项目都是一个画廊。并且画廊有一些可以水平滚动的帖子。
  • 所以你只需要水平滚动吗?只需在其中使用水平滚动来膨胀视图,它就会平滑而完美地滚动..换一种方式试试..
  • 我之前使用过 ScrollView。毫无疑问,滚动是好的。但是 Horizo​​ntalScrollView 没有视图回收,因此我曾经遇到内存不足错误。

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


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    onInterceptTouchEvent 很重要,因此 ListView 可以决定是否应该从子级窃取触摸事件。

    touch slop 很重要,因为它定义了在我们采取行动之前需要多少移动(在这种情况下是从孩子那里窃取事件)。那是因为只需用手指触摸屏幕(没有明显/预期的移动)就会生成 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;
    }
    

    【讨论】:

      【解决方案3】:

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

      【讨论】:

      • 当我为ListView重写上述方法时,RecyclerView的水平滚动是完美的。但是在RecyclerView 上向上或向下滚动(从RecyclerView 内部开始触摸)不会垂直滚动ListViewListView 仅当触摸从ListView 的某处开始时才会垂直滚动,例如,两个水平的RecyclerViews 之间有一些间隙。
      • 我编辑了我的答案,并添加了对 super.onInterceptTouchEvent 的调用。 ListView 的行为与 ScrollView 不同,我在内存中有 ScrollView.onInterceptTouchEvent :)
      • 它在大多数情况下都有效,但并不流畅。有时列表视图只是不动。
      【解决方案4】:

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

      【讨论】:

      • 我有同样的问题,我的观察是 setNestedScrollingEnabled(false) 修复了父 scrollview/recyclerview 的平滑度,但不修复子 scrollviews/recyclerviews 的平滑度
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-25
      • 2016-01-13
      • 1970-01-01
      相关资源
      最近更新 更多