在 Android 应用中,大部分情况下都会使用一个垂直滚动的 View 来显示内容(比如 ListView、RecyclerView 等)。但是有时候你还希望垂直滚动的View 里面的内容可以水平滚动。如果直接在垂直滚动的 View 里面使用水平滚动的 View,则滚动操作并不是很流畅。

比如下图中的示例:

为什么会出现这个问题呢?

上图中的布局为一个 RecyclerView 使用的是垂直滚动的 LinearLayoutManager 布局管理器,而里面每个 Item 为另外一个 RecyclerView 使用的是水平滚动的 LinearLayoutManager。而在 Android系统的事件分发 中,即使最上层的 View 只能垂直滚动,当用户水平拖动的时候,最上层的 View 依然会拦截点击事件。下面是 RecyclerView.java 中 onInterceptTouchEvent 的相关代码:

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {  
  ...
 
  switch (action) {
    case MotionEvent.ACTION_DOWN:
        ...
 
    case MotionEvent.ACTION_MOVE: {
        ...
 
        if (mScrollState != SCROLL_STATE_DRAGGING) {
          boolean startScroll = false;
          if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
            ...
            startScroll = true;
          }
          if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
            ...
            startScroll = true;
          }
          if (startScroll) {
            setScrollState(SCROLL_STATE_DRAGGING);
          }
      }
    } break;
      ...
 
  }
  return mScrollState == SCROLL_STATE_DRAGGING;
}
 

注意上面的 if 判断:

if(canScrollVertically && Math.abs(dy) > mTouchSlop) {...}  
 

RecyclerView 并没有判断用户拖动的角度, 只是用来判断拖动的距离是否大于滚动的最小尺寸。 如果是一个只能垂直滚动的 View,这样实现是没有问题的。如果我们在里面再放一个 水平滚动的 RecyclerView ,则就出现问题了。

可以通过如下的方式来修复该问题:

if(canScrollVertically && Math.abs(dy) > mTouchSlop && (canScrollHorizontally || Math.abs(dy) > Math.abs(dx))) {...}  
 

下面是一个完整的实现 BetterRecyclerView.java

public class BetterRecyclerView extends RecyclerView{
  private static final int INVALID_POINTER = -1;
  private int mScrollPointerId = INVALID_POINTER;
  private int mInitialTouchX, mInitialTouchY;
  private int mTouchSlop;
  public BetterRecyclerView(Contextcontext) {
    this(context, null);
  }
 
  public BetterRecyclerView(Contextcontext, @Nullable AttributeSetattrs) {
    this(context, attrs, 0);
  }
 
  public BetterRecyclerView(Contextcontext, @Nullable AttributeSetattrs, int defStyle) {
    super(context, attrs, defStyle);
    final ViewConfigurationvc = ViewConfiguration.get(getContext());
    mTouchSlop = vc.getScaledTouchSlop();
  }
 
  @Override
  public void setScrollingTouchSlop(int slopConstant) {
    super.setScrollingTouchSlop(slopConstant);
    final ViewConfigurationvc = ViewConfiguration.get(getContext());
    switch (slopConstant) {
      case TOUCH_SLOP_DEFAULT:
        mTouchSlop = vc.getScaledTouchSlop();
        break;
      case TOUCH_SLOP_PAGING:
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(vc);
        break;
      default:
        break;
    }
  }
 
