android中view手势滑动冲突的解决方法

编辑作者:www.789mux.com    在线用户:58    标签: sp    元素   

在开发中,简单的布局很容易解决,但是当涉及到复杂的页面时,特别是当滚动视图用于与小屏幕手机的兼容性时,点击事件会发生很多冲突,今天爱站技术频道为大家带来android中view手势滑动冲突的解决方法。

Android手势事件的冲突跟点击事件的分发过程息息相关,由三个重要的方法来共同完成,分别是:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。

public boolean dispatchTouchEvent(MotionEvent ev)

这个方法用来进行事件的分发。如果事件传递到view,那么这个方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

   

 public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法内部调用,用来判断是拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

 public boolean onTouchEvent(MotionEvent event)

 在dispathcTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接到事件。

例:

  public boolean dispatchTouchEvent(MotionEvent ev){

       boolean consume = false;

      if(onInterceptTouchEvent(ev)){

          consume = onTouchEvent(ev);

       }  else {

         consum = child.dispathcTouchEvent(ev);

      }

    return consume;

   }

手势冲突的解决方法就是用上面的三个方法;主要分为两种解决方法:·1外部拦截法 2内部拦截法

1.常见的滑动冲突场景

1.1 外部滑动方向和内部滑动的方向不一致
android中view手势滑动冲突的解决方法-第1张图片

这种情况我们经常遇见,比如使用viewpaper+listview时,在这种效果中,可以通过左右滑动切换页面,而每一个页面往往又是一个listview,本来在这种情况下是有冲突的,但是Viewpaper内部处理了这个滑动冲突,因此采用viewpaper我们无需关注这个问题,如果我们采用的不是Viewpaper而是ScrollView等,那么必须手动处理滑动冲突,否则内外两层只能有一层滑动,那就是滑动冲突。另外内部左右滑动,外部上下滑动也同样属于该类。

1.2 外部滑动方向和内部滑动方向一致
android中view手势滑动冲突的解决方法-第2张图片

这种情况就比较复杂,当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题,因为当手指开始滑动的时候,系统无法知道用户到底是想让那一层动,所以当手指滑动的时候就会出现问题,要么只能一层动,要么内外两成动的都很卡顿。

2.给出解决方案

2.1 外部拦截法

针对场景1,我们可以发现外部和内部的滑动方向不一样也就是说只要判断当前dy和dx的大小,如果dy>dx,那么当前就是竖直滑动,否则就是水平滑动。明确了这个我就就可以根据当前的手势开始拦截了。
android中view手势滑动冲突的解决方法-第3张图片


从上一节中我们分析了view的事件分发,我们知道点击事件的分发顺序是 通过父布局分发,如果父布局没有拦截,即onInterceptTouchEvent返回false,才会传递给子View。所以我们就可以利用onInterceptTouchEvent()这个方法来进行事件的拦截。来看一下代码:

 public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
      intercepted = false;
      break;
    }
    case MotionEvent.ACTION_MOVE: {
      if(父容器拦截的规则){
        intercepted=true;
      }else{
        intercepted=false;
      }
      break;
    }
    case MotionEvent.ACTION_UP: {
      intercepted = false;
      break;
    }
    default:
      break;
    }
    mLastXIntercept=x;
    mLastYIntercept=y;
    return intercepted;
  }

上面的代码差多就是外部拦截的通用模板了,在onInterceptTouchEvent方法中,

首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截事件,因为一旦父容器拦截了ACTION_DOWN这个事件,那么后续的ACTION_MOVE和ACTION_UP事件将直接交给父容器处理,这个时候事件没法继续传递给子元素了;

然后是ACTION_MOVE这个事件,这个事件可以根据需要决定是否拦截,如果父容器需要拦截就返回true,否则返回false;

最后是ACTION_UP这个事件,这里必须返回false,因为这个事件本身也没有太多意义。

下面我们来具体做一下拦截的操作,我们需要在水平滑动的时候父容器拦截事件。

public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
      intercepted = false;
      break;
    }
    case MotionEvent.ACTION_MOVE: {
      int deltaX=x-mLastXIntercept;
      int deltaY=y=mLastYIntercept;
      if(Math.abs(deltaX)>Math.abs(deltaY)){
        intercepted=true;
      }else{
        intercepted=false;
      }
      break;
    }
    case MotionEvent.ACTION_UP: {
      intercepted = false;
      break;
    }
    default:
      break;
    }
    mLastXIntercept=x;
    mLastYIntercept=y;
    return intercepted;
  }

 从上面的代码来看,我们只是修改了一下拦截条件而已,所以说外部拦截还是很简单方便的。在滑动的过程中,当水平方向的距离大时就判定水平滑动。

还是一贯我们做实验来证明理论的风格,我们来自定义一个HorizontalScrollView来体现一下用外部拦截法解决冲突的快感。

先上一下代码:

package com.gxl.viewtest;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.text.LoginFilter;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * s
 * Created by GXL on 2016/7/25 0025.
 */
public class HorizontalScrollView extends ViewGroup {

  private final String TAG = "HorizontalScrollView";
  private VelocityTracker mVelocityTracker;
  private Scroller mScroller;
  private int mChildrenSize;
  private int mChildWidth;
  private int mChildIndex;
  //上次滑动的坐标
  private int mLastX = 0;
  private int mLastY = 0;
  //上次上次拦截滑动的坐标
  private int mLastXIntercept = 0;
  private int mLastYIntercept = 0;

  public HorizontalScrollView(Context context) {
    super(context);
    init(context);
  }

