欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android事件的分发机制详解

程序员文章站 2024-03-02 19:40:46
在分析android事件分发机制前,明确android的两大基础控件类型:view和viewgroup。view即普通的控件,没有子布局的,如button、textview...

在分析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监听了。

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