欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

浅谈Android实践之ScrollView中滑动冲突处理解决方案

程序员文章站 2024-02-27 13:02:27
1. 前言  在android开发中,如果是一些简单的布局,都很容易搞定,但是一旦涉及到复杂的页面,特别是为了兼容小屏手机而使用了scrollview以后,就会...

1. 前言

 在android开发中,如果是一些简单的布局,都很容易搞定,但是一旦涉及到复杂的页面,特别是为了兼容小屏手机而使用了scrollview以后,就会出现很多点击事件的冲突,最经典的就是scrollview中嵌套了listview。我想大部分刚开始接触android的同学们都踩到过这个坑,这一篇文章就从最近做的一个项目讲起,然后在过程中提供一些解决冲突的思路。

2. 项目起始

项目有一个页面,涉及到了viewpager,mapview,listview,也就是说在一个页面中,会有这三个view,很明显,屏幕无法完全显示,需要scrollview来做一下支援,就引入了scrollview作为外层的容器。但是由于这个页面的数据展示需要做到用户手动下拉刷新,于是又引入了官方的swiperefreshlayout。

于是这个页面的布局就成了这样子。如下图(细节布局忽略)。

浅谈Android实践之ScrollView中滑动冲突处理解决方案

图-1 布局图

加入了scrollview和swiperefreshlayout之后引入了新的问题,就是各个控件之间的事件冲突,嵌套在scrollview中的viewpager、mapview、listview都需要能够正确的处理点击事件,特别是listview,需求要求它在scrollview中可以滑动,两种滑动混淆在一起,不是特别好处理。

问题提出来了,下面直接看解决思路。

3. 解决滑动冲突的思路

在viewgroup中有个方法叫requestdisallowintercepttouchevent(boolean disallowintercept),这个方法可以用来控制该viewgroup是否截断点击事件。我们解决滑动冲突的时候,其实就是在某个时机去调用这个方法,让父布局不截断点击事件,将点击事件传递到子view,让相关的子view去处理。 

下面就是关于在项目中处理各种点击事件冲突的一些例子和思考。处理的方法只是提供一种思路,可能并不是最优的方法,肯定存在其他思路的解决方案。 

以下处理滑动冲突的方案都是在子view的ontouchlistener里面进行处理,并没有去复写控件的点击事件处理过程,在使用中还是比较方便的。

3.1 mapview地图页面滑动冲突

mapview与scrollview的冲突主要在于,当用户点击到mapview地图并且滑动的时候,希望由地图map去处理点击事件,并包括后续的滑动事件、双手指缩放地图等等。 

在scrollview中,是会默认截断点击事件的,导致用户点击到地图后,地图基本是没有反应,更别谈双手指缩放地图了。 

用户手指点击到地图,并且滑动的时候,很难确定用户是要scrollview上下滑动还是操控地图内容滑动,所以我简单的认为,只要用户手指点击到地图,就是要对地图进行操作;当用户手指抬起,就认为用户不需要操作地图了。 

解决思路也很简单,就是在用户点击到地图或者滑动地图时候,让scrollview不截断点击事件,并传递给子view处理,也就是地图去处理点击事件;当用户手指抬起的时候,将scrollview的状态恢复至之前的状态,也就是scrollview可以截断点击事件。

我使用的是百度地图,直接上代码,更容易理解。

mmapview.getchildat(0).setontouchlistener(new view.ontouchlistener() {
      @override
      public boolean ontouch(view v, motionevent event) {
        if(event.getaction() == motionevent.action_up){
          //允许scrollview截断点击事件,scrollview可滑动
          mscrollview.requestdisallowintercepttouchevent(false);
        }else{
          //不允许scrollview截断点击事件,点击事件由子view处理
          mscrollview.requestdisallowintercepttouchevent(true);
        }
        return false;
      }
    });

3.2 viewpager滑动冲突解决

