View 事件处理

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,即我们刚才设置拦截。这样就会在上一层进行处理了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容