事件,如:onTouchEventonClickonLongClick等。

事件通常重要的有三种:MotionEvent.ACTION_DOWN / ACTION_MOVE / ACTION_UP

事件的响应原理:最广泛应用的就是监听、回调,进而形成了事件响应的过程。

  要了解View的事件分发机制,首先,我们要熟悉dispatchTouchEvent()和onTouchEvent()两个函数,这两个函数都是View的函数,要理解View事件的分发机制,只要清楚这两个函数就基本上清楚了。

  • dispatchTouchEvent(): 此函数负责事件的分发,你只需要记住当触摸一个View控件,首先会调用这个函数就行,在这个函数体里决定将事件分发给谁来处理。
  • onTouchEvent(MotionEvent event): 此函数负责执行事件的处理,负责处理事件。

  参数event为手机屏幕触摸事件封装类的对象,其中封装了该事件的所有信息,例如触摸的位置、触摸的类型以及触摸的时间等。该对象会在用户触摸手机屏幕时被创建。

1  View的事件分发(以Button为例)

  我们知道,View做为所有控件的父类,它本身定义了很多接口来监听触摸在View上的事件,那么当手指触摸到View时候,该响应“点击”还是”触摸”呢,就是根据dispatchTouchEventonTouchEvent这两个函数组合实现的,我们之下的讨论,仅对常用的“点击OnClick”和“触摸onTouch”来讨论,顺藤摸瓜,找出主线,进而搞清楚View的事件分发机制。

对于按钮,点击它一下,我们期望2种结果,第一种:它响应一个点击事件。第二种:不响应点击事件。

第一种源码:
public class MainActivity extends Activity implements OnClickListener ,OnTouchListener{
  private Button btnButton;
  protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       btnButton=(Button) findViewById(R.id.btn);
       btnButton.setOnClickListener(this);
       btnButton.setOnTouchListener(this);
   }

  public void onClick(View v) {
       Log.e("View", "onClick===========>"); 
}

  public boolean onTouch(View v, MotionEvent event) {
       Log.e("View", "onTouch..................................");
       return false;
  }
}

第二种源码:
…… // 同上
public boolean onTouch(View v, MotionEvent event) {
       Log.e("View", "onTouch..................................");
       return true;
  }

结果分析:上面两处代码,第一种执行了OnClick函数和OnTouch函数,第二种执行了OnTouch函数,并没有执行OnClick函数,而且对两处代码进行比较,发现只有在onTouch处返回值true和false不同。当onTouch返回false,onClick被执行了,返回true,onClick未被执行。

为什么会这样呢?我们只有深入源码才能分析出来。

  前面提到,触摸一个View就会执行dispatchTouchEvent方法去“分发”事件,既然触摸的是按钮Button,那么我们就查看Button的源码,寻找dispatchTouchEvent方法,Button源码中没有dispatchTouchEvent方法,但知道Button继承自TextView,寻找TextView,发现它也没有dispatchTouchEvent方法,继续查找TextView的父类View,发现View有dispatchTouchEvent方法,那我们就分析dispatchTouchEvent方法。主要代码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
   if (onFilterTouchEventForSecurity(event)) {
      if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&mOnTouchListener.onTouch(this, event))
          return true;  // 截断,跳出
      if (onTouchEvent(event))
          return true;
  }
   return false;
}

 

分析:先来看dispatchTouchEvent函数返回值,如果返回true,表明事件被处理了,反之,表明事件未被处理。

  • mOnTouchListener != null,判断该控件是否注册了OnTouchListener对象的监听,
  • (mViewFlags & ENABLED_MASK) == ENABLED,判断当前的控件是否能被点击(比如Button默认可以点击,ImageView默认不许点击,看到这里就了然了),
  • mOnTouchListener.onTouch(this, event)这个是关键,这个调用,就是回调你注册在这个View上的mOnTouchListener对象的onTouch方法。如果你在onTouch方法里返回false,那么这个判断语句就跳出,去执行下面的程序,否则,条件成立,就直接返回了,不再执行下面的程序。
  • if (onTouchEvent(event)) 这个判断很重要,能否回调OnClickListener接口的onClick函数,关键在于此,可以肯定的是,如果上面if返回true,那么就不会执行并回调OnClickListener接口的onClick函数

接下来,我们看onTouchEvent这个函数,看它是如何响应点击事件的。主要代码如下:

 

