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

android事件分发机制的实现原理

程序员文章站 2023-12-18 17:33:52
android中的事件处理,以及解决滑动冲突问题都离不开事件分发机制,android中的事件流,即motionevent都会经历一个从分发,拦截到处理的一个过程。即disp...

android中的事件处理,以及解决滑动冲突问题都离不开事件分发机制,android中的事件流,即motionevent都会经历一个从分发,拦截到处理的一个过程。即dispatchtouchevent(),oninterceptevent()到ontouchevent()的一个过程,在dispatchtouchevent()负责了事件的分发过程,在dispatchtouchevent()中会调用oninterceptevent()与ontouchevent(),如果oninterceptevent()返回true,那么会调用到当前view的ontouchevent()方法,如果不拦截,事件就会下发到子view的dispatchtouchevent()中进行同样的操作。本文将带领大家从源码角度来分析android是如何进行事件分发的。

android中的事件分发流程最先从activity的dispatchtouchevent()开始:

public boolean dispatchtouchevent(motionevent ev) {
  if (ev.getaction() == motionevent.action_down) {
    onuserinteraction();
  }
  if (getwidow().superdispatchtouchevent(ev)) {
    return true;
  }
  return ontouchevent(ev);
}

这里调用了getwindow().superdispatchtouchevent(ev),这里可以看出activity将motionevent传寄给了window。而window是一个抽象类,superdispatchtouchevent()也是一个抽象方法,这里用到的是window的子类phonewindow。

@override
public boolean superdispatchtouchevent(motionevent event) {
  return mdecor.superdispatchtouchevent(event);
} 

从这里可以看出,event事件被传到了decorview,也就是我们的顶层view.我们继续跟踪:

public boolean superdispatchtouchevent(motionevent event) {
  return super.dispatchtouchevent(event);
}

这里调用到了父类的dispatchtouchevent()方法,而decorview是继承自framelayout,framelayout继承了viewgroup,所以这里会调用到viewgroup的dispatchtouchevent()方法。

所以整个事件流从activity开始,传递到window,最后再到我们的view(viewgroup也是继承自view)中,而view才是我们整个事件处理的核心阶段。

我们来看一下viewgroup的dispatchtouchevent()中的实现:

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();
    }

这是dispatchtouchevent()开始时截取的一段代码,我们来看一下,首先,当我们手指按下view时,会调用到resettouchstate()方法,在resettouchstate()中:

private void resettouchstate() {
  cleartouchtargets();
  resetcancelnextupflag(this);
  mgroupflags &= ~flag_disallow_intercept;
  mnestedscrollaxes = scroll_axis_none;
}

我们继续跟踪cleartouchtargets()方法:

private void cleartouchtargets() {
  touchtarget target = mfirsttouchtarget;
  if (target != null) {
    do {
      touchtarget next = target.next;
      target.recycle();
      target = next;
    } while (target != null);
    mfirsttouchtarget = null;
  }
}

在cleartouchtargets()方法中,我们最终将mfirsttouchtarget赋值为null,我们继续回到dispatchtouchevent()中,接着执行了下段代码:

// 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;
    }

当view被按下或mfirsttouchtarget != null 的时候,从前面可以知道,当每次view被按下时,也就是重新开始一次事件流的处理时,mfirsttouchtarget都会被设置成null,一会我们看mfirsttouchtarget是什么时候被赋值的。

从disallowintercept属性我们大概能猜到是用来判断是否需要坐拦截处理,而我们知道可以通过调用父view的requestdisallowintercepttouchevent(true)可以让我们的父view不能对事件进行拦截,我们先来看看requestdisallowintercepttouchevent()方法中的实现:

@override
public void requestdisallowintercepttouchevent(boolean disallowintercept) {

  if (disallowintercept == ((mgroupflags & flag_disallow_intercept) != 0)) {
    // we're already in this state, assume our ancestors are too
    return;
  }

  if (disallowintercept) {
    mgroupflags |= flag_disallow_intercept;
  } else {
    mgroupflags &= ~flag_disallow_intercept;
  }

  // pass it up to our parent
  if (mparent != null) {
    mparent.requestdisallowintercepttouchevent(disallowintercept);
  }
}

