面试系列之事件分发机制全解析

栏目: IOS · Android · 发布时间: 6年前

内容简介:事件分发机制虽然大家都知道是什么东西,但有可能其中的一些细节重点要点还是不清晰,本文将结合实例带你攻克事件分发。可以通过重写滑动冲突的本质其实是一个
  • 1.事件分发流程图(究极重点)
  • 2.在遍历子View时如何从内层的子View开始遍历?
  • 3.滑动冲突有哪些场景?滑动冲突处理原则是什么?
  • 4.ACTION_POINTER_DOWN ,event.getX(int index)什么时候发生?
  • 5.View的滑动方式有哪些?
  • 6.ScrollView里面有一个button,然后按住button向上滑,讲述事件传递过程?
  • 7.按住一个button,然后手指移到别处,click事件还能不能响应?

事件分发机制虽然大家都知道是什么东西,但有可能其中的一些细节重点要点还是不清晰,本文将结合实例带你攻克事件分发。

1. 事件分发流程图(究极重点)

面试系列之事件分发机制全解析
  1. 一般来说, 一组事件序列为ACTION_DOWN(一个,手指点下)->ACTION_MOVE(N个,手指移动)->ACTION_UP(一个,手指抬起),必须以DOWN事件开始,UP事件结束
  2. 当一个View消费事件后,后续的事件都直接交由它去处理但有两种情形需要注意:
    1. ViewGroup进行了拦截,后续事件将交由ViewGroup的onTouchEvent去处理。
    2. 可以在处理事件的View的onTouchEvent()中手动的去调用其他View的onTouchEvent()将事件强行传递给其他View处理,但这样违背了事件分发的本质。
  • (这里指的消费事件其实并不是onTouchEvent返回true而是ACTION_DOWN事件时是否返回true,当一个View消费事件后后续的MOVE和UP事件都交由当前这个消费了事件的View去处理。)
  1. onInterceptTouchEvent在DOWN事件和MOVE事件返回true进行拦截其实是非常没有必要的,如果在DOWN拦截那么后续事件都不会交由子View去判断,在UP事件拦截那么消费了事件的子View的UP事件将无法进行响应。
  2. 如果onInterceptTouchEvent在MOVE事件返回true的话那么 首先会发送一个ACTION_CANCEL 事件给原先处理事件的View,之后后续的MOVE和UP事件将直接发送给其自身的onTouchEvent去处理且其自身的onInterceptTouchEvent将不会被调用。(也就是说onInterceptTouchEvent一旦返回true那么之后的事件将不会在触发其自身的onInterceptTouchEvent方法,onInterceptTouchEvent在返回true以后将不再调用)。
  3. View的onTouchEvent默认消耗事件(ACTION_DOWN返回true),除非它是不可点击的(clickable和longClickable同时为false),如果View设置了onClickListener则clickable为true,设置了onLongClickListener则longClickable为true。View默认longClickable都为false,clickable非情况(如Button为true,TextView为false)。
  • View是否消耗事件顺序:onTouch(setOnTouchListener)->onTouchEvent->setOnClickListener->setOnLongClickListener。
  1. View的enable不影响onTouchEvent的返回值,View当enable为false时只要clickable为true照样可以消费事件只不过ACTION_UP时不会有任何响应。

2.在遍历子View时如何从内层的子View开始遍历?

可以通过重写 getChildDrawingOrder 方法去改变遍历规则。

3.滑动冲突有哪些场景?滑动冲突处理原则是什么?

滑动冲突的本质其实是一个 策略问题 ,在开发中我们通常都是通过在子View中去调用requestDisallowInterceptTouchEvent方法配合父View中的onInterceptTouchEvent方法去使用。

下边给出一个例子:

public class MyLayout extends LinearLayout{
    public MyLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:   //表示父类需要拦截
                    return true;
            default:
                break;
        }
        return false;    //如果设置拦截,除了down,其他都是父类处理
    }
}
复制代码
public class MyButton extends Button {
    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);//禁用父View的拦截方法。
                break;
            case MotionEvent.ACTION_MOVE:
                if(满足条件){
                getParent().requestDisallowInterceptTouchEvent(false);//解除父View的拦截的禁用。
                }
        }
        return true;
    }
}
复制代码
  • 可以看到当 ACTION_DOWN 事件时我们在View自身的onTouchEvent中调用了 getParent().requestDisallowInterceptTouchEvent(true)这个方法,当此方法调用后在方法内部中会改变FLAG_DISALLOW_INTERCEPT标志位为true,这时在ViewGroup中的dispatchTouchEvent中如果检测到FLAG_DISALLOW_INTERCEPT为true的话将跳过onInterceptTouchEvent的调用而直接返回false,也就是父View直接不进行拦截,这时我们的事件都将由子View去处理,同时也不用担心父View的拦截方法会对事件进行拦截,当我们在移动时满足事件可被父View进行拦截时则需要调用getParent().requestDisallowInterceptTouchEvent(false)将父View的拦截方法的禁用解除掉,这时父View的onInterceptTouchEvent将可继续去判断是否需要进行事件的拦截。

在ViewGroup的dispatchTouchEvent中我们可以看到如下代码:

// 发生ACTION_DOWN事件或者已经发生过ACTION_DOWN,并且将mFirstTouchTarget赋值,才进入此区域,主要功能是拦截器
final boolean intercepted;
//onInterceptTouchEvent返回true后之后将不再执行onInterceptTouchEvent方法,因为其将mFirstTouchTarget字段置为了null。
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
    //disallowIntercept:是否禁用事件拦截的功能(默认是false),即不禁用
    //可以在子View通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改,不让该View拦截事件
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    //默认情况下会进入该方法
    if (!disallowIntercept) {
        //调用拦截方法
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action);
    } else {
        intercepted = false;
    }
} else {
    // 当没有触摸targets,且不是down事件时,开始持续拦截触摸。
    intercepted = true;
}
复制代码