public boolean onTouchEvent(MotionEvent event) {
//……
// 可以点击或长按
  if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
      switch (event.getAction()) {
          case MotionEvent.ACTION_UP:
              if (!focusTaken) {
            if (mPerformClick == null) {
                mPerformClick = new PerformClick();
            }
            if (!post(mPerformClick)) {
                performClick();  // 这里就是去执行OnClick()回调函数,实现点击
            }
              }
              break;
           case MotionEvent.ACTION_DOWN:
                    ……
       }
       return true;
   }
   return false;
}

// 如果Button注册了OnClickListener
  protected OnClickListener mOnClickListener;
    public interface OnClickListener { void onClick(View v); }
    public void setOnClickListener(OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        mOnClickListener = l;
  }
    public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        if (mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnClickListener.onClick(this);
            return true;
        }
        return false;
  }

  从上面主要代码可以看出onTouchEvent传参MotionEvent类型,它封装了触摸的活动事件,其中就有ACTION_DOWN、ACTION_MOVE、ACTION_UP三个事件。

  我们在来看看onTouchEvent的返回值,因为onTouchEvent是在dispatchTouchEvent事件分发处理中调用的,如果onTouchEvent返回truedispatchTouchEvent就返回true,表明事件被处理了,反之,事件未被处理。  从上面主要代码可以看出onTouchEvent传参MotionEvent类型,它封装了触摸的活动事件,其中就有ACTION_DOWN、ACTION_MOVE、ACTION_UP三个事件。

  程序的关键在那个长长的if的判断里,我们发现无论switch的分支在什么地方跳出,返回都是true。这就表明,无论是三个事件中的哪一个,都会返回true。参照右图,结合上述,不难理解View的分发机制了。

2  ViewGroup的事件分发

  ViewGroup事件分发机制较View的稍微复杂一些,不过对View的机制只要精确的理解后,仔细看过这一节,睡几觉起来,估计也就悟出来了,学习就是这么奇怪,当下理解不了或模糊的地方,只要脑子有印象,忽然一夜好像就懂了。先来看下面的一个简单布局,我们将通过例子,了解ViewGroup+View的android事件处理机制。

 

  上图所示:黑色为线性布局LinearLayout,深灰色为相对布局RelativeLayout,按钮Button三部分组成。RelativeLayout为LinearLayout的子布局,Button为RelativeLayout的子布局。以下RelativeLayout简称(R),LinearLayout简称(L),Button简称(B)。

经过前面讲解,我们首先知道这样两件事情:

  A、(R)和(L)的父类是ViewGroup,(B)的父类是View。

  B、dispatchTouchEvent这个函数很重要,不论是ViewGroup还是View,都由它来处理事件的消费和传递。

分析:当手指点击按钮B时,事件传递的顺序是从底向上(从外向内)传递的,也就是按照L->R->B的顺序由下往上逐层传递,响应正好相反,是自上而下。

1) L首先接收到点击事件,L的父类是ViewGroup类,并将事件传递给dispatchTouchEvent方法,dispatchTouchEvent函数中判断该控件L是否重载了onInterceptTouchEvent方法进行事件拦截:

    • 默认返回false不拦截,那么dispatchTouchEvent方法将事件传递给R去处理(进入第2流程处理),
    • 如果返回true表示当前L控件拦截了事件向其它控件的传递,交给它自己父类View的dispatchTouchEvent去处理,在父方法的dispatchTouchEvent中,将会按照前面讲的View的事件处理机制去判断,比如判断L是否重载了onTouch方法,是否可点击,是否做了监听等事件。

2) R也是ViewGroup的子类,因此与第1流程基本相似,如果onInterceptTouchEvent返回了false,表示事件将不拦截继续传递给B。

3) B是View的子类,它没有onInterceptTouchEvent方法,直接交给自己父类View的dispatchTouchEvent去处理。

总结:onInterceptTouchEvent只有ViewGroup才有,当一个控件是继承自ViewGroup而来的,那么它就可能会有子控件,因此,才有可能传递给子控件,而继承自View的控件不会有子控件,也就没有onInterceptTouchEvent函数了。通过dispatchTouchEvent分发的控件返回值True和false,如果消费了,返回True,就不再继续传递了;反之false,没有消费,如果有子控件将继续传递。

  A、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发

  B、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法

  C、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true);  阻止ViewGroup对其MOVE或者UP事件进行拦截;