这里也是通过设置标志位做判断处理,所以这里是通过改变mgroupflags标志,然后在dispatchtouchevent()刚发中变更disallowintercept的值判断是否拦截,当为true时,即需要拦截,这个时候便会跳过onintercepttouchevent()拦截判断,并标记为不拦截,即intercepted = false,我们继续看viewgroup的onintercepttouchevent()处理:

public boolean onintercepttouchevent(motionevent ev) {
  if (ev.isfromsource(inputdevice.source_mouse)
      && ev.getaction() == motionevent.action_down
      && ev.isbuttonpressed(motionevent.button_primary)
      && isonscrollbarthumb(ev.getx(), ev.gety())) {
    return true;
  }
  return false;
}

即默认情况下,只有在action_down时,viewgroup才会表现为拦截。

我们继续往下看:

final int childrencount = mchildrencount;
if (newtouchtarget == null && childrencount != 0) {
   final float x = ev.getx(actionindex);
   final float y = ev.gety(actionindex);
   // find a child that can receive the event.
   // scan children from front to back.
   final arraylist<view> preorderedlist = buildtouchdispatchchildlist();
   final boolean customorder = preorderedlist == null
              && ischildrendrawingorderenabled();
   final view[] children = mchildren;
   for (int i = childrencount - 1; i >= 0; i--) {
      final int childindex = getandverifypreorderedindex(
                childrencount, i, customorder);
            final view child = getandverifypreorderedview(
                preorderedlist, children, childindex);

            // if there is a view that has accessibility focus we want it
            // to get the event first and if not handled we will perform a
            // normal dispatch. we may do a double iteration but this is
            // safer given the timeframe.
            if (childwithaccessibilityfocus != null) {
              if (childwithaccessibilityfocus != child) {
                continue;
              }
              childwithaccessibilityfocus = null;
              i = childrencount - 1;
            }

            if (!canviewreceivepointerevents(child)
                || !istransformedtouchpointinview(x, y, child, null)) {
              ev.settargetaccessibilityfocus(false);
              continue;
            }

            newtouchtarget = gettouchtarget(child);
            if (newtouchtarget != null) {
              // child is already receiving touch within its bounds.
              // give it the new pointer in addition to the ones it is handling.
              newtouchtarget.pointeridbits |= idbitstoassign;
              break;
            }

            resetcancelnextupflag(child);
            if (dispatchtransformedtouchevent(ev, false, child, idbitstoassign)) {
              // child wants to receive touch within its bounds.
              mlasttouchdowntime = ev.getdowntime();
              if (preorderedlist != null) {
                // childindex points into presorted list, find original index
                for (int j = 0; j < childrencount; j++) {
                  if (children[childindex] == mchildren[j]) {
                    mlasttouchdownindex = j;
                    break;
                  }
                }
              } else {
                mlasttouchdownindex = childindex;
              }
              mlasttouchdownx = ev.getx();
              mlasttouchdowny = ev.gety();
              newtouchtarget = addtouchtarget(child, idbitstoassign);
              alreadydispatchedtonewtouchtarget = true;
              break;
            }

            // the accessibility focus didn't handle the event, so clear
            // the flag and do a normal dispatch to all children.
            ev.settargetaccessibilityfocus(false);
          }
          if (preorderedlist != null) preorderedlist.clear();
        }

这段代码首先会通过一个循环去遍历所有的子view,最终会调用到dispatchtransformedtouchevent()方法,我们继续看dispatchtransformedtouchevent()的实现:

