ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

aqvz7021 8年前
   <h2><strong>ViewPager,ScrollView 嵌套ViewPager滑动冲突解决</strong></h2>    <p>本篇主要讲解一下几个问题</p>    <ul>     <li> <p>粗略地介绍一下View的事件分发机制</p> </li>     <li> <p>解决事件滑动冲突的思路及方法</p> </li>     <li> <p>ScrollView 里面嵌套ViewPager导致的滑动冲突</p> </li>     <li> <p>ViewPager里面嵌套ViewPager 导致的滑动冲突</p> </li>     <li> <p>轮播图的几种实现方式</p> </li>    </ul>    <h2><strong>先看一下效果图</strong></h2>    <h3><strong>ScrollView里面嵌套ViewPager</strong></h3>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9ef83325ac784038a317715a9a8ceaa7.gif"></p>    <h3><strong>ViewPager里面嵌套ViewPager</strong></h3>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a4e1bd9ee3848909479e9414725779ad.gif"></p>    <h2><strong>View的 事件分发机制</strong></h2>    <p>这篇博客大打算详细讲解View的事件分发机制,因为网上已经出现了一系列的好 文章,我自己的水平也有限,目前肯定写得不咋的。</p>    <p>先啰嗦一下,View 的事件分发机制主要涉及到一下三个 方法</p>    <ul>     <li> <p>dispatchTouchEvent ,这个方法主要是用来分发事件的</p> </li>     <li> <p>onInterceptTouchEvent,这个方法主要是用来拦截事件的(需要注意的是ViewGroup才有这个方法,View没有onInterceptTouchEvent这个方法</p> </li>     <li> <p>onTouchEvent这个方法主要是用来处理事件的</p> </li>     <li> <p>requestDisallowInterceptTouchEvent(true),这个方法能够影响父View是否拦截事件,true表示 不拦截事件,false表示拦截事件</p> </li>    </ul>    <h3><strong>下面引用 图解 Android 事件分发机制 这一篇博客的内容</strong></h3>    <p style="text-align:center"><img src="https://simg.open-open.com/show/68c904baa4b8a8df491c9f6e49e61755.png"></p>    <ul>     <li> <p>仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View</p> </li>     <li> <p>事件从左上角那个白色箭头开始,由Activity的dispatchTouchEvent做分发</p> </li>     <li> <p>箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。</p> </li>     <li> <p>dispatchTouchEvent和 onTouchEvent的框里有个【true---->消费】的字,表示的意思是如果方法返回true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。</p> </li>     <li> <p>目前所有的图的事件是针对ACTION_DOWN的,对于ACTION_MOVE和ACTION_UP我们最后做分析。</p> </li>     <li> <p>之前图中的Activity 的dispatchTouchEvent 有误(图已修复),只有return super.dispatchTouchEvent(ev) 才是往下走,返回true 或者 false 事件就被消费了(终止传递)。</p> </li>    </ul>    <h3><strong>总结</strong></h3>    <p>当TouchEvent发生时,首先Activity将TouchEvent传递给最顶层的View,TouchEvent最先到达最顶层 view 的 dispatchTouchEvent ,然后由 dispatchTouchEvent 方法进行分发,</p>    <ul>     <li>如果dispatchTouchEvent返回true 消费事件,事件终结。</li>     <li> <p>如果dispatchTouchEvent返回 false ,则回传给父View的onTouchEvent事件处理;</p> <p>onTouchEvent事件返回true,事件终结,返回false,交给父View的OnTouchEvent方法处理</p> </li>     <li> <p>如果dispatchTouchEvent返回super的话,默认会调用自己的onInterceptTouchEvent方法</p> <p>默认的情况下interceptTouchEvent回调用super方法,super方法默认返回false,所以会交给子View的onDispatchTouchEvent方法处理</p> <p>如果 interceptTouchEvent 返回 true ,也就是拦截掉了,则交给它的 onTouchEvent 来处理,</p> <p>如果 interceptTouchEvent 返回 false ,那么就传递给子 view ,由子 view 的 dispatchTouchEvent 再来开始这个事件的分发。</p> </li>    </ul>    <h2><strong>解决事件滑动冲突的思路及方法</strong></h2>    <h3><strong>常见的三种情况</strong></h3>    <p>第一种情况,滑动方向不同</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cb62d3ebc30b798dda3bea724e193732.jpg"></p>    <p>第二种情况,滑动方向相同</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5f6b21805ce7211a61cd9c8e166845d2.jpg"></p>    <p>第三种情况,上述两种情况的嵌套</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fecba5ef9b3513c99d572b5f546162b4.jpg"></p>    <h3><strong>解决思路</strong></h3>    <p>看了上面三种情况,我们知道他们的共同特点是父View 和子View都想争着响应我们的触摸事件,但遗憾的是我们的触摸事件 同一时刻只能被某一个View或者ViewGroup拦截消费,所以就产生了滑动冲突?那既然同一时刻只能由某一个View或者ViewGroup消费拦截,那我们就只需要 决定在某个时刻由这个View或者ViewGroup拦截事件,另外的 某个时刻 有另外一个View或者ViewGroup拦截事件不就OK了吗?综上,正如 在 <strong> <em>《Android开发艺术》</em> </strong> 一书提出的,总共 有两种接觉方案</p>    <p>下面的两种方法针对第一种情况(滑动方向不同),父View是上下滑动,子View是左右滑动的情况。</p>    <h3><strong>外部解决法</strong></h3>    <p>从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截,不要的时候返回false,为代码大概 如下</p>    <pre>  <code class="language-java">@Override  public boolean onInterceptTouchEvent(MotionEvent ev) {      final float x = ev.getX();      final float y = ev.getY();        final int action = ev.getAction();      switch (action) {          case MotionEvent.ACTION_DOWN:              mDownPosX = x;              mDownPosY = y;                break;          case MotionEvent.ACTION_MOVE:              final float deltaX = Math.abs(x - mDownPosX);              final float deltaY = Math.abs(y - mDownPosY);              // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截              if (deltaX > deltaY) {                  return false;              }      }        return super.onInterceptTouchEvent(ev);  }</code></pre>    <h3><strong>内部解决法</strong></h3>    <p>从子View左右,父View先不要拦截任何事件,所有的 事件传递给 子View,如果子View需要此事件就消费掉,不需要此事件的话就交给 父View处理。</p>    <p>实现思路 如下,重写子 View的dispatchTouchEvent方法,在Action_down 动作中通过方法 requestDisallowInterceptTouchEvent(true) 先请求 父 View不要拦截事件,这样保证 子View能够 接受到Action_move事件,再在Action_move动作中根据 自己的逻辑是否要拦截事件,不要的 话交给 父View处理</p>    <pre>  <code class="language-java">@Override  public boolean dispatchTouchEvent(MotionEvent ev) {      int x = (int) ev.getRawX();      int y = (int) ev.getRawY();      int dealtX = 0;      int dealtY = 0;        switch (ev.getAction()) {          case MotionEvent.ACTION_DOWN:              dealtX = 0;              dealtY = 0;              // 保证子View能够接收到Action_move事件              getParent().requestDisallowInterceptTouchEvent(true);              break;          case MotionEvent.ACTION_MOVE:              dealtX += Math.abs(x - lastX);              dealtY += Math.abs(y - lastY);              Log.i(TAG, "dealtX:=" + dealtX);              Log.i(TAG, "dealtY:=" + dealtY);              // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截              if (dealtX >= dealtY) {                  getParent().requestDisallowInterceptTouchEvent(true);              } else {                  getParent().requestDisallowInterceptTouchEvent(false);              }              lastX = x;              lastY = y;              break;          case MotionEvent.ACTION_CANCEL:              break;          case MotionEvent.ACTION_UP:              break;        }      return super.dispatchTouchEvent(ev);  }</code></pre>    <h2><strong>ScrollView 里面嵌套ViewPager导致的滑动冲突</strong></h2>    <h3>外部解决法</h3>    <p>如上面所述,从 父ViewScrollView着手,重写 OnInterceptTouchEvent方法,在上下滑动的时候拦截事件,在左右滑动的时候不拦截事件,返回 false,这样确保子View 的dispatchTouchEvent方法会被调用,代码 如下</p>    <pre>  <code class="language-java">/**   * @ explain:这个ScrlloView不拦截水平滑动事件,   * 是用来解决 ScrollView里面嵌套ViewPager使用的   * @ author:xujun on 2016/10/25 15:28   * @ email:gdutxiaoxu@163.com   */  public class VerticalScrollView extends ScrollView {        public VerticalScrollView(Context context) {          super(context);      }        public VerticalScrollView(Context context, AttributeSet attrs) {          super(context, attrs);      }        public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {          super(context, attrs, defStyleAttr);      }        @TargetApi(21)      public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int              defStyleRes) {          super(context, attrs, defStyleAttr, defStyleRes);      }        private float mDownPosX = 0;      private float mDownPosY = 0;        @Override      public boolean onInterceptTouchEvent(MotionEvent ev) {          final float x = ev.getX();          final float y = ev.getY();            final int action = ev.getAction();          switch (action) {              case MotionEvent.ACTION_DOWN:                  mDownPosX = x;                  mDownPosY = y;                    break;              case MotionEvent.ACTION_MOVE:                  final float deltaX = Math.abs(x - mDownPosX);                  final float deltaY = Math.abs(y - mDownPosY);                  // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截                  if (deltaX > deltaY) {                      return false;                  }          }            return super.onInterceptTouchEvent(ev);      }  }</code></pre>    <h3><strong>内部解决法</strong></h3>    <p>如上面上述,通过requestDisallowInterceptTouchEvent(true)方法来影响父View是否拦截事件,我们通过重写ViewPager的 dispatchTouchEvent()方法,在左右滑动的时候请求父View ScrollView不要拦截事件,其他的时候拦截事件</p>    <pre>  <code class="language-java">/**   * @ explain:这个 ViewPager是用来解决ScrollView里面嵌套ViewPager的 内部解决法的   * @ author:xujun on 2016/10/25 16:38   * @ email:gdutxiaoxu@163.com   */  public class MyViewPager extends ViewPager {        private static final String TAG = "xujun";        int lastX = -1;      int lastY = -1;        public MyViewPager(Context context) {          super(context);      }        public MyViewPager(Context context, AttributeSet attrs) {          super(context, attrs);      }        @Override      public boolean dispatchTouchEvent(MotionEvent ev) {          int x = (int) ev.getRawX();          int y = (int) ev.getRawY();          int dealtX = 0;          int dealtY = 0;            switch (ev.getAction()) {              case MotionEvent.ACTION_DOWN:                  dealtX = 0;                  dealtY = 0;                  // 保证子View能够接收到Action_move事件                  getParent().requestDisallowInterceptTouchEvent(true);                  break;              case MotionEvent.ACTION_MOVE:                  dealtX += Math.abs(x - lastX);                  dealtY += Math.abs(y - lastY);                  Log.i(TAG, "dealtX:=" + dealtX);                  Log.i(TAG, "dealtY:=" + dealtY);                  // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截                  if (dealtX >= dealtY) {                      getParent().requestDisallowInterceptTouchEvent(true);                  } else {                      getParent().requestDisallowInterceptTouchEvent(false);                  }                  lastX = x;                  lastY = y;                  break;              case MotionEvent.ACTION_CANCEL:                  break;              case MotionEvent.ACTION_UP:                  break;            }          return super.dispatchTouchEvent(ev);      }  }</code></pre>    <h2><strong>注意事项(坑)</strong></h2>    <p>当我们ScrollView的最上层的Layout里面多多个孩子的时候,当下面一个孩子是RecyclerView或者ListView的时候,往往会活动滑动到ListView或者RecyclerView 的第一个item,导致进入界面的时候会导致RecyclerView 上面的 View被滑动到界面意外,看不见,这时候的用户体验是比较差的</p>    <p>即结构如下面的时候</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d90bf63c998297559b34f6104ed758a4.jpg"></p>    <h3><strong>在Activity中的相关解决方法</strong></h3>    <p>于是我查找了相关的资料,在Activity中完美解决,主要要一下两种方法</p>    <p>第一种方法,重写Activity的onWindowFocusChanged()方法,在里面调用mNoHorizontalScrollView.scrollTo(0,0);方法,滑动到顶部,因为onWindowFocusChanged是在所有View绘制完毕的时候才会回调的,不熟悉的话建议先回去看一下Activity的生命周期的相关介绍</p>    <pre>  <code class="language-java">private void scroll() {      mNoHorizontalScrollView.scrollTo(0,0);  }    @Override  public void onWindowFocusChanged(boolean hasFocus) {      super.onWindowFocusChanged(hasFocus);      if(hasFocus  && first){          first=false;          scroll();      }  }</code></pre>    <p>第二种解决方法,调用RecyclerView上面的View的一下方法,让其获取焦点</p>    <pre>  <code class="language-java">view.setFocusable(true);    view.setFocusableInTouchMode(true);    view.requestFocus();</code></pre>    <p>这段代码在初始化的时候就让该界面的顶部的某一个控件获得焦点,滚动条自然就显示到顶部了。</p>    <h3><strong>在Fragment中的相关解决方法</strong></h3>    <p>同样是调用第二种方法,调用RecyclerView上面的View的一下方法,让其获取焦点</p>    <pre>  <code class="language-java">view.setFocusable(true);    view.setFocusableInTouchMode(true);    view.requestFocus();</code></pre>    <p>这段代码在初始化的时候就让该界面的顶部的某一个控件获得焦点,滚动条自然就显示到顶部了。但是给方法存在缺点,就是当我们上面的view如果滑动到一半的时候,切换到下一个Fragment,在切换回来的时候,RecyclerView的第一个item会自动滑动到顶部。目前我还没有找到相对比较好的解决这个问题的方法,大家知道相关解决方法的话也欢迎联系我,可以加我 微信或者在留言区评论,谢谢</p>    <p>个人疑点</p>    <p>借鉴于解决Activity的方法,目前我还没有找到一个方法是在Fragemnt界面完全绘制完毕以后回调的方法,如果大家知道怎样处理的 话,欢迎大家提出来</p>    <h2><strong>ViewPager里面嵌套ViewPager导致的滑动冲突</strong></h2>    <h3><strong>内部解决法</strong></h3>    <p>从子View ViewPager着手,重写 子View的 dispatchTouchEvent方法,在子 View需要拦截的时候进行拦截,否则交给父View处理,代码如下</p>    <pre>  <code class="language-java">public class ChildViewPager extends ViewPager {        private static final String TAG = "xujun";      public ChildViewPager(Context context) {          super(context);      }        public ChildViewPager(Context context, AttributeSet attrs) {          super(context, attrs);      }        @Override      public boolean dispatchTouchEvent(MotionEvent ev) {          int curPosition;            switch (ev.getAction()) {              case MotionEvent.ACTION_DOWN:                  getParent().requestDisallowInterceptTouchEvent(true);                  break;              case MotionEvent.ACTION_MOVE:                  curPosition = this.getCurrentItem();                  int count = this.getAdapter().getCount();                  Log.i(TAG, "curPosition:=" +curPosition);                  // 当当前页面在最后一页和第0页的时候,由父亲拦截触摸事件                  if (curPosition == count - 1|| curPosition==0) {                      getParent().requestDisallowInterceptTouchEvent(false);                  } else {//其他情况,由孩子拦截触摸事件                      getParent().requestDisallowInterceptTouchEvent(true);                  }            }          return super.dispatchTouchEvent(ev);      }  }</code></pre>    <h3><strong>外部解决法</strong></h3>    <p>这个如果要采用内部解决法来解决的话想,相对很麻烦,我提一下自己的个人思路,我们可以先测量子View在哪个区域,然后我们在根据我们按下的点是否在区域以内,如果是的话,在根据子View时候需要拦截进行处理</p>    <h2><strong>讨论</strong></h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/166254ae88eb456c0976bdd8645e575b.png"></p>    <p>对于这种效果,上面是轮播图的,下面是RecyclerView或者ListView的,一般有一下几种实现方式</p>    <ul>     <li> <p>使用我们上述提高的ScrollView里面嵌套ViewPager和RecyclerView,这种实现方式需要自己解决View滑动事件的冲突,同时还有我在上述提高的在Fragment中存在的问题</p> </li>     <li> <p>使用listView的addHeaderView来实现,或者是通过多种不同的item来实现</p> </li>     <li> <p>使用RecyclerView添加headerView来实现,或者复用多种不同的item来实现。</p> </li>     <li> <p>使用SupportLibrary中的CoordinatorLayout等控件</p> </li>    </ul>    <p>其布局文件如下,Activity代码见项目中的SixActivity</p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>    <android.support.design.widget.CoordinatorLayout      xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:app="http://schemas.android.com/apk/res-auto"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:background="@android:color/background_light"      android:fitsSystemWindows="true"  >        <android.support.design.widget.AppBarLayout          android:layout_width="match_parent"          android:layout_height="300dp"          android:fitsSystemWindows="true"          android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"        >              <android.support.design.widget.CollapsingToolbarLayout              android:layout_width="match_parent"              android:layout_height="match_parent"              app:layout_scrollFlags="scroll|snap">                  <android.support.v4.view.ViewPager                  android:id="@+id/viewPager"                  android:layout_width="match_parent"                  android:layout_height="match_parent"                >                </android.support.v4.view.ViewPager>                <TextView                  android:id="@+id/tv_page"                  android:layout_width="match_parent"                  android:layout_height="wrap_content"                  android:layout_gravity="bottom"                  android:gravity="right"                  android:text="1/10"                  android:textColor="#000"/>            </android.support.design.widget.CollapsingToolbarLayout>        </android.support.design.widget.AppBarLayout>          <android.support.v7.widget.RecyclerView          android:id="@+id/recyclerView"          android:layout_width="match_parent"          android:layout_height="match_parent"          app:layout_behavior="@string/appbar_scrolling_view_behavior">        </android.support.v7.widget.RecyclerView>      </android.support.design.widget.CoordinatorLayout></code></pre>    <h2><strong>总结</strong></h2>    <ul>     <li> <p>当我们滑动方向不同的时候,采用外部解决法和内部解决法,复杂度差不多。</p> </li>     <li> <p>当我们滑动的方向相同的话,建议采用内部解决法来解决,因为采用外部解决法复杂度比较高。而且有时候我们是采用别人的开源控件,这时候去修改别人的源码可能会发生一些意想不到的bug。</p> </li>    </ul>    <h2><strong>题外话</strong></h2>    <ul>     <li> <p>在这篇文章的最后提高的实现轮播图+list列表的几种实现形式,刚开始是不想写的,后面因为ScrollView里面嵌套ViewPager和RecyclerView在fragment中RecyclerView抢占焦点,在某些情况下用户体验不好,才写出来的,跟这篇博客要讲解的View滑动事件冲突没有多大关系,只是给读者提供多种思路而已</p> </li>     <li> <p>至于CoordinatorLayout,是google IO 2015中提出来的,功能很强大,可以说是专门为了解决嵌套导滑动而产生的,极大地方便了开发者,对于初学者,可以暂时不必掌握它,先把其他的基础学好就好</p> </li>     <li> <p>同时买一下广告,欢迎大家到我的github上面star或者fork,谢谢</p> </li>    </ul>    <p>参考文章: <a href="/misc/goto?guid=4959675424536518722" rel="nofollow,noindex">图解 Android 事件分发机制</a></p>    <p> </p>    <p>来自:http://www.jianshu.com/p/fd528c2311da</p>    <p> </p>