从Android源码的角度理解应用开发(1)-Touch机制

版权声明:转载请注明本文出自远古大钟的博客(http://blog.csdn.net/duo2005duo),谢谢支持! https://blog.csdn.net/duo2005duo/article/details/51604119

转载请注明本文出自远古大钟的博客(http://blog.csdn.net/duo2005duo),谢谢支持!

Touch概述

Touch操作即是用手触摸或者用鼠标操作屏幕所造成的事件触发。这些事件最基本的包括按下Down,移动Move,取消Cancel和离开触摸屏Up四种事件。一个完整的Touch过程一般是由Down->(Move)->Up/Cancel这四个事件组成,值得注意的是,一个完整的触摸事件必须由Down开始,再到Up/Cancel结束,中间的Move可以有可以没有,当然Touch事件不止这四个事件,但这四个事件是最基本,开发中必须考虑到的。
当用户开启应用触摸屏幕,系统服务就通过IPC(Binder)通知应用的主线程Looper中,最终传递到我们应用中Activity,View和ViewGroup中。
需要对Touch机制清晰才可以解决以下一些类似问题:
1. touch监听没被调用到
2. 双层滑动模块嵌套后发生滑动不了的现象
3. 设置了onClickListener后,点击View没有反馈
4. 点击两下View才调用onClickListener的bug

宏观

这里写图片描述
以上是Touch事件的传递顺序,一个Touch事件要传递到View中,必须经过Activity向下分发,如果在ViewGroup在子View中找到可以处理这个事件的View,则向下再传递下去,否则ViewGroup会尝试处理这个事件。下面详细介绍View,ViewGroup,Activity这三个类收到Touch事件的处理已经它们如何分发Touch事件。

View的Touch逻辑

Android中View对于Touch的处理逻辑主要集中在以下三个个位置中

//最主要的触摸事件的分发逻辑,向接收Touch事件的子View(包括自己)派发事件,对于View而非ViewGroup来说,这里只会对自己分发。
boolean dispatchTouchEvent(MotionEvent event);

//当前View处理触摸事件的可选方法,在dispatchTouchEvent()中被调用
void setOnTouchListener(OnTouchListener l);
public interface OnTouchListener {
    boolean onTouch(View v, MotionEvent event);
}

//当前View处理触摸事件的默认方法,在dispatchTouchEvent()中被调用,如果已经设置了OnTouchListener,并且OnTouchListener已经消费了这个Touch事件,返回true,则不会触发这个方法。
boolean onTouchEvent(MottionEvent event);

以上几个方法返回值为true代表事件被处理,如果返回false,则代表事件没有被处理。

View之dispatchTouchEvent

让我们看一下dispatchTouchEvent的逻辑,删除部分不重要代码,源码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    //1.停止嵌套滑动
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }
    //2.安全监测
    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;

        //3.如果当前View使能(setEnabled(true)),则调用Touch监听器    
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //4.如果Touch监听器返回false或者没有调用Touch监听器,则返回调用onTouchEvent()
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    //停止嵌套滑动
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

从上面的源码中可以看出View的dispatchTouchEvent()主要的逻辑如下:
1.停止嵌套滑动(5.0以后添加的)
2.做了安全监测,如果View开启了安全检测(setFilterTouchesWhenObscured(true))并且当前View所在的Window被其他Window遮盖的话,则不会调用再处理Touch事件
3.如果当前View使能,才会调用OnTouchListener
4.不管View使能与否,只要OnTouchListener没有处理事件,就会让onTouchEvent()来处理事件,View不使能的情况下会调用View自己的onTouchEvent()

感悟:在dispatchTouchEvent中,会优先调用listener,后再调用onTouchEvent,View的dispatchTouchEvent()的作用不能理解成向其他View传递Touch,它的作用应用理解为对组合(listener)继承(onTouchEvent)的派发。我们知道,最常用的两种复用技术无非就是组合继承,谷歌为了方便开发者,将这两种技术用dispatchTouchEvent()结合起来。此外,还有一个enable的属性,这个属性改变了dispatchTouchEvent中的调用组合继承的逻辑。

View之onTouchEvent

前面我们已经知道如果事件没有被OnTouchListener处理的话,将会被onTouchEvent()处理。
onTouchEvent()在源码中主要是处理

press :按下时候View状态的改变,比如View的背景的drawable会变成press 状态
click/tap: 快速点击
longClick:长按
focus:跟press类似,也是View状态的改变
touchDelegate:分发这个点击事件给其他的View,这个点击事件传到其他View前会改变这个事件的点击坐标,如果在指定的Rect里面,则是View的中点坐标,否则在View之外

让我们看一下OnTouchEvent的逻辑,源码如下:

public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            //1.View不使能的情况下(setEnabled(false)),依然可能消费事件
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        //2.用TouchDelegate将自己的区域变成其他View中心点的操作
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        //3.从这里跟1结合可以知道,只要View是Clickable或者LongClickable,就一定消费事件
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    //只有在press的情况下,才可能click,longClick也做了同样的判断
                    if ((mPrivateFlags & PRESSED) != 0) {
                        //4.如果我们在当前View还没获取焦点,并且能在touch下foucus,那么第一次点击只会将这个View的状态改成focus,而不会触发click
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        //5.已经有longClick执行过了,就不再执行click了
                        if (!mHasPerformedLongPress) {
                            if (mPendingCheckForLongPress != null) {
                                removeCallbacks(mPendingCheckForLongPress);
                            }
                            if (!focusTaken) {
                                performClick();
                            }
                        }
                        //6.取消press
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (!post(mUnsetPressedState){
                            mUnsetPressedState.run();
                        }
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    //7.press,定时检测并且执行longclick
                    mPrivateFlags |= PRESSED;
                    refreshDrawableState();
                    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
                        postCheckForLongClick();
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    //8.清理状态
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    break;

                case MotionEvent.ACTION_MOVE:
                    final int x = (int) event.getX();
                    final int y = (int) event.getY();

                    //9.如果移动到View外,则不press,如果移动到View内,则press
                    int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                            (y < 0 - slop) || (y >= getHeight() + slop)) {
                        // Outside button
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press checks
                            if (mPendingCheckForLongPress != null) {
                                removeCallbacks(mPendingCheckForLongPress);
                            }
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    } else {
                        // Inside button
                        if ((mPrivateFlags & PRESSED) == 0) {
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                        }
                    }
                    break;
            }
            return true;
        }

        return false;