  public HorizontalScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
  }

  public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
  }

  public void init(Context context) {
    mVelocityTracker = VelocityTracker.obtain();
    mScroller = new Scroller(context);
  }

  public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN: {
        intercepted = false;
        break;
      }
      case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastXIntercept;
        int deltaY = y - mLastYIntercept;
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
          intercepted = true;
        } else {
          intercepted = false;
        }
        break;
      }
      case MotionEvent.ACTION_UP: {
        intercepted = false;
        break;
      }
      default:
        break;
    }
    mLastX = x;
    mLastY = y;
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    mVelocityTracker.addMovement(event);
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        break;
      case MotionEvent.ACTION_MOVE:
        int deltaX = x - mLastX;
        if((getScrollX()-deltaX)>=0&&(getScrollX()-deltaX)<=(getMeasuredWidth()-ScreenUtils.getScreenWidth(getContext()))) {
          scrollBy(-deltaX, 0);
        }
        break;
      case MotionEvent.ACTION_UP:
        mVelocityTracker.computeCurrentVelocity(1000);
        float xVelocityTracker = mVelocityTracker.getXVelocity();
        if (Math.abs(xVelocityTracker) > 50) {
          if (xVelocityTracker > 0) {
            Log.i(TAG, "快速向右划");
          } else {
            Log.i(TAG, "快速向左划");
          }
        }
        mVelocityTracker.clear();
        break;
    }
    mLastX = x;
    mLastY = y;
    return true;
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measuredWidth = 0;
    int measureHeight = 0;
    final int childCount = getChildCount();
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);

    if (childCount == 0) {
      setMeasuredDimension(0, 0);
    } else if (heightSpaceMode == MeasureSpec.AT_MOST && widthSpaceMode == MeasureSpec.AT_MOST) {
      final View childView = getChildAt(0);
      measuredWidth = childView.getMeasuredWidth() * childCount;
      measureHeight = childView.getMeasuredHeight();
      setMeasuredDimension(measuredWidth, measureHeight);
    } else if (heightSpaceMode == MeasureSpec.AT_MOST) {
      measureHeight = getChildAt(0).getMeasuredHeight();
      setMeasuredDimension(widthSpaceSize, measureHeight);
    } else if (widthSpaceMode == MeasureSpec.AT_MOST) {
      final View childView = getChildAt(0);
      measuredWidth = childView.getMeasuredWidth() * childCount;
      setMeasuredDimension(measuredWidth, heightSpaceSize);
    }
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    Log.i(TAG, "onLayout: " + getMeasuredWidth());
    int childleft = 0;
    final int childCount = getChildCount();
    mChildrenSize = childCount;
    for (int i = 0; i < mChildrenSize; i++) {
      final View childView = getChildAt(i);
      if (childView.getVisibility() != View.GONE) {
        final int childWidth = childView.getMeasuredWidth();
        mChildWidth = childWidth;
        childView.layout(childleft, 0, childleft + mChildWidth, childView.getMeasuredHeight());
        childleft += childWidth;
      }
    }
  }

  private void smoothScrollTo(int destX,int destY)
  {
    int scrollX=getScrollX();
    int delta=destX-scrollX;
    mScroller.startScroll(scrollX,0,delta,0,1000);
  }

  @Override
  public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
      postInvalidate();
    }
  }
}

再来看一下布局文件
  

 <com.gxl.viewtest.HorizontalScrollView
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:background="#00ff00"
    >

    <ListView
      android:id="@+id/listview1"
      android:layout_width="600dp"
      android:layout_height="match_parent"
      android:background="@color/colorPrimary"
      >
    </ListView>

    <ListView
      android:id="@+id/listview2"
      android:layout_width="600dp"
      android:layout_height="match_parent"
      android:background="@color/colorAccent"
      >
    </ListView>

    <ListView
      android:id="@+id/listview3"
      android:layout_width="600dp"
      android:layout_height="match_parent"
      android:background="#ff0000"
      >
    </ListView>

  </com.gxl.viewtest.HorizontalScrollView>

以上就是外部处理滑动冲突的代码,认真看一下,思路还是很清晰的。里面还涉及了一些自定义View的知识,我会在后面的博文中认真分析一下代码,你先看一下onInterceptTouchEvent处理滑动冲突的部分。
看一下效果图哈。
android中view手势滑动冲突的解决方法-第4张图片

2.2 内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器去处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,这个方法的大体解释就是:

requestDisallowInterceptTouchEvent是ViewGroup类中的一个公用方法,参数是一个boolean值,官方介绍如下

Called when a child does not want this parent and its ancestors to intercept touch events with ViewGroup.onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.

android系统中,一次点击事件是从父view传递到子view中,每一层的view可以决定是否拦截并处理点击事件或者传递到下一层,如果子view不处理点击事件,则该事件会传递会父view,由父view去决定是否处理该点击事件。在子view可以通过设置此方法去告诉父view不要拦截并处理点击事件,父view应该接受这个请求直到此次点击事件结束。

使用起来外部拦截事件略显复杂一点。下面我也先来看一下它的通用模板(注意下面的代码是定义在子View中的):

public boolean onInterceptTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
      parent.requestDisallowInterceptTouchEvent(true); //父布局不要拦截此事件
      break;
    }
    case MotionEvent.ACTION_MOVE: {
      int deltaX=x-mLastXIntercept;
      int deltaY=y=mLastYIntercept;
      if(父容器需要拦截的事件){
        parent.requestDisallowInterceptTouchEvent(false); //父布局需要要拦截此事件
      }
      break;
    }
    case MotionEvent.ACTION_UP: {
      intercepted = false;
      break;
    }
    default:
      break;
    }
    mLastXIntercept=x;
    mLastYIntercept=y;
    return super.dispathTouchEvent(event);
  }

上面的代码是android中view手势滑动冲突的解决方法,当面对不同的滑动策略时,只需对其内部条件进行修改,其他策略不需要进行修改。

请发表您的评论