在这个项目中,viewpager在页面最顶层,如果只是scrollview里面嵌套了viewpager,因为这两个控件是不同方向的滑动事件,所以基本不会出现冲突。 

但是由于引入了swiperefreshlayout,我发现在滑动viewpager的时候,很容易就触发了swiperefreshlayout的下来刷新,进而有可能阻断了viewpager的左右滑动效果,体验很不好。而且在滑动viewpager的过程中,用户滑动肯定不是一直水平的,会有一定程度向上向下的滑动。 

viewpager处理冲突和地图处理冲突有些不同,因为当用户点击到viewpager,在滑动过程中,基本就可以猜测到用户是想左右滑动viewpager还是上下滑动scrollview(或者下拉刷新),这就不能像地图一样,在点击到viewpager就禁止scrollview截断点击事件(或者swiperefreshlayout下拉刷新功能),需要在滑动过程中做出判断。 

解决思路就是,设定一个阈值,一旦用户在x轴也就是横向滑动距离超过这个阈值,我就认为用户是要左右滑动viewpager,就禁止scrollview截断点击事件同时设置swiperefreshlayout不能下拉刷新。当用户抬起手指,就认为用户对viewpager的操作已经完毕,将scrollview和swiperefreshlayout状态恢复。

 mviewpager.setontouchlistener(new view.ontouchlistener() {
  @override
  public boolean ontouch(view v, motionevent event) {
    int action = event.getaction();

    if(action == motionevent.action_down) {
      // 记录点击到viewpager时候,手指的x坐标
      mlastx = event.getx();
    }
    if(action == motionevent.action_move) {
      // 超过阈值
      if(math.abs(event.getx() - mlastx) > 60f) {
        mrefreshlayout.setenabled(false);
        mscrollview.requestdisallowintercepttouchevent(true);
      }
    }
    if(action == motionevent.action_up) {
      // 用户抬起手指,恢复父布局状态
      mscrollview.requestdisallowintercepttouchevent(false);
      mrefreshlayout.setenabled(true);
    }
    return false;
  }
});

用户点击到viewpager时候,记录下点击位置的x坐标,当用户滑动过程中,如果在x轴上面的滑动超过阈值(我写的是60f,这个可以在实际使用中自行设置最佳的阈值),就禁止scrollview截断点击事件,同时设置不可下拉刷新。当用户手指离开屏幕,将scrollview和swiperefreshlayout的状态恢复。

3.3 listview滑动冲突解决

在scrollview中嵌套listview,会出现各种各样奇怪的问题。比如说listview显示有问题,可能才一两个item那么高,并没有完全的展开。网上流传解决这种问题的方法会有两种。

  • 根据展示数据的个数乘以每一个item的高度,计算出listview的总体高度,然后动态的设置listview的高度
  • 复写listview的onmeasure(int widthmeasurespec, int heightmeasurespec)方法,让listview完全展开

这两种方法都可以解决listview展示不完全的问题,而且也可以滑动(其实是使用scrollview的滑动效果),但是有一个最大的遗憾,就是listview里面的view不能复用了。因为这两种方法都是算出了listview的全部高度,然后将listview控件的高度设置成这个高度,这样的话,listview就相当于一个linearlayout的布局了,失去了复用view的优势,而且在某些场景可能还没有linearlayout好用,更甚的是,如果有大量图片的话,很容易就oom了,这是在研发过程中最不希望看见的。 

可以参考一下美团,美团的首页,就是一个scrollview,下滑的时候会发现,并不能无限向下滑动,到了底部会提醒跳转到一个二级页面去查看全部的团购信息。这是处理scrollview里面嵌套类似listview列表布局的时候的一种解决方案。
但是在我遇见的这个项目里面,并不能这样处理。 

上面的提到的两种解决思路很明确,如果想要listview正常展示就需要确定listview的高度,这个很重要。 

