View 事件处理

View

1.View 事件体系

1.1 基础知识

  • 位置参数

    getRawX() / getRawY() //获取当前View 相对于手机屏幕的x和y坐标
    getX() / getY()  //获取相对于当前view左上角的x和y坐标
    int translationX   //移动量
    int translationY 
    

    View 事件处理_第1张图片

    图例:
    View 事件处理_第2张图片
  • 点击与滑动

    • MotionEvent :ACTION_DOWN , ACTION_MOVE, ACTION_UP

    • TouchSlop : 滑动的最小距离单位,获取:

      ViewConfiguration.get(getContext()).getSacledTouchSlop();
      
  • VeloCity Tracker

    用来在onEvent()中获取滑动速度

     VelocityTracker velocityTracker = VelocityTracker.obtain();
     velocityTracker.addMovement(event);
     velocityTracker.computeCurrentVelocity(1000); //1000ms内移动的像素数
     int xVelocity = (int) velocityTracker.getXVelocity();
     int yVelocity = (int) velocityTracker.getYVelocity();
     velocityTracker.clear();
     velocityTracker.recycle();
    
  • GestureDetector

    用来做手势检测,支持并包含onEvent()中的各种手势,同时额外的支持:onLongPress,onDoubleTap

      final GestureDetector gestureDetector = new GestureDetector(this);
      //解决长按屏幕无法拖动
      gestureDetector.setIsLongpressEnabled(false);
      mButton.setOnTouchListener(new OnTouchListener() {
      @Override public boolean onTouch(View v, MotionEvent event) {
      //接管onTouchEvent
              return gestureDetector.onTouchEvent(event);
          }
      });
    
  • Scroller

    弹性滑动

1.2 View的滑动

View的滑动主要有三种方式

  • View本身的scrollTo / scrollBy

    /**
         * Set the scrolled position of your view. This will cause a call to
         * {@link #onScrollChanged(int, int, int, int)} and the view will be
         * invalidated.
         * @param x the x position to scroll to
         * @param y the y position to scroll to
         */
        public void scrollTo(int x, int y) {
            if (mScrollX != x || mScrollY != y) {
                int oldX = mScrollX;
                int oldY = mScrollY;
                mScrollX = x;
                mScrollY = y;
                invalidateParentCaches();
                onScrollChanged(mScrollX, mScrollY, oldX, oldY);
                if (!awakenScrollBars()) {
                    postInvalidateOnAnimation();
                }
            }
        }
    
       /**
         * Move the scrolled position of your view. This will cause a call to
         * {@link #onScrollChanged(int, int, int, int)} and the view will be
         * invalidated.
         * @param x the amount of pixels to scroll by horizontally
         * @param y the amount of pixels to scroll by vertically
         */
        public void scrollBy(int x, int y) {
            scrollTo(mScrollX + x, mScrollY + y);
        }
    

    ​ scrollTo() 其实是调用onScrollChanged() 来进行绝对滑动。

    ​ 这里解释下所谓的滑动:通常我们所理解的一个Layout布局文件只是该视图的显示区域,超过了这个显示区域将不能显示到父视图的区域中 ,也就是说其实内容只是超出了他所在的view的显示区域,因此才不显示的。这里的scrollTo/srollBy 只能移动内容的位置,不能移动view本身的位置。这里的mScrollX / mScrollY 当向左滑动或者向上滑动时取正值,反之取负值。

    ​ 内容移动,位置不移动,背景不移动,点击事件不移动

  • 施加平移动画

    ​ 动画仅仅移动一个影像而已,但是实际并没有发生移动。

    ​ 带来的问题:view影像移动到了新的位置,但是系统并不认为他移动了,点击事件同样也在原来位置,点击移动后的View没有响应。

    ​ 解决方案:使用属性动画 / 在新位置设置一个新的View不显示

    ​ 内容移动,位置移动,背景移动 (肃然都是假的) 点击事件不移动

     
      android:fillAfter = "true"
          
              
            
        
    
                  
                  ObjectAnimator  .ofFloat(view, "rotationX", 0.0F, 360.0F)
           .setDuration(500)
           .start();  
    
  • 改变View的LayoutParams重新绘制

    ​ 内容移动,位置移动,点击事件移动