  @Override
  public boolean onInterceptTouchEvent(MotionEvent e) {
    final int action = MotionEventCompat.getActionMasked(e);
    final int actionIndex = MotionEventCompat.getActionIndex(e);
 
    switch (action) {
      case MotionEvent.ACTION_DOWN:
        mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
        mInitialTouchX = (int) (e.getX() + 0.5f);
        mInitialTouchY = (int) (e.getY() + 0.5f);
        return super.onInterceptTouchEvent(e);
 
      case MotionEventCompat.ACTION_POINTER_DOWN:
        mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
        mInitialTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
        mInitialTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
        return super.onInterceptTouchEvent(e);
 
      case MotionEvent.ACTION_MOVE: {
        final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
        if (index < 0) {
          return false;
        }
 
        final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
        final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
        if (getScrollState() != SCROLL_STATE_DRAGGING) {
          final int dx = x - mInitialTouchX;
          final int dy = y - mInitialTouchY;
          final boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally();
          final boolean canScrollVertically = getLayoutManager().canScrollVertically();
          boolean startScroll = false;
          if (canScrollHorizontally && Math.abs(dx) > mTouchSlop && (Math.abs(dx) >= Math.abs(dy) || canScrollVertically)) {
            startScroll = true;
          }
          if (canScrollVertically && Math.abs(dy) > mTouchSlop && (Math.abs(dy) >= Math.abs(dx) || canScrollHorizontally)) {
            startScroll = true;
          }
          return startScroll && super.onInterceptTouchEvent(e);
        }
        return super.onInterceptTouchEvent(e);
      }
 
      default:
        return super.onInterceptTouchEvent(e);
    }
  }
}
 

其他问题

当用户快速滑动(fling)RecyclerView 的时候, RecyclerView 需要一段时间来确定其最终位置。 如果用户在快速滑动一个子的水平 RecyclerView,在子 RecyclerView 还在滑动的过程中,如果用户垂直滑动,则是无法垂直滑动的。原因是子 RecyclerView 依然处理了这个垂直滑动事件。

所以,在快速滑动后的滚动到静止的状态中,子 View 不应该响应滑动事件了,再次看看 RecyclerView 的 onInterceptTouchEvent() 代码:

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {  
    ...
 
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            ...
 
            if (mScrollState == SCROLL_STATE_SETTLING) {
                getParent().requestDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);
            }
 
            ...
    }
    return mScrollState == SCROLL_STATE_DRAGGING;
}
 

可以看到,当 RecyclerView 的状态为 SCROLL_STATE_SETTLING (快速滑动后到滑动静止之间的状态)时, RecyclerView 告诉父控件不要拦截事件。

同样的,如果只有一个方向固定,这样处理是没问题的。

针对我们这个嵌套的情况,父 RecyclerView 应该只拦截垂直滚动事件,所以可以这么修改父 RecyclerView:

public class FeedRootRecyclerView extends BetterRecyclerView{  
  public FeedRootRecyclerView(Contextcontext) {
    this(context, null);
  }
 
  public FeedRootRecyclerView(Contextcontext, @Nullable AttributeSetattrs) {
    this(context, attrs, 0);
  }
 
  public FeedRootRecyclerView(Contextcontext, @Nullable AttributeSetattrs, int defStyle) {
    super(context, attrs, defStyle);
  }
 
  @Override
  public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    /* do nothing */
  }
}
 

下图为最终的结果:

如果感兴趣可以下载 示例项目 ,注意示例项目中使用 kotlin,所以需要配置 kotlin 插件。

原文:http://nerds.headout.com/fix-horizontal-scrolling-in-your-android-app/

