美文网首页Android
View 事件处理

View 事件处理

作者: GordenNee | 来源:发表于2017-04-28 17:07 被阅读25次

    View

    1.View 事件体系

    1.1 基础知识

    • 位置参数

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

      图例:
    • 点击与滑动

      • 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不显示

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

       <set xmlns:android="http://schemas.android.com/apk/res/android" >
        android:fillAfter = "true"
            
          <translate
              android:duration="500"
              android:fromYDelta="-100%"
              android:toYDelta="0%" >      
              <alpha
                  android:duration="500"
                  android:fromAlpha="0.0"
                  android:interpolator="@android:anim/decelerate_interpolator"
                  android:toAlpha="1.0" />
          </translate>
      </set>
                    
                    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 如图:

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

    • 事件传递的源码解读

       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 事件处理

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