4.ACTION_POINTER_DOWN ,event.getX(int index)什么时候发生?

获取事件时需调用MotionEvent.getActionMasked()而不是MotionEvent.getAction(),只有MotionEvent.getActionMasked()可以支持多点触控。

常见值:

  • ACTION_DOWN :第一个手指按下(之前没有任何手指触摸到 View)
  • ACTION_UP :最后一个手指抬起(抬起之后没有任何?指触摸到 View,这个手指未必是 ACTION_DOWN 的那个手指)
  • ACTION_MOVE 有手指发生移动
  • ACTION_POINTER_DOWN 额外手指按下(按下之前已经有别的手指触摸到 View)
  • ACTION_POINTER_UP 有手指抬起,但不是最后一个(抬起之后,仍然还有别的手指在触摸着 View)

默认的event.getX()其实可以理解为 event.getX(0),这是针对于一根手指的情况,再多点触控的情况下我们需要通过调用event.getX(int index)来传入参数以区别当前是第几根手指在进行移动 (这里的index是会变的,但是手指的ID是不会变的,我们需要通过ID找到对应手指的index)

多点触控一般写法实例: github.com/rengwuxian/…

5.View的滑动方式有哪些?

大致可分为下边三个方法(只有layout方法是可以真正改变View坐标位置)

1.layout:

对View进行重新布局定位。在onTouchEvent()方法中获得控件滑动前后的偏移。然后通过layout方法重新设置。

// 视图坐标方式
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录触摸点坐标
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                // 在当前left、top、right、bottom的基础上加上偏移量
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
//                        offsetLeftAndRight(offsetX);
//                        offsetTopAndBottom(offsetY);
                break;
        }
        return true;
    }
复制代码

2.ScrollTo/ScrollBy:

本质是View内容的移动,需要通过父容器的该方法来滑动当前View,Scroller: 平滑滑动,通过重载computeScroll(),使用scrollTo/scrollBy完成滑动效果,Scroller只是一个移动的机制,真正还是需要调用去scrollTo/scrollBy去进行移动。

Scroll中与之相关的各种API中的参数都要跟实际我们认知相反, 比如想往自身右边移动100不是去调用scrollerBy(100,0)而是调用scrollerBy(-100,0)。

public class ScrollButton extends android.support.v7.widget.AppCompatButton {
    Scroller scroller;
    int direction = -1;

    public ScrollButton(Context context) {

        this(context,null);
    }

    public ScrollButton(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public ScrollButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        scroller = new Scroller(context);
    }

    @Override
    public void computeScroll() {
        if(scroller!=null){
            if(scroller.computeScrollOffset()){//判断scroll是否完成
                ((View) getParent()).scrollTo(
                        scroller.getCurrX(),scroller.getCurrY()
                );//执行本段位移

                invalidate();//进行下段位移
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                scroller.startScroll(((int) getX()), ((int) getY()), ((int) getX())*direction,
                        ((int) getY())*direction);//开始位移,真正开始是在下面的invalidate
                direction*=-1;//改变方向
                invalidate();//开始执行位移
                break;
        }
        return super.onTouchEvent(event);
    }
}
复制代码

3.属性动画:

动画对View进行滑动: setTranslationX,setTranslationY。

6.ScrollView里面有一个button,然后按住button向上滑,讲述事件传递过程?

面试系列之事件分发机制全解析

这里的情形可以理解为上图的情景,大家可以自行带入场景。

当手指按下时由于scrollView中onInterceptTouchEvent没对down事件进行拦截同时button的onTouchEvent是默认返回true的(clickable=true)那么button首先会消耗down事件,当我们手指移动时会触发MOVE事件,这时ScrollView的拦截事件将进行拦截(onInterceptTouchEvent在MOVE时返回true)同时会发送 CANCLE事件给button(CANCLE的触发时机是父View进行拦截后会发送给原先处理事件的子View通知它不要处理后续事件了),之后的MOVE和UP事件将直接交由ScrollView的onTouchEvent去处理同时其自身的onInterceptTouchEvent不会再被触发(onInterceptTouchEvent返回true后将不被调用)。

7.按住一个button,然后手指移到别处,click事件还能不能响应?

不能响应。

当手指移动时在View的OnTouchEvent的MOVE事件中会不断检测当前手指是否在View区域内,如果出了View区域的话那么会将mPressed这个标志位置为false,当手指抬起时在UP事件中如果mPressed为false的话将不会触发任何响应(一定要注意的是会触发MOVE和UP事件,因为一个View在DOWN事件返回true后后续的事件序列都会交给其去处理,只不过在这种情况下没有任何响应效果)。

View的onTouchEvent:

public boolean onTouchEvent(MotionEvent event) {
   ....
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                //如果mPrivateFlags为false则prepressed为false,将不会执行后续UP事件中的任何逻辑
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                ....
                }
                break;
            ...
            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);
                              //判断手指是否在View的区域中
                if (!pointInView(x, y, mTouchSlop)) {
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        removeLongPressCallback();
                        //如果手指移出View区域将改变mPrivateFlags
                        setPressed(false);
                    }
                }
                break;
        }
        return true;
    }
    return false;
}
复制代码

以上所述就是小编给大家介绍的《面试系列之事件分发机制全解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Graph Theory

Introduction to Graph Theory

Douglas B. West / Prentice Hall / 2000-9-1 / USD 140.00

For undergraduate or graduate courses in Graph Theory in departments of mathematics or computer science. This text offers a comprehensive and coherent introduction to the fundamental topics of graph ......一起来看看 《Introduction to Graph Theory》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器