美文网首页安卓必须知道的android进阶教程程序员
Android开发艺术探索-第三章-View的事件体系

Android开发艺术探索-第三章-View的事件体系

作者: KuTear | 来源:发表于2016-01-18 20:13 被阅读1039次

    layout: post
    date: 2016-01-08
    title: Android开发艺术探索-第三章-View的事件体系
    categories: blog
    tags: [Activity,Android,View,MotionEvent,TouchSlop]
    category: Android
    description:


    本文首发于个人博客KuTear,转载引用请注明原出处.谢谢!
    另外,更多文章分享请查看博客KuTear

    3.1 View的基础知识

    • 位置参数

      top、left、right、bottom,在3.0之后增加了x、y、translationX、translationY.这里的所有参数都是相对其父布局来说的.
      下面是具体的含义表示

      View参数

      其中参数的关系为

             x = left + translationX
             y = top + translationY
      
    • MontionEvent和TouchSlop

      MontionEvent代表着触摸事件封装的数据,包括常用的Action和位置参数等.如上面图示,注意函数getRaw*()是相对与屏幕的.
      TouchSlop表示滑动的最小常量.是常量(int),不是具体的类.获取方式为:

             ViewConfiguration.get(getContext()).getScaledTouchSlop()
      
    • VelocityTracker,GestureDetector和Scroller

      VelocityTracker用于追踪手指在滑动过程中的速度,包括水平和垂直方向上的速度。
      速度计算公式:

             速度 = (终点位置 - 起点位置) / 时间段
      

      速度可能为负值,例如当手指从屏幕右边往左边滑动的时候。此外,速度是单位时间内移动的像素数,单位时间不一定是1秒钟,可以使用方法
      computeCurrentVelocity(xxx)指定单位时间是多少,单位是ms。例如通过computeCurrentVelocity(1000)来获取速度,手指在1s中
      滑动了100个像素,那么速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)来获取速度,在100ms内手指只是滑动了
      10个像素,那么速度是10,即10(像素/100ms)。
      VelocityTracker的使用方式:

             //初始化
             VelocityTracker mVelocityTracker = VelocityTracker.obtain();
             //在onTouchEvent方法中
             mVelocityTracker.addMovement(event);
             //获取速度
             mVelocityTracker.computeCurrentVelocity(1000);
             float xVelocity = mVelocityTracker.getXVelocity();
             //重置和回收
             mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用
             mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用
      

      GestureDetector用于辅助检测用户的单击、滑动、长按、双击等行为。GestureDetector的使用比较简单,主要也是辅助检测常见的触屏事件。
      作者建议:如果只是监听滑动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector。

      GestureDetector DoubleTabListener GestureListener
             //自定义的View,实现相关接口(onGestureListener,onDoubleTabListener)
             GestureDetector mGestureDetector = 
                     new GestureDetector(this/*context*/,listener/*onGestureListener*/);
             
             //function onTouchEvent(...)或onTouchListener的onTouch(...)中,直接返回
             return mGestureDetector.onTouchEvent(event)
      

      更多使用参见[参考2]

    3.2 View的滑动

    • layout

             public void layout (int l, int t, int r, int b)
      

      参数都是相对与父布局.

             @Override
             public boolean onTouchEvent(MotionEvent event) {
                 int rawX = (int) (event.getRawX()); //相对与屏幕的坐标
                 int rawY = (int) (event.getRawY());
                 switch (event.getAction()) {
                     case MotionEvent.ACTION_DOWN:
                         // 记录触摸点坐标
                         lastX = rawX;
                         lastY = rawY;
                         break;
                     case MotionEvent.ACTION_MOVE:
                         // 计算偏移量
                         int offsetX = rawX - lastX;
                         int offsetY = rawY - lastY;
                         // 在当前left、top、right、bottom的基础上加上偏移量
                         layout(getLeft() + offsetX,
                                 getTop() + offsetY,
                                 getRight() + offsetX,
                                 getBottom() + offsetY);
                         // 重新设置初始坐标
                         lastX = rawX;
                         lastY = rawY;
                         break;
                 }
                 return true;
             }
      
    • offsetLeftAndRight和offsetTopAndBottom

      使用方法同上几乎一致

             //直接在onTouchEvent中调用,替换上面的layout(...)部分
             offsetLeftAndRight(offestX);
             offsetTopAndBottom(offestY);
      
    • LayoutParams

      这个方式在平时开发中应该使用的比较多.使用也是很简单,就是修改params的某些参数

             //ViewGroup.MarginLayoutParams layoutParams = 
             //               (ViewGroup.MarginLayoutParams) getLayoutParams();
             //LinearLayout.LayoutParams extends ViewGroup.MarginLayoutParams,
             //几乎所有的LayoutParms都是继承至
             //ViewGroup.MarginLayoutParams,
             //所以ViewGroup.MarginLayoutParams是通用的...
             LinearLayout.LayoutParams layoutParams = 
                                   (LinearLayout.LayoutParams) getLayoutParams();
             layoutParams.leftMargin = getLeft() + offsetX;
             layoutParams.topMargin = getTop() + offsetY;
             setLayoutParams(layoutParams);
             //requestLayout();效果和上面这一句一样
      
    • 动画

      动画部分在Android群英传-第七章 Android动画机制与使用技巧中已经有比较详细的说明,在这里就不做说明.

    • ViewDragHelper

      ViewDragHelper的使用过程其实也是比较简单的,主要用户控制部分都在Callback中.CallBack中的函数比较多

      CallBack

      下面是一个简单的栗子:

             //初始化
             mDragHelper = ViewDragHelper.create(this/*要处理的ViewGroup*/, 
                            1.0f/*敏感度*/, new DragHelperCallback()/*前面说的Callback*/);
             
             //复写一些函数,代码几乎固定
             @Override
             public boolean onInterceptTouchEvent(MotionEvent ev) {
               final int action = MotionEventCompat.getActionMasked(ev);
               if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                   mDragHelper.cancel();
                   return false;
               }
               return mDragHelper.shouldInterceptTouchEvent(ev);
             }
             @Override
             public boolean onTouchEvent(MotionEvent ev) {
               mDragHelper.processTouchEvent(ev);
               return true;
             }
      

      这里没有详细写出CallBack的代码,可以在这里查看.

    • ScrollTo和ScrollBy

      根据函数名称就知道这两个函数的区别,To是到具体的点,by只是与当前的偏移.
      这两个函数不是针对view本身,而是针对其内容,具体来说就是ViewGroup调用这两函数,是其内部的view在移动,view调用是其内容在动(TextView-->文本,ImageView-->图像)
      另一方面就是他的参数不同与其他,正数X往左,正数Y往上.原因查看这里,
      如果想要移动View,就需要在她的parent上调用这函数,下面是个栗子

           //替换上文onTouchEvent中的layout(...)
           ((ViewGroup) getParent()).scrollBy(-offsetX, -offsetY);
      
    • Scroller

      在以前都不知道有这个类,哎,基础不够诶.下面一个栗子说明

           //初始化,还可以使用插值器
           Scroller mScroller = new Scroller(mContext,interpolator/*插值器,可以不用*/);
           
           //View的computescroll()
           @Override
           public void computeScroll() {
               super.computeScroll();
               // 判断Scroller是否执行完毕
               if (mScroller.computeScrollOffset()) {
                   ((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY());
                   // 通过重绘来不断调用computeScroll
                   invalidate();//很重要
               }
           }
           
           //启动
           mScroller.startScroll(startX,startY,dX,dY,duration);
      

      本质上Scroller不能移动View,在我看来她同属性动画中的ValueAnimator是一样的,因为他们都只是按照某种插值器产生数值,需要自己把数值同移动
      相联系.

    3.3 View的事件分发机制

    1. 事件分发过程的三个重要方法

      • dispatchTouchEvent

        函数原型

           public boolean dispatchTouchEvent(MotionEvent ev)
        

        主要的功能是负责事件的分发.
        返回值:
        true: 表示向下分发中断
        false: 表示继续向下分发

      • onInterceptTouchEvent

        函数原型

           public boolean onInterceptTouchEvent(MotionEvent event)
        

        主要功能是负责事件的拦截
        返回值:
        true:拦截,事件交由自己(View/ViewGroup)的onTouchEvent(...)处理
        false:不拦截,事件继续向下分发.

      • onTouchEvent

        函数原型

           public boolean onTouchEvent(MotionEvent event)
        

        主要功能是处理触摸事件
        返回值:
        true:表示消费了这个事件.
        false:表示没有消费该事件,返回到上级处理.如果一直得不到处理,最终反馈到Activity的onTouchEvent(...)

    2. 函数之间的逻辑关系

      • 以上三个函数的伪代码

        类似于递归调用的方式

          public boolean dispatchTouchEvent(MotionEvent ev) {
              boolean consume = false;
              if (onInterceptTouchEvent(ev)) {
                  consume = onTouchEvent(ev);
              } else {
                  consume = child.dispatchTouchEvent(ev);
              }
              return consume;
          }
        
      • 函数与监听接口

        在通常情况下,我们为Button等组件设置了onClickListener接口,有时也会设置onTouchListener接口,但在什么时候接口中的方法才会执行呢?如果设置了onTouchListener接口监听,会对View(ViewGroup)的onTouchEvent有一定的影响.如果设置了onTouchListener,她的onTouch的返回值会影响view中onTouchEvent的调用与否,onTouch返回值的含义与onTouchEvent一样,表示是否消费了该事件.onTouch会先于onTouchEvent执行.伪代码为

           //true表示消费掉
           if(!listener.onTouch(ev)){
               onTouchEvent(ev);
           }
        

        对于onClickListener接口,他内部方法onCLick的调用是在onTouchEvent中(根据上面就知道如果在onTouchListener的onTouch中返回true,onclick就不会再执行了),其内部部分代码如下.

           //View#onTouchEvent(...)
           if (mPerformClick == null) {
              mPerformClick = new PerformClick();
           }
           if (!post(mPerformClick)) {
              performClick();
           }
           
           //点击事件的处理者 
           private final class PerformClick implements Runnable {
              @Override
              public void run() {
                  performClick();
              }
          }
          
          //点击调用onClick函数
          public boolean performClick() {
              //ListenerInfo封装了各种监听
              final ListenerInfo li = mListenerInfo;
              if (...) {
                  //调用部分
                  li.mOnClickListener.onClick(this);
                  result = true;
              }
              ...
              return result;
          }
        

        根据上面的描述,知道调用顺序为onTouchListener#onTouch,返回值决定是否继续执行view的onTouchEvent,最后在onTouchEvent中执行onClickListener的onClick方法.

    3. 分发过程

      • Activity分发

        触摸事件最先到达Activity,所以首先会在Activity中分发

               //Activity#dispatchTouchEvent()
               public boolean dispatchTouchEvent(MotionEvent ev) {
                   if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                       onUserInteraction();
                   }
                   //分发到Window.
                   if (getWindow().superDispatchTouchEvent(ev)) {
                       //true表示不再向下分发
                       return true;
                   }
                   return onTouchEvent(ev);
               }
        

        在getWindow()中返回mWindow,最终在函数attach(...)中发现

               mWindow = new PhoneWindow(this);
        

        PhoneWindow不在SDK中,在在线源码(Android源码)网站上可以找到相关的代码

               public boolean superDispatchTouchEvent(MotionEvent event ) {
                   //DecorView extends FrameLayout 
                   //       DecorView#superDispatchTouchEvent(ev)
                   //       public boolean superDispatchTouchEvent(MotionEvent event) {
                   //               //来到了ViewGroup
                   //               return super.dispatchTouchEvent(event);
                   //       }
                   return mDecorView.superDispatchTouchEvent(event);
               }
        

        由此就把事件分发到了ViewGroup,接下来就是在VieGroup中分发.

    • View分发

      函数dispatchTouchEvent(...)中的部分代码

             ...
             if (onFilterTouchEventForSecurity(event)) {
                 //noinspection SimplifiableIfStatement
                 ListenerInfo li = mListenerInfo;
                 if (li != null && li.mOnTouchListener != null
                         && (mViewFlags & ENABLED_MASK) == ENABLED
                         && li.mOnTouchListener.onTouch(this, event)) {
                     result = true;
                 }
                 // result==true,函数onTouchEvent(...)就执行不到了,而影想result的主要就是
                 //li.mOnTouchListener.onTouch(this, // event)的返回值,返回true,
                 //表示事件被处理了,自然不需要在调用onTouchEvent(...)来重新处理
                 // 前面说过onClick(...)是在onTouchEvent(...)中调用的.即优先级小于onTouch()
                 if (!result && onTouchEvent(event)) {
                     result = true;
                 }
             }
             ... 
      

      函数onTouchEvent(...)主要就是处理事件,前面已经说过onClick的执行过程了.这里就不说了.

    • ViewGroup分发

      函数dispatchTouchEvent(...)中的部分代码

             // Check for interception.
             final boolean intercepted;
             // 事件为ACTION_DOWN或者mFirstTouchTarget不为null
             //(即已经找到能够接收touch事件的目标组件)时if成立
             if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
                 //判断disallowIntercept(禁止拦截)标志位
                 //因为在其他地方可能调用了
                 //requestDisallowInterceptTouchEvent(boolean disallowIntercept)
                 //从而禁止执行是否需要拦截的判断
                 //(有点拗口~其实看requestDisallowInterceptTouchEvent()方法名就可明白)
                 final boolean disallowIntercept = 
                                    (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                 //补充:根据下面的代码可以发现, disallowIntercept 的值等于函数
                 //requestDisallowInterceptTouchEvent的参数.                 
                 if (!disallowIntercept) {
                     intercepted = onInterceptTouchEvent(ev);
                     ev.setAction(action); // restore action in case it was changed
                 } else {
                     intercepted = false;
                 }
             } else {
                 // There are no touch targets and this action is not an initial down
                 // so this view group continues to intercept touches.
                 intercepted = true;
             }
      

      注意上文代码中的注释部分,这里看一下部分requesrDisallowInterceptTouchEvent(...)的部分源码

           public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
                     //更具这里可以看出,当disallowIntercept=true时,
                     //(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 成立,
                     //这就意味着上面一段代码中的disallowIntercept=true;
                     if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
                         // We're already in this state, assume our ancestors are too
                         return;
                     }
                     ...
            }
      

      由此可见VIewGroup只会在ACTION=ACTION_DOWN或者mFirstTouchTarget != null时才判断是否拦截事件,因为一个事件序列(DOWN->MOVE->...->UP)只能有一个View处理.但是mFirstTouchTarget != null表示什么呢?

      当事件被ViewGroup的子元素成功处理了(子View的onTouchEvent/onTouch返回了true??),mFirstTouchTarget被赋值指向子元素(即!=null)

      函数dispatchTouchEvent(...)的部分实现.

         final View[] children = mChildren;
         for (int i = childrenCount - 1; i >= 0; i--) {
             final int childIndex = customOrder
                     ? getChildDrawingOrder(childrenCount, i) : i;
             final View child = (preorderedList == null)
                     ? children[childIndex] : preorderedList.get(childIndex);
             // If there is a view that has accessibility focus we want it
             // to get the event first and if not handled we will perform a
             // normal dispatch. We may do a double iteration but this is
             // safer given the timeframe.
             if (childWithAccessibilityFocus != null) {
                 if (childWithAccessibilityFocus != child) {
                     continue;
                 }
                 childWithAccessibilityFocus = null;
                 i = childrenCount - 1;
             }
      
             if (!canViewReceivePointerEvents(child)
                     || !isTransformedTouchPointInView(x, y, child, null)) {
                 ev.setTargetAccessibilityFocus(false);
                 continue;
             }
             newTouchTarget = getTouchTarget(child);
             if (newTouchTarget != null) {
                 // Child is already receiving touch within its bounds.
                 // Give it the new pointer in addition to the ones it is handling.
                 // 找到接收Touch事件的子View!!!!!!!即为newTouchTarget.
                 newTouchTarget.pointerIdBits |= idBitsToAssign;
                 break;
             }
      
             resetCancelNextUpFlag(child);
             //注意这个方法,再后面再看看..根据源码,
             //可以知道它返回的是子View(child)的dispatchTouchEvent(...)
             //当child==null,返回super.dispatchTouchEvent(...),
             //即View的dispatchTouchEvent(...)
             if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                 // Child wants to receive touch within its bounds.
                 mLastTouchDownTime = ev.getDownTime();
                 if (preorderedList != null) {
                     // childIndex points into presorted list, find original index
                     for (int j = 0; j < childrenCount; j++) {
                         if (children[childIndex] == mChildren[j]) {
                             mLastTouchDownIndex = j;
                             break;
                         }
                     }
                 } else {
                     mLastTouchDownIndex = childIndex;
                 }
                 mLastTouchDownX = ev.getX();
                 mLastTouchDownY = ev.getY();
                 //找到了事件的处理者,终止循环
                 newTouchTarget = addTouchTarget(child, idBitsToAssign);
                 alreadyDispatchedToNewTouchTarget = true;
                 break;
             }
      
             // The accessibility focus didn't handle the event, so clear
             // the flag and do a normal dispatch to all children.
             ev.setTargetAccessibilityFocus(false);
         }
      

    同样是dispatchTouchEvent(...)的部分代码

                // Dispatch to touch targets.
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    //这里说明没有子View处理该事件,只得有View的dispatchTouchEvent(...)来处理.
                    //关于该函数的部分源码在后面介绍.
                    handled = dispatchTransformedTouchEvent(ev, canceled, null/*child*/,
                            TouchTarget.ALL_POINTER_IDS);
                } else {
                 ...   
                }
        
     函数addTouchTarget(...)的具体实现.
                
                private TouchTarget addTouchTarget(View child, int pointerIdBits) {
                    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
                    target.next = mFirstTouchTarget;
                    mFirstTouchTarget = target;
                    return target;
                }
                
      函数dispatchTransformedTouchEvent(...)的部分实现.
      
            ....
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            ....
            return handled.
    

    3.4 View的滑动冲突

    • 常见的滑动冲突的场景:

      1. 外部滑动方向和内部滑动方向不一致,例如viewpager中包含listview;
      2. 外部滑动方向和内部滑动方向一致,例如viewpager的单页中存在可以滑动的bannerview;
      3. 上面两种情况的嵌套,例如viewpager的单个页面中包含了bannerview和listview。
    • 滑动冲突处理规则

      可以根据滑动距离和水平方向形成的夹角;或者根绝水平和竖直方向滑动的距离差;或者两个方向上的速度差等

    • 解决方式

      1. 外部拦截法:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要就不拦截。该方法需要重写父容器的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: {
                 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;
         }
        
      2. 内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。

           public boolean dispatchTouchEvent(MotionEvent event) {
               int x = (int) event.getX();
               int y = (int) event.getY();
           
               switch (event.getAction()) {
               case MotionEvent.ACTION_DOWN: {
                   getParent().requestDisallowInterceptTouchEvent(true);
                   break;
               }
               case MotionEvent.ACTION_MOVE: {
                   int deltaX = x - mLastX;
                   int deltaY = y - mLastY;
                   if (当前view需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
                       getParent().requestDisallowInterceptTouchEvent(false);
                   }
                   break;
               }
               case MotionEvent.ACTION_UP: {
                   break;
               }
               default:
                   break;
               }
               mLastX = x;
               mLastY = y;
               return super.dispatchTouchEvent(event);
           }
        

        父View的onInterceptTouchEvent(...)伪代码

           public boolean  onInterceptTouchEvent(MotionEvent ev){
               if(ev.getAction() == MotionEvent.ACTION_DOWN){
                   retuen false;
               }else{
                   retuen true;
               }
           }        
        

        内部拦截法过程说明,父类在ACTION_DOWN时不拦截,子类在ACTION_DOWN时拦截,这时mFirstTouchTarget!=null, disallowIntercept = true,这意味着父类的onInterceptTouchEvent(...)不会再被执行,并且一个事件序列只有一个View来处理,则所有的后续ACTION_MOVE都会传到子View,当在子View中判断到某个事件应该由父View处理,只需重置disallowIntercept=false即可,即调用函数requestDisallowInterceptTouchEvent(false),这时事件就到父View的onTouchEvent(...)处理的(因为onInterceptionTouchEvent在非ACTION_DOWN时都返回true).如果父类没有在设置requestDisallowInterceptTouchEvent(true)的话,这个事件就会一直都在父View中做处理了.(注:为个人理解,若有不对,望其指出)

    参考

    1. Art of Android Development Reading Notes 3

    2. 用户手势检测-GestureDetector使用详解

    3. ViewDragHelper详解

    4. Android触摸屏事件派发机制详解与源码分析一(View篇)

    相关文章

      网友评论

      • 541aba870cb1:hi,刚读完你的文章,有点没明白这句话。
        内部拦截法过程说明,父类在ACTION_DOWN时不拦截,子类在ACTION_DOWN时拦截,
        子类在什么时候拦截的这个down事件啊?

      本文标题:Android开发艺术探索-第三章-View的事件体系

      本文链接:https://www.haomeiwen.com/subject/ovpkkttx.html