private boolean dispatchtransformedtouchevent(motionevent event, boolean cancel,
    view child, int desiredpointeridbits) {
  final boolean handled;

  // canceling motions is a special case. we don't need to perform any transformations
  // or filtering. the important part is the action, not the contents.
  final int oldaction = event.getaction();
  if (cancel || oldaction == motionevent.action_cancel) {
    event.setaction(motionevent.action_cancel);
    if (child == null) {
      handled = super.dispatchtouchevent(event);
    } else {
      handled = child.dispatchtouchevent(event);
    }
    event.setaction(oldaction);
    return handled;
  }

  // calculate the number of pointers to deliver.
  final int oldpointeridbits = event.getpointeridbits();
  final int newpointeridbits = oldpointeridbits & desiredpointeridbits;

  // if for some reason we ended up in an inconsistent state where it looks like we
  // might produce a motion event with no pointers in it, then drop the event.
  if (newpointeridbits == 0) {
    return false;
  }

  // if the number of pointers is the same and we don't need to perform any fancy
  // irreversible transformations, then we can reuse the motion event for this
  // dispatch as long as we are careful to revert any changes we make.
  // otherwise we need to make a copy.
  final motionevent transformedevent;
  if (newpointeridbits == oldpointeridbits) {
    if (child == null || child.hasidentitymatrix()) {
      if (child == null) {
        handled = super.dispatchtouchevent(event);
      } else {
        final float offsetx = mscrollx - child.mleft;
        final float offsety = mscrolly - child.mtop;
        event.offsetlocation(offsetx, offsety);

        handled = child.dispatchtouchevent(event);

        event.offsetlocation(-offsetx, -offsety);
      }
      return handled;
    }
    transformedevent = motionevent.obtain(event);
  } else {
    transformedevent = event.split(newpointeridbits);
  }

  // perform any necessary transformations and dispatch.
  if (child == null) {
    handled = super.dispatchtouchevent(transformedevent);
  } else {
    final float offsetx = mscrollx - child.mleft;
    final float offsety = mscrolly - child.mtop;
    transformedevent.offsetlocation(offsetx, offsety);
    if (! child.hasidentitymatrix()) {
      transformedevent.transform(child.getinversematrix());
    }

    handled = child.dispatchtouchevent(transformedevent);
  }

  // done.
  transformedevent.recycle();
  return handled;
}

这段代码就比较明显了,如果child不为null,始终会调用到child.dispatchtouchevent();否则调用super.dispatchtouchevent();

如果child不为null时,事件就会向下传递,如果子view处理了事件,即dispatchtransformedtouchevent()即返回true。继续向下执行到addtouchtarget()方法,我们继续看addtouchtarget()方法的执行结果:

private touchtarget addtouchtarget(@nonnull view child, int pointeridbits) {
  final touchtarget target = touchtarget.obtain(child, pointeridbits);
  target.next = mfirsttouchtarget;
  mfirsttouchtarget = target;
  return target;
}

这个时候我们发现mfirsttouchtarget又出现了,这时候会给mfirsttouchtarget重新赋值,即mfirsttouchtarget不为null。也就是说,如果事件被当前view或子view消费了,那么在接下来的action_move或action_up事件中,mfirsttouchtarget就不为null。但如果我们继承了该viewgroup,并在onintercepttouchevent()的action_move中拦截了事件,那么后续事件将不会下发,将由该viewgroup直接处理,从下面代码我们可以得到:

// dispatch to touch targets, excluding the new touch target if we already
      // dispatched to it. cancel touch targets if necessary.
      touchtarget predecessor = null;
      touchtarget target = mfirsttouchtarget;
      while (target != null) {
        final touchtarget next = target.next;
        if (alreadydispatchedtonewtouchtarget && target == newtouchtarget) {
          handled = true;
        } else {
          final boolean cancelchild = resetcancelnextupflag(target.child)
              || intercepted;
          if (dispatchtransformedtouchevent(ev, cancelchild,
              target.child, target.pointeridbits)) {
            handled = true;
          }
          if (cancelchild) {
            if (predecessor == null) {
              mfirsttouchtarget = next;
            } else {
              predecessor.next = next;
            }
            target.recycle();
            target = next;
            continue;
          }
        }
        predecessor = target;
        target = next;
      }

当存在子view并且事件被子view消费时,即在action_down阶段mfirsttouchtarget会被赋值,即在接下来的action_move事件中,由于intercepted为true,所以将action_cancel 事件传递过去,从dispatchtransformedtouchevent()中可以看到:

if (cancel || oldaction == motionevent.action_cancel) {
    event.setaction(motionevent.action_cancel);
    if (child == null) {
      handled = super.dispatchtouchevent(event);
    } else {
      handled = child.dispatchtouchevent(event);
    }
    event.setaction(oldaction);
    return handled;
  }

并将mfirsttouchtarget 最终赋值为 next,而此时mfirsttouchtarget位于touchtarget链表尾部,所以mfirsttouchtarget会赋值为null,那么接下来的事件将不会进入到onintercepttouchevent()中。也就会直接交由该view处理。

如果我们没有进行事件的拦截,而是交由子view去处理,由于viewgroup的onintercepttouchevent()默认并不会拦截除了action_down以外的事件,所以后续事件将继续交由子view去处理,如果存在子view且事件位于子view内部区域的话。

