• 2.2 事件分发机制


    事件,如: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正常分页加载显示解决方案

     

  • 相关阅读:
    深度历险:Redis 内存模型详解
    Redis 的 8 大应用场景!
    Java并发计数器探秘
    更改系统环境设置,让alias永远生效
    GoldenGate中使用FILTER,COMPUTE 和SQLEXEC命令
    数据集成实例
    客户视角:Oracle ETL工具ODI
    OGG-00782
    Oracle过程包加密
    Concurrent Request:Inactive phase,No Manager status
  • 原文地址:https://www.cnblogs.com/keyarchen/p/6044480.html
Copyright © 2020-2023  润新知