View的onTouch方法归纳为一下几点:
1.不管View使能与否,只要clickable或者longclickable,就一定消费事件(返回true)
2.如果View不使能,并且clickable或者longclick,就只会消费事件但不做其他任何操作
3.如果View使能,先看看TouchDelegate消费与否,如果不消费再给自己消费
4.处理包括focus,press,click,longclick

感悟:谷歌选择继承的方式作为默认的实现,这个实现非常重要,并不可少。因为它关联着focus,click,press,longClick,同时还关联着TouchDelegate,错误地改写他或者添加Listener的后果都将可能导致以上5种机制失效。另外,对于Click与onLongClick,有专门对应的属性focus,press进行关联。longClick的Touch过程中必须保持一致是press状态。

ViewGroup的Touch逻辑

而ViewGroup继承与View,并覆盖了

dispatchTouchEvent(MotionEvent event);

而且比View多了一个处理Touch的位置:

viewgroup.onInterceptTouchTouchEvent(MotionEvent event);

这个方法的返回值主要用于是否阻止向子View派发触摸事件,默认返回false,不阻止。
精简后的源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;

    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    if (action == MotionEvent.ACTION_DOWN) {
        //1.只有在非拦截的情况的下寻找target
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            // 防止onInterceptTouchEvent()的时候改变Action
            ev.setAction(MotionEvent.ACTION_DOWN);
            // 遍历子View,第一个消费这个事件的子View的为Target
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                //当然只遍历可见的,并且没有在进行动画的。
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        // offset the event to the view's coordinate system
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        if (child.dispatchTouchEvent(ev))  {
                            // Event handled, we have a target now.
                            mMotionTarget = child;
                            return true;
                        }
                                            }
                }
            }
        }
    }

    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    //up或者cancel的时候清空DisallowIntercept
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // 如果没有target,则把自己当成View,向自己派发事件
    final View target = mMotionTarget;
    if (target == null) {
        // We don't have a target, this means we're handling the
        // event as a regular view.
        ev.setLocation(xf, yf);
        return super.dispatchTouchEvent(ev);
    }

    // 如果有Target,拦截了,则对Target发送Cancel,并且清空Target
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setAction(MotionEvent.ACTION_CANCEL);
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
            // target didn't handle ACTION_CANCEL. not much we can do
            // but they should have.
        }
        // clear the target
        mMotionTarget = null;
        // Don't dispatch this event to our own view, because we already
        // saw it when intercepting; we just want to give the following
        // event to the normal onTouchEvent().
        return true;
    }
    //up 或者 cancel清空Target
    if (isUpOrCancel) {
        mMotionTarget = null;
    }

    //如果有Target,并且没有拦截,则向Target派发事件,这个事件会转化成Target的坐标系
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);

    return target.dispatchTouchEvent(ev);
}