修复垂直滑动RecyclerView嵌套水平滑动RecyclerView水平滑动不灵敏问题的更多相关文章

  1. RecyclerView嵌套ScrollView导致RecyclerView内容显示不全

    我们在使用RecyclerView嵌套至ScrollView内的时候 RecyclerView不在屏幕内的数据会不显示出来,这里是一个坑,我们需要重写RecyclerView /** * Create ...

  2. Android RecyclerView(瀑布流)水平/垂直方向分割线

     Android RecyclerView(瀑布流)水平/垂直方向分割线 Android RecyclerView不像过去的ListView那样随意的设置水平方向的分割线,如果要实现Recycle ...

  3. RecyclerView借助ItemTouchHelper实现拖动和滑动删除功能

    RecyclerView是官方推荐代替ListView的空间,怎样实现RecyclerView列表元素的拖动呢? 官方提供了ItemTouchHelper类使用过程例如以下: 定义ItemTouchH ...

  4. ItemTouchHelper(实现RecyclerView上添加拖动排序与滑动删除的所有事情)

    简单介绍: ItemTouchHelper是一个强大的工具,它处理好了关于在RecyclerView上添加拖动排序与滑动删除的所有事情.它是RecyclerView.ItemDecoration的子类 ...

  5. Android RecyclerView嵌套RecyclerView

    原理 RecyclerView嵌套RecyclerView的条目,项目中可能会经常有这样的需求,但是我们将子条目设置为RecyclerView之后,却显示不出来.自己试了很久,终于找到了原因:必须先设 ...

  6. 关于RecyclerView嵌套导致item复用异常,界面异常的问题

    常规需求: 外层RecyclerView嵌套内层RecyclerView , 在上下滑动的时候会出现item数据以及view的显示异常. 解决办法: 1.重写  getItemViewType  方法 ...

  7. RecyclerView嵌套TextView时显示文字不全的解决方法之一

    先描述一下这个小bug:简单的TextView嵌套RecyclerView作为itemView时,可能会在文本中出现布局覆盖的现象,itemView的布局其实很简单,就是一个RelativeLayou ...

  8. Android NestedScrollView与RecyclerView嵌套,以及NestedScrollView不会滚动到屏幕顶部解决

    ①NestedScrollView与RecyclerView嵌套,导致滚动惯性消失 解决:mRecyclerView.setNestedScrollingEnabled(false); ②Nested ...

  9. 自定义RecyclerView.ItemDecoration,实现RecyclerView的分割线效果

    [转] 原文 自定义RecyclerView.ItemDecoration,实现RecyclerView的分割线效果 字数1598 阅读302 评论2 喜欢23 1.背景   RecyclerView ...

随机推荐

  1. spring+ibatis+多数据源

    环境:spring3.1+ibatis2.3.4+oracle+hbase要求:需要在工程中操作两个不同的数据源,一个是mssql,另一个是hbase.实现:    <bean id=" ...

  2. 1分钟实现Autodesk Vault登录对话框

      .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courie ...

  3. 微信小程序(应用号)开发资源汇总整理 - 一直更新中

    开源项目 wechat-weapp-gank - 微信小程序版Gank客户端 wechat-dribbble - 微信小程序-Dribbble wechatApp-demo - 微信小程序 DEMO ...

  4. 运维之Centos apache vsftpd配置

    安装Apache yum install httpd -y chkconfig httpd on service httpd start 配置一下iptables iptables -I INPUT ...

  5. 大数据计算平台Spark内核解读

    1.Spark介绍 Spark是起源于美国加州大学伯克利分校AMPLab的大数据计算平台,在2010年开源,目前是Apache软件基金会的顶级项目.随着 Spark在大数据计算领域的暂露头角,越来越多 ...

  6. JavaScript Patterns 5.6 Static Members

    Public Static Members // constructor var Gadget = function (price) { this.price = price; }; // a sta ...

  7. SSRS报表连接超时的问题

    这段时间遇到一个问题就是ReportService 中采用了远程连接的报表偶尔会断开连接,导致报表导出异常,查阅了很多资料,几天来就是断断续续的终于解决了这个问题,下面把一些解决的点一一展示出来,便于 ...

  8. Java NIO 同步非阻塞

    同步非阻塞IO (NIO) NIO是基于事件驱动思想的,实现上通常采用Reactor(http://en.wikipedia.org/wiki/Reactor_pattern)模式,从程序角度而言,当 ...

  9. .NET框架设计(常被忽视的框架设计技巧)

    阅读目录: 1.开篇介绍 2.元数据缓存池模式(在运行时构造元数据缓存池) 2.1.元数据设计模式(抽象出对数据的描述数据) 2.2.借助Dynamic来改变IOC.AOP动态绑定的问题 2.3.元数 ...

  10. jQuery简单入门(三)

    3.事件和动画 A. 事件 Aa. DOM加载 1.为什么使用jQuery加载DOM方法? 以javascript的window.onload()方法加载DOM的先决条件是:必须等待被请求页面的资源全 ...