所以首先,我需要在布局文件中设置listview的高度,是一个明确的数值。设置高度之后,如果listview中的数据的item总高度超过listview所设置的高度,就可以复用view了。但是这只是解决了listview的显示问题,listview与scrollview的滑动冲突,并没有解决。 

要解决滑动的冲突,最主要的是确定禁止scrollview截断点击事件的时机,然后来分析有哪些时机。

  • scrollview在未滑动到底部时候,如果点击并滑动listview时候,listview是不能滑动的,不禁止。
  • 如果scrollview滑动到底部,且listview已经到顶部,继续下拉listview,其实会拉动scrollview,不禁止。
  • 如果scrollview滑动到底部,用户向上滑,listview滑动,禁止scrollview截断点击事件能力

很明显,在判断禁止scrollview截断点击事件时机的时候,需要知道scrollview是否滑动到了底部。于是,重写了scrollview的scrollchanged()方法,来判断scrollview是否滑动到底部(sdk api 23版本中scrollview可以设置setonscrollchangelistener()来监听滑动的变化,但是之前版本不支持,为了兼容,自己需要重写)。

@override
protected void onscrollchanged(int l, int t, int oldl, int oldt){
  super.onscrollchanged(l,t,oldl,oldt);
  // 滑动的距离加上本身的高度与子view的高度对比
  if(t + getheight() >= getchildat(0).getmeasuredheight()){
    // scrollview滑动到底部
    if(monscrolltobottomlistener != null) {
      monscrolltobottomlistener.onscrolltobottom();
    }
  } else {
    if(monscrolltobottomlistener != null) {
      monscrolltobottomlistener.onnotscrolltobottom();
    }
  }
}

public void setscrolltobottomlistener(onscrolltobottomlistener listener) {
  this.monscrolltobottomlistener = listener;
}

public interface onscrolltobottomlistener {
  void onscrolltobottom();
  void onnotscrolltobottom();
}

有了思路,而且scrollview滑动到底部的标识也可以拿到,下面就可以直接来解决滑动冲突了,直接看代码。

mscrollview.setscrolltobottomlistener(new bottomscrollview.onscrolltobottomlistener() {
  @override
  public void onscrolltobottom() {
    issvtobottom = true;
  }

  @override
  public void onnotscrolltobottom() {
    issvtobottom = false;
  }
});

mlistview.setontouchlistener(new view.ontouchlistener() {
  @override
  public boolean ontouch(view v, motionevent event) {
    int action = event.getaction();

    if(action == motionevent.action_down) {
      mlasty = event.gety();
    }
    if(action == motionevent.action_move) {
      int top = mlistview.getchildat(0).gettop();
      float nowy = event.gety();
      if(!issvtobottom) {
        // 允许scrollview拦截点击事件, scrollview滑动
        mscrollview.requestdisallowintercepttouchevent(false);
      } else if(top == 0 && nowy - mlasty > threshold_y_list_view) {
        // 允许scrollview拦截点击事件, scrollview滑动
        mscrollview.requestdisallowintercepttouchevent(false);
      } else {
        // 不允许scrollview拦截点击事件, listview滑动
        mscrollview.requestdisallowintercepttouchevent(true);
      }
    }
    return false;
  }
});

相对于其他的控件来说,listview和scrollview之间的滑动冲突更难解决,但其实在实际使用中并不推荐scrollview里面嵌套listview,一旦业务复杂,很容易出现各种ui和业务逻辑冲突的错误。

4. 运行效果

由于地图加入比较麻烦,所以在demo中并没有引入地图。看一下运行效果。

浅谈Android实践之ScrollView中滑动冲突处理解决方案

图-2 运行效果

5. 总结

本篇文章只是提供一种解决方法的思路,在具体的场景下,交互往往是贴合具体业务需求的。但是不管怎么样,找出点击事件截断和处理的时机是最重要的,围绕这个关键点,总能找出相应的解决方法。

附上demo工程地址:demo

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

上一篇: 简单计算器

下一篇: 日期计算器