综上,ViewGroup的主要的任务是找一个Target,并且用这个target传递事件,主要逻辑如下
1.在Down并且不拦截的时候会多出一个寻找Target的过程,在这个过程中遍历子View,如果子View的dispatchTouch为true,则这个子View就是当前ViewGroup的Target。找Target是处理Down事件时候特有的,其他事件不会触发找Target。
2.如果没有Target,则发送把自己当做一个View去处理这个事件(super.dispatchTouch())
3.如果有Target并且拦截,则发送Cancel给子View
4.如果有Target并且不拦截,则调用Target的dispatchTouch
5.可以利用requestDisallowInterceptTouchEvent(boolean)来强制viewparent不拦截事件。但是作用域限于一个Touch的过程(Down->Up/Cancel)

感悟:我们发现,在ViewGroup的分发过程中,相对原先的View分发过程中,多了对子View分发功能,同时,添加了向下屏蔽分发的功能(onInterceptTouchEvent)和对应禁止屏蔽的功能(disallowIntercept )。Enable属性也不作用于ViewGroup类的分发过程。 此外,原本事件的分发应该是一个树的广度遍历,谷歌为了加速遍历过程,在Touch过程一次广度遍历(Down)中加入了一个指针(Target),使得在本次Touch过程的接下来的流程中只需树高度个数的target即可。

对于dispatchTouchEvent返回值,分发过程只有Down过程用到,那么是不是返回值在其他阶段就没有作用了呢?
不是的,在Activity的dispatchTouch逻辑中会用到。

Activity的Touch逻辑

ViewGroup收到的事件是由Activity发送出去的。Activity的Touch逻辑非常简单,源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
      if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          onUserInteraction();
      }
      //这个最终传递到setContentView对应的View中
      if (getWindow().superDispatchTouchEvent(ev)) {
          return true;
      }
      //如果ContentView没有对时间进行处理,统一由Activity的onTouchEvent()来处理
      return onTouchEvent(ev);
}

Activity的Touch逻辑归纳如下:
Activity将事件经过一些步骤发送给ContentView,如果ContentView没消费,就交给Activity自己处理。

问题解决

文章开头提出了几个问题,可以用这篇文章的分析解决:

  1. touch监听器没被调用到?
    View.dispatchTouchEvent(),ViewGroup.dispatchTouchEvent()
    a)如果是View非使能,直接用setEnabled(true)
    b)如果是事件被这个View的viewparent拦截了。可以修改这个viewparent的onInterceptTouchTouchEvent(),或者在这个View中调用getParent().requestDisallowInterceptTouchEvent()

  2. 双层滑动模块嵌套后发生滑动不了的现象?
    ViewGroup.dispatchTouchEvent()
    如果是事件被这个View的viewparent拦截了。可以修改这个viewparent的onInterceptTouchTouchEvent(),或者在这个View中调用getParent().requestDisallowInterceptTouchEvent()

  3. 设置了onClickListener后,点击View没有反应?
    View.onTouchEvent()
    a)如果是View非使能,直接用setEnabled(true)
    b)可能覆盖了onTouchEvent(),需要在覆盖的方法调用super.onTouchEvent()或者手动调用performClick()

  4. 点击两下View才调用onClickListener的bug?
    View.onTouchEvent()
    这个其实是安卓的设计,当某个View调用了setFocusableInTouchMode(true)后,第一次点击会引起这个View的focus,第二次点击才会调用onClickListener,只需要设置setFocusableInTouchMode(false)即可。

总结图

这里写图片描述

猜你喜欢

转载自blog.csdn.net/duo2005duo/article/details/51604119
今日推荐