所以无论是否进行拦截,事件流都会交由view的dispatchtouchevent()中进行处理,我们接下来跟踪一下view中的dispatchtouchevent()处理过程:

if (actionmasked == motionevent.action_down) {
    // defensive cleanup for new gesture
    stopnestedscroll();
  }

  if (onfiltertoucheventforsecurity(event)) {
    if ((mviewflags & enabled_mask) == enabled && handlescrollbardragging(event)) {
      result = true;
    }
    //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;
    }
  }

当被按下时,即action_down时,view会停止内部的滚动,如果view没有被覆盖或遮挡时,首先会进行mlistenerinfo是否为空的判断,我们看下mlistenerinfo是在哪里初始化的:

listenerinfo getlistenerinfo() {
  if (mlistenerinfo != null) {
    return mlistenerinfo;
  }
  mlistenerinfo = new listenerinfo();
  return mlistenerinfo;
}

这里可以看出,mlistenerinfo一般不会是null,知道在我们使用它时调用过这段代码,而当view被加入window中的时候,会调用下面这段代码,从注释中也可以看出来:

/**
 * add a listener for attach state changes.
 *
 * this listener will be called whenever this view is attached or detached
 * from a window. remove the listener using
 * {@link #removeonattachstatechangelistener(onattachstatechangelistener)}.
 *
 * @param listener listener to attach
 * @see #removeonattachstatechangelistener(onattachstatechangelistener)
 */
public void addonattachstatechangelistener(onattachstatechangelistener listener) {
  listenerinfo li = getlistenerinfo();
  if (li.monattachstatechangelisteners == null) {
    li.monattachstatechangelisteners
        = new copyonwritearraylist<onattachstatechangelistener>();
  }
  li.monattachstatechangelisteners.add(listener);
}

到这里我们就知道,mlistenerinfo一开始就是被初始化好了的,所以li不可能为null,li.montouchlistener != null即当设置了touchlistener时不为null,并且view是enabled状态,一般情况view都是enable的。这个时候会调用到ontouch()事件,当ontouch()返回true时,这个时候result会赋值true。而当result为true时,ontouchevent()将不会被调用。

从这里可以看出,ontouch()会优先ontouchevent()调用;
当view设置touch监听并返回true时,那么它的ontouchevent()将被屏蔽。否则会调用ontouchevent()处理。

那么让我们继续来看看ontouchevent()中的事件处理:

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);
  }

首先,当view状态是disabled时,只要view是clickable或long_clickable或context_clickable,都会返回true,而button默认是clickable的,textview默认不是clickable的,而view一般默认都不是long_clickable的。

我们继续向下看:

if (mtouchdelegate != null) {
    if (mtouchdelegate.ontouchevent(event)) {
      return true;
    }
  }

如果有代理事件,仍然会返回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, x, y);
        }
        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;
  }

当view是clickable或long_clickable或context_clickable状态时,当手指抬起时,如果设置了click监听,最终会调用到performclick(),触发click()事件。这点从performclick()方法中可以看出:

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;
}

从这里我们也可以得出,click事件会在ontouchevent()中被调用,如果view设置了ontouch()监听并返回true,那么click事件也会被屏蔽掉,不过我们可以在ontouch()中通过调用view的performclick()继续执行click()事件,这个就看我们的业务中的需求了。

从这里我们可以看出,如果事件没有被当前view或子view处理,即返回false,那么事件就会交由外层view继续处理,直到被消费。

如果事件一直没有被处理,会最终传递到activity的ontouchevent()中。

到这里我们总结一下:

事件是从activity->window->view(viewgroup)的一个传递流程;

如果事件没有被中途拦截,那么它会一直传到最内层的view控件;

如果事件被某一层拦截,那么事件将不会向下传递,交由该view处理。如果该view消费了事件,那么接下来的事件也会交由该view处理;如果该view没有消费该事件,那么事件会交由外层view处理,...并最终调用到activity的ontouchevent()中,除非某一层消费了该事件;

一个事件只能交由一个view处理;

dispatchtouchevent()总是会被调用,而且最先被调用,onintercepttouchevent()和ontouchevent()在dispatchtouchevent()内部调用;

子view不能干扰viewgroup对action_down事件的处理;

子view可以通过requestdisallowintercepttouchevent(true)控制父view不对事件进行拦截,跳过onintercepttouchevent()方法的执行。

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

上一篇:

下一篇: