当前位置: 移动技术网 > 移动技术>移动开发>Android > Android事件的分发机制详解

Android事件的分发机制详解

2019年07月24日  | 移动技术网移动技术  | 我要评论

在分析android事件分发机制前,明确android的两大基础控件类型:view和viewgroup。view即普通的控件,没有子布局的,如button、textview. viewgroup继承自view,表示可以有子控件,如linearlayout、listview这些。今天我们先来了解view的事件分发机制。
先看下代码,非常简单,只有一个button,分别给它注册了onclick和ontouch的点击事件。

btn.setonclicklistener(new view.onclicklistener() {
      @override
      public void onclick(view v) {
        log.i("tag", "this is button onclick event");
      }
    });
    btn.setontouchlistener(new view.ontouchlistener() {
      @override
      public boolean ontouch(view v, motionevent event) {
        log.i("tag", "this is button ontouch action" + event.getaction());
        return false;
      }
    });

运行一下项目,结果如下:
 i/tag: this is button ontouch action0
 i/tag: this is button ontouch action2
 i/tag: this is button ontouch action2
 i/tag: this is button ontouch action1
 i/tag: this is button onclick event 
可以看到,ontouch是有先于onclick执行的,因此事件的传递顺序是先ontouch,在到onclick。具体为什么这样,下面会通过源码来说明。这时,我们可能注意到了,ontouch的方法是有返回值,这里是返回false,我们将它改为true再运行一次,结果如下:
 i/tag: this is button ontouch action0
 i/tag: this is button ontouch action2
 i/tag: this is button ontouch action2
 i/tag: this is button ontouch action2
 i/tag: this is button ontouch action1

对比两次结果,我们发现onclick方法不再执行,为什么会这样,下面我将通过源码给大家一步步理清这个思路。
查看源码时,首先要知道所有view类型控件事件入口都是dispatchtouchevent(),所以我们直接进入到view这个类里面的dispatchtouchevent()方法看一下。 

public boolean dispatchtouchevent(motionevent event) {
    // if the event should be handled by accessibility focus first.
    if (event.istargetaccessibilityfocus()) {
      // we don't have focus or no virtual descendant has it, do not handle the event.
      if (!isaccessibilityfocusedvieworhost()) {
        return false;
      }
      // we have focus and got the event, then use normal event dispatch.
      event.settargetaccessibilityfocus(false);
    }
    boolean result = false;
    if (minputeventconsistencyverifier != null) {
      minputeventconsistencyverifier.ontouchevent(event, 0);
    }
    final int actionmasked = event.getactionmasked();
    if (actionmasked == motionevent.action_down) {
      // defensive cleanup for new gesture
      stopnestedscroll();
    }
    if (onfiltertoucheventforsecurity(event)) {
      //noinspection simplifiableifstatement
      listenerinfo li = mlistenerinfo;
      if (li != null && li.montouchlistener != null
          && (mviewflags & enabled_mask) == enabled
          && li.montouchlistener.ontouch(this, event)) {
        result = true;
      }
      if (!result && ontouchevent(event)) {
        result = true;
      }
    }
    if (!result && minputeventconsistencyverifier != null) {
      minputeventconsistencyverifier.onunhandledevent(event, 0);
    }
    // clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an action_down but we didn't want the rest
    // of the gesture.
    if (actionmasked == motionevent.action_up ||
        actionmasked == motionevent.action_cancel ||
        (actionmasked == motionevent.action_down && !result)) {
      stopnestedscroll();
    }
    return result;
  }

从源码第25行处可以看到,montouchlistener.ontouch()的方法首先被执行,如果li != null && li.montouchlistener != null&& (mviewflags & enabled_mask) == enabled&& li.montouchlistener.ontouch(this, event)都为真的话,result赋值为true,否则就执行ontouchevent(event)方法。

从上面可以看到要符合条件有四个,
 1、listenerinfo li,它是view中的一个静态类,里面定义view的事件的监听等等,所以有涉及到view的事件,listenerinfo都会被实例化,因此li不为null
 2、montouchilistener是在setontouchlistener方法里面赋值的,只要touch事件被注册,montouchilistener一定不会null
 3、 (mviewflags & enabled_mask) == enabled,是判断当前点击的控件是否是enable的,button默认为enable,这个条件也恒定为true,
 4、重点来了,li.montouchlistener.ontouch(this, event)就是回调控件ontouch方法,当这个条件也为true时,result=true,ontouchevent(event)将不会被执行。如果ontouch返回false,就会再执行ontouchevent(event)方法。
我们接着再进入到ontouchevent方法查看源码。

public boolean ontouchevent(motionevent event) {
    final float x = event.getx();
    final float y = event.gety();
    final int viewflags = mviewflags;
    final int action = event.getaction();
    if ((viewflags & enabled_mask) == disabled) {
      if (action == motionevent.action_up && (mprivateflags & pflag_pressed) != 0) {
        setpressed(false);
      }
      // a disabled view that is clickable still consumes the touch
      // events, it just doesn't respond to them.
      return (((viewflags & clickable) == clickable
          || (viewflags & long_clickable) == long_clickable)
          || (viewflags & context_clickable) == context_clickable);
    }
    if (mtouchdelegate != null) {
      if (mtouchdelegate.ontouchevent(event)) {
        return true;
      }
    }
    if (((viewflags & clickable) == clickable ||
        (viewflags & long_clickable) == long_clickable) ||
        (viewflags & context_clickable) == context_clickable) {
      switch (action) {
        case motionevent.action_up:
          boolean prepressed = (mprivateflags & pflag_prepressed) != 0;
          if ((mprivateflags & pflag_pressed) != 0 || prepressed) {
            // take focus if we don't have it already and we should in
            // touch mode.
            boolean focustaken = false;
            if (isfocusable() && isfocusableintouchmode() && !isfocused()) {
              focustaken = requestfocus();
            }
            if (prepressed) {
              // the button is being released before we actually
              // showed it as pressed. make it show the pressed
              // state now (before scheduling the click) to ensure
              // the user sees it.
              setpressed(true, x, y);
            }
            if (!mhasperformedlongpress && !mignorenextupevent) {
              // this is a tap, so remove the longpress check
              removelongpresscallback();
              // only perform take click actions if we were in the pressed state
              if (!focustaken) {
                // use a runnable and post this rather than calling
                // performclick directly. this lets other visual state
                // of the view update before click actions start.
                if (mperformclick == null) {
                  mperformclick = new performclick();
                }
                if (!post(mperformclick)) {
                  performclick();
                }
              }
            }
            if (munsetpressedstate == null) {
              munsetpressedstate = new unsetpressedstate();
            }
            if (prepressed) {
              postdelayed(munsetpressedstate,
                  viewconfiguration.getpressedstateduration());
            } else if (!post(munsetpressedstate)) {
              // if the post failed, unpress right now
              munsetpressedstate.run();
            }
            removetapcallback();
          }
          mignorenextupevent = false;
          break;
        case motionevent.action_down:
          mhasperformedlongpress = false;
          if (performbuttonactionontouchdown(event)) {
            break;
          }
          // walk up the hierarchy to determine if we're inside a scrolling container.
          boolean isinscrollingcontainer = isinscrollingcontainer();
          // for views inside a scrolling container, delay the pressed feedback for
          // a short period in case this is a scroll.
          if (isinscrollingcontainer) {
            mprivateflags |= pflag_prepressed;
            if (mpendingcheckfortap == null) {
              mpendingcheckfortap = new checkfortap();
            }
            mpendingcheckfortap.x = event.getx();
            mpendingcheckfortap.y = event.gety();
            postdelayed(mpendingcheckfortap, viewconfiguration.gettaptimeout());
          } else {
            // not inside a scrolling container, so show the feedback right away
            setpressed(true, x, y);
            checkforlongclick(0);
          }
          break;
        case motionevent.action_cancel:
          setpressed(false);
          removetapcallback();
          removelongpresscallback();
          mincontextbuttonpress = false;
          mhasperformedlongpress = false;
          mignorenextupevent = false;
          break;
        case motionevent.action_move:
          drawablehotspotchanged(x, y);
          // be lenient about moving outside of buttons
          if (!pointinview(x, y, mtouchslop)) {
            // outside button
            removetapcallback();
            if ((mprivateflags & pflag_pressed) != 0) {
              // remove any future long press/tap checks
              removelongpresscallback();
              setpressed(false);
            }
          }
          break;
      }
      return true;
    }
    return false;
  }

从源码的21行我们可以看出,该控件可点击就会进入到switch判断中,当我们触发了手指离开的实际,则会进入到motionevent.action_up这个case当中。我们接着往下看,在源码的50行,调用到了mperformclick()方法,我们继续进入到这个方法的源码看看。 

public boolean performclick() {
    final boolean result;
    final listenerinfo li = mlistenerinfo;
    if (li != null && li.monclicklistener != null) {
      playsoundeffect(soundeffectconstants.click);
      li.monclicklistener.onclick(this);
      result = true;
    } else {
      result = false;
    }
    sendaccessibilityevent(accessibilityevent.type_view_clicked);
    return result;
  }

现在我们可以看到,只要listenerinfo和monclicklistener不为null就会调用onclick这个方法,之前说过,只要有监听事件,listenerinfo就不为null,带monclicklistener又是在哪里赋值呢?我们再继续看下它的源码。

public void setonclicklistener(@nullable onclicklistener l) {
    if (!isclickable()) {
      setclickable(true);
    }
    getlistenerinfo().monclicklistener = l;
  }

看到这里一切就清楚了,当我们调用setonclicklistener方法来给按钮注册一个点击事件时,就会给monclicklistener赋值。整个分发事件的顺序是ontouch()-->ontouchevent(event)-->performclick()-->onclick()。
 现在我们可以解决之前的问题。
1、ontouch方法是优先于onclick,所以是执行了ontouch,再执行onclick。 
2、无论是dispatchtouchevent还是ontouchevent,如果返回true表示这个事件已经被消费、处理了,不再往下传了。在dispathtouchevent的源码里可以看到,如果ontouchevent返回了true,那么它也返回true。如果dispatchtouchevent在执行ontouch监听的时候,ontouch返回了true,那么它也返回true,这个事件提前被ontouch消费掉了。就不再执行ontouchevent了,更别说onclick监听了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网