实际应用中能解决哪些问题呢?

  比如你需要写一个左侧隐藏menu,主Activity上有个Button、ListView或者任何可以响应点击的View,你在当前View上死命的滑动,菜单栏也出不来;因为MOVE事件被子View处理了~

  你需要这么做:在ViewGroup的dispatchTouchEvent中判断用户是不是想显示菜单,如果是,则在onInterceptTouchEvent(ev)拦截子View的事件;自己进行处理,这样onTouchEvent就可以顺利展现出菜单栏了。

 

实例:ViewPager+ListView滑动事件拦截

  在Android开发过程中,你一定会用到ViewPager这个控件,最让人头疼的就是各种滑动冲突,比如说:在ListView,SrollView中嵌套ViewPager,在作侧边栏滑动时和ViewPager的冲突,甚至还有ViewPager嵌套ViewPager的情况等等,解决起来很麻烦。这些冲突无非就是横向滑动和纵向滑动的一个冲突,而我们要解决的就是要判断将事件给父控件还是子控件处理。

import android.content.Context; 
import android.support.v4.view.ViewPager; 
import android.util.AttributeSet; 
import android.util.Log; 
import android.view.MotionEvent; 
public class ChildViewPager extends ViewPager {
public ChildViewPager(Context context, AttributeSet attrs) {  
  super(context, attrs);  
}
public ChildViewPager(Context context) { 
  super(context);  
}
private float xDistance, yDistance, xLast, yLast,xDown, mLeft; // 滑动距离及坐标 归还父控件焦点
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        getParent().requestDisallowInterceptTouchEvent(true);
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("touch", "ACTION_DOWN");
                xDistance = yDistance = 0f;
                xLast = ev.getX();
                yLast = ev.getY();
                xDown = ev.getX();
                mLeft = ev.getX(); // 解决与侧边栏滑动冲突
                break;
            case MotionEvent.ACTION_MOVE:
                final float curX = ev.getX();
                final float curY = ev.getY();

                xDistance += Math.abs(curX - xLast);
                yDistance += Math.abs(curY - yLast);
                xLast = curX;
                yLast = curY;
                if (mLeft < 100 || xDistance < yDistance) {
                   getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    if (getCurrentItem() == 0) {
                        if (curX < xDown) {
                            getParent().requestDisallowInterceptTouchEvent(true);
                        } else {
                            getParent().requestDisallowInterceptTouchEvent(false);
                        }
                    } else if (getCurrentItem() == (getAdapter().getCount()-1)) {
                        if (curX > xDown) {
                            getParent().requestDisallowInterceptTouchEvent(true);
                        } else {
                            getParent().requestDisallowInterceptTouchEvent(false);
                        }
                    } else {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

  核心代码就是 getParent().requestDisallowInterceptTouchEvent(false);这句;当为false则通知父view可以拦截touch事件,由父view处理,而当为true时,则会阻止父层的View截获touch事件,这样就会返回个子view处理。

  那么好,解决了这个关键性问题接下来的问题,就是要解决什么时候交给子view或是父view的问题了。当ScrollView和ListView中嵌套ViewPager的时候,多数是在作轮播的幻灯片,冲突无非就是能左右滑动,不能上下滑动的问题;而ViewPager内嵌套ViewPager这是子view不能滑动,一划就是父ViewPager滑动的问题;这两个问题上面的自定义ViewPager都能解决,解决思路是在事件分发down的时候记录xLast和yLast,然后在move的时候比较xDistance和yDistance即x轴差和y轴差,如果x轴差小于y轴差,则说明是上下滑动,此时将事件还给父view,反之左右滑动把事件交给子view。

  那mLeft是干嘛用的啊?是这样的,可能我在项目中会用到侧边栏,我的用的是SlidingMenuLibrary这个开源组件,可以设置将侧边栏划出的区域,这时就会跟ViewPager冲突,都是左右滑,事件被子view吃掉了,侧边栏画不出来,所以我就加个mLeft当点击屏幕左侧边缘(即down时x<100)时,将事件还给父view这样既不影响侧边栏划出,也不影响ViewPager的滑动。同样你可以举一反三,灵活控制事件响应区域。

  再给大家说一个扩展的,如果用过ViewPagerIndicatorLibrary朋友,可能会遇到,ViewPager内套ViewPager,这时我们就想能不能在内部ViewPager滑到最后一页时,父ViewPager在切换到下一页,提供一个思路:就是在事件分发时判断当前页是不是第一页或最后一页,如果是最后一页且向左滑将事件交给父view即可,以此类推。

 

ScrollView嵌套ListView正常分页加载显示解决方案