1.3 弹性滑动

目前上面的平移方式都很粗暴,视觉上看会很粗暴,需要一个平缓的滑动,而不是瞬间完成。弹性滑动的基本原理是将一次大的华东分成若干个小的滑动。

实现方法也有三种

  • Scroller

    
    
    

Scroller mScroller = new Scroller(MainApplication.getContext());

private void smoothScroll(int destX, int destY) {
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    mScroller.startScroll(scrollX, 0, deltaX, 0, 500);
    invalidate();
}

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

首先看下startScroll() 

​```java
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
      mMode = SCROLL_MODE;
      mFinished = false;
      mDuration = duration;
      mStartTime = AnimationUtils.currentAnimationTimeMillis();
      mStartX = startX;
      mStartY = startY;
      mFinalX = startX + dx;
      mFinalY = startY + dy;
      mDeltaX = dx;
      mDeltaY = dy;
      mDurationReciprocal = 1.0f / (float) mDuration;
  }

​ startScroll中只是初始化相关参数,并没有实质功能.实际的实现实在invalidate().invalidate()方法会引起View的重绘,也就是会调用onDraw()方法,onDraw()又会调用ViewGroup中的computScroll()方法,但是该方法是个空的方法,需要自己去重写实现。看下我们实现的方法内容。很简单,首先获取Scroller的scrollX和scrollY,然后调用scrollTo移动到指定位置。接着再去调用invalidate()发起第二次重绘.....循环下去。

​ 那么这个scrollX是怎么变化的,可以看到在scrollTo前调用了computeScrollOffset()方法:

    /**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed *                                            mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            }
        }
    }

首先获取得到已经滑动的时间,接着需要注意一个变量mDurationReciprocal ,它是什么呢,我们在startScroll时有初始化它mDurationReciprocal = 1.0f / (float) mDuration 。

那么timePassed * mDurationReciprocal 就是已经滑动的时间占据总滑动时间的百分比,接着大家就可以理解了,计算得到当前要移动到的位置,并返回true,如果已经滑动结束,那么就会返回false,不在进行下面的滑动。

  • 值动画

    与Scroller的机制大致一样,逐渐移动。

            float int startx = 0;
            float final int deltax = 0;
            ValueAnimator animator = ValueAnimator.ofFloat(0,1).setDuration(1000);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float percent = (int)animation.getAnimatedValue();
                    iv.scrollTo(0 + (int)(percent * deltax),0 );
                }
            });
    
  • 延时策略

    • Handler 延时发送message去移动
    • View的PostDelayed()
    • 使用线程的sleep方法,while循环移动并 sleep

1.4 View的事件分发机制

  • 首先时间的分发机制主要会涉及到三分方法

    • public boolean dispatchTouchEvent(MotionEvent event)
      
      public boolean onInterceptTouchEvent(MotionEvent event)
      
      public boolean onTouchEvent(MotionEvent event)
      
  • 事件传递逻辑顺序:

    对于一个ViewGroup,当点击事件发生时,它的dispatchTouchEvent() 会被调用,如果这个ViewGroup的onIntercaptTouchEvent()返回true,表示它要拦截当前事件,那么事件就会交给当前ViewGroup的TouchEvent来处理;如果返回false表示不拦截,那么就会viewGroup的子元素就会调用dispatchTouchEvent(),如此反复直至事件被处理。

  • 事件响应优先级

    onTouchListener > onTouchEvent >onClickListener

  • 事件传递顺序 activity ->window ->view 如图:

    View 事件处理_第3张图片

    整体传递和处理呈U字型逻辑。

  • 事件传递的源码解读

    View 事件处理_第4张图片

   public boolean dispatchTouchEvent(MotionEvent ev) {      
     
                 // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
     
     
        // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                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;
            }
     }

理解下这段源码:两种情况下会去判断是否要拦截。第一种就是按下时MotionEvent.ACTION_DOWN,第二种是mFirstTouchTarget!=null ,这个mFirstTouchTarget 可以这样理解,它会在当前view不处理,交给子view处理时将mFirstTouchTarget 置为false。在交给过子view处理过后,后面每一次都要进行判断是否拦截。也就是只要当前的ViewGroup拦截一次事件,那么后面不需要进行onInterceptEvent判断是否需要拦截,直接进行拦截。 再换个说法,只要当前ViewGroup处理过一次事件(除开按下),那么后面的事件都由他处理。

这里还有个标志位的判断:FLAG_DISALLOW_INTERCEPT;这个标志位一旦被设置,那么它将无法在拦截除ACTION_DOWN之外的事件,因为ACTION_DOWN会重置该标志位。因此在ACTION_DOWN时,必然会调用onInterceptEvent。可以看到 // Handle an initial down 这部分代码对标志位进行了重置.

接下来会去循环 判断子元素是否能够接收到事件,能否接收到有了两个条件:1,在上一级view的区域内2,没有在播放动画。 子元素会去调用它的dispatchTouchEvent。如果当前的子元素的dispatchTouchEvent返回false说明没有处理,那么就会接着for循环,调用同一级的下一个子元素的dispatchTouchEvent;如果的当前的dispatchTouchEvent 返回true;说明子元素处理了改时间,那么就会将mFirstTouchTarget 赋值。也就是我们最开始说的逻辑。

如果循环结束事件都没有被处理,有两种情况:1.ViewGroup没有子元素 2,子元素处理了点击事件,但是在dispatchTouchEvent 返回了false,因为这个方法可以重写。 这两种情况下,viewGroup 交给他的父类即View的dispatchTouchEvent来处理,最终会调用到onTouchEvent来处理。

1.5 滑动冲突处理

滑动冲突主要有三种情况:

  • 外部滑动方向和内部滑动方向不一致
  • 外部滑动方向和内部滑动方向一致
  • 外部滑动方向和内部滑动方向一致 + 不一致

解决方案:

​ 基本思想,根据需求如果需要外部滑动时,就在外部进行拦截,否则不拦截。

具体的实践也有两种实现方式:

  • 重写外部的onInterceptTouchEvent,根据需求判断是否拦截
  • 外部拦截除了ACTION_DOWN之外的事件,其余全部交给内部处理,当需要时调用外部的处理。

1.6 总结点

  • 在自定义的底层View的onTouchEvent中最好不要直接返回true或者false,而是调用super.onTouchEvent(),去让上一层view去处理返回结果。这里考虑的主要点在于,onClick是触发在View的ACTION_UP,因此必须去调用父类View的onTouchEvent,来触发onClick。否则直接返回结果是不会触发onClick的。

  • onClic是在ACTION_UP时才会触发,如果在当前View触发了ACTION_DOWN和ACTION_MOVE,但是MOVE出了当前的View范围,就会导致当前的View并不会接收到ACTION_UP,也就不会触发ACTION_DOWN.

  • (存在疑虑)如果当前的View没有设置OnClick,那么在ACTION_DOWN时就会返回false,也就是说所有的ACTION都会移交给上层的ViewGroup来处理,当前VIew不处理任何ACTION

  • 滑动拦截

    • 外部拦截:大于某个值时才进行拦截;一旦拦截,那么后续操作都会由外部来处理,所以要滑动大于某个值才进行拦截。其中外部的ACTION_UP必须要设为false,因为点击时候,可能会触发ACTION_MOVE,但是移动的距离很小,没有触发拦截,也就是说子类View是应该要触发OnClick的,但是如果在ACTION_UP时,父类return true,name就会拦截掉,导致ACTION_UP传递不到View中,也就不会触发OnClick。

    • 内部拦截:使用到了getParent().requestDisallowInterceptTouchEvent() 表示ViewGroup是否不拦截;

      ViewGroup要把ACTION_DOWN设为不拦截,这样才能到达View,把ACTION_MOVE和ACTION_UP设为拦截。

      可以在view的ACTION_DOWN时进行调用getParent().requestDisallowInterceptTouchEvent(true),表示viewGroup不进行拦截,操作交给当前View来处理。当满足某个条件时,让ViewGroup来进行处理,getParent().requestDisallowInterceptTouchEvent(false),即进行拦截,接着调用onInterCeptTopuchEvent,即我们刚才设置拦截。这样就会在上一层进行处理了。

你可能感兴趣的:(View 事件处理)