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

详细分析Android中onTouch事件传递机制

程序员文章站 2024-03-05 14:08:54
ontach介绍 ontach是android系统中整个事件机制的基础。android中的其他事件,如onclick、onlongclick等都是以ontach为基础的。...

ontach介绍

ontach是android系统中整个事件机制的基础。android中的其他事件,如onclick、onlongclick等都是以ontach为基础的。

ontach包括从手指按下到离开手机屏幕的整个过程,在微观形式上,具体表现为action_down、action_move和action_up等过程。

ontach两种主要定义形式如下:

1.在自定义控件中,常见的有重写ontouchevent(motionevent ev)方法。如在开发中经常可以看到重写的ontouchevent方法,

并且其中有针对不同的微观表现(action_down、action_move和action_up等)做出的相应判断,执行逻辑并可能返回不同的布尔值。

2.在代码中,直接对现有控件设置setontouchlistener监听器。并重写监听器的ontouch方法。ontouch回调函数中有view和motionevent

ontouch事件传递机制

大家都知道一般我们使用的ui控件都是继承自共同的父类——view。所以view这个类应该掌管着ontouch事件的相关处理。那就让我们去看看:在view中寻找touch相关的方法,其中一个很容易地引起了我们的注意: dispatchtouchevent(motionevent event)

根据方法名的意思应该是负责分发触摸事件的,下面给出了源码:

/**
 * pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event the motion event to be dispatched.
 * @return true if the event was handled by the view, false otherwise.
 */
 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;
}

源码有点长,但我们不必每一行都看。首先注意到dispatchtouchevent的返回值是boolean类型的,注释上的解释:@return true if the event was handled by the view, false otherwise.也就是说如果该触摸事件被这个view消费了就返回true,否则返回false。在方法中首先判断了该event是否是否得到了焦点,如果没有得到焦点直接返回false。然后让我们把目光转向if (li != null && li.montouchlistener != null&& (mviewflags & enabled_mask) == enabled&& li.montouchlistener.ontouch(this, event))这个片段,看到这里有一个名为li的局部变量,属于 listenerinfo 类,经 mlistenerinfo 赋值得到。listenerinfo只是一个包装类,里面封装了大量的监听器。

再在 view 类中去寻找 mlistenerinfo ,可以看到下面的代码:

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

因此我们可以知道mlistenerinfo是不为空的,所以li也不是空,第一个判断为true,然后看到li.montouchlistener,前面说过listenerinfo是一个监听器的封装类,所以我们同样去追踪montouchlistener:

/**
 * register a callback to be invoked when a touch event is sent to this view.
 * @param l the touch listener to attach to this view
 */
public void setontouchlistener(ontouchlistener l) {
 getlistenerinfo().montouchlistener = l;
}

正是通过上面的方法来设置 montouchlistener 的,我想上面的方法大家肯定都很熟悉吧,正是我们平时经常用的 xxx.setontouchlistener ,好了我们从中得知如果设置了ontouchlistener则第二个判断也为true,第三个判断为如果该view是否为enable,默认都是enable的,所以同样为true。还剩最后一个:li.montouchlistener.ontouch(this, event) ,显然是回调了第二个判断中监听器的ontouch()方法,如果ontouch()方法返回true,则上面四个判断全部为true,dispatchtouchevent()方法会返回true,并且不会执行if (!result && ontouchevent(event))这个判断;而在这个判断中我们又看到了一个熟悉的方法:ontouchevent() 。所以想要执行ontouchevent,则在上面的四个判断中必须至少有一个false。

那就假定我们在ontouch()方法中返回的是false,这样就顺利地执行了ontouchevent,那就看看ontouchevent的源码吧:

/**
 * implement this method to handle touch screen motion events.
 * <p>
 * if this method is used to detect click actions, it is recommended that
 * the actions be performed by implementing and calling
 * {@link #performclick()}. this will ensure consistent system behavior,
 * including:
 * <ul>
 * <li>obeying click sound preferences
 * <li>dispatching onclicklistener calls
 * <li>handling {@link accessibilitynodeinfo#action_click action_click} when
 * accessibility features are enabled
 * </ul>
 *
 * @param event the motion event.
 * @return true if the event was handled, false otherwise.
 */
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;
}

这段源码比 dispatchtouchevent 的还要长,不过同样我们挑重点的看:
if (((viewflags & clickable) == clickable || (viewflags & long_clickable) == long_clickable) || (viewflags & context_clickable) == context_clickable)
看到这句话就大概知道了主要是判断该view是否是可点击的,如果可以点击则接着执行,否则直接返回false。可以看到if里面用switch来判断是哪种触摸事件,但在最后都是返回true的。

还有一点要注意:在 action_up 中会执行 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;
}

可以看到上面的li.monclicklistener.onclick(this); ,没错,我们好像又有了新的发现。根据上面的经验,这句代码会去回调我们设置好的点击事件监听器。也就是我们平常用的xxx.setonclicklistener(listener);

/**
 * register a callback to be invoked when this view is clicked. if this view is not
 * clickable, it becomes clickable.
 *
 * @param l the callback that will run
 *
 * @see #setclickable(boolean)
 */
public void setonclicklistener(@nullable onclicklistener l) {
 if (!isclickable()) {
 setclickable(true);
 }
 getlistenerinfo().monclicklistener = l;
}

我们可以看到上面方法设置正是mlistenerinfo的点击监听器,验证了上面的猜想。到了这里ontouch事件的传递机制基本已经分析完成了,也算是告一段落了。

好了,这下我们可以解决开头的问题了,顺便我们再来小结一下:在dispatchtouchevent中,如果设置了ontouchlistener并且view是enable的,那么首先被执行的是ontouchlistener中的ontouch(view v, motionevent event) 。若ontouch返回true,则dispatchtouchevent不再往下执行并且返回true;不然会执行ontouchevent,在ontouchevent中若view是可点击的,则返回true,不然为false。还有在ontouchevent中若view是可点击以及当前触摸事件为action_up,会执行performclick() ,回调onclicklistener的onclick方法。

下面是我画的一张草图:

详细分析Android中onTouch事件传递机制

还有一点值得注意的地方是:假如当前事件是action_down,只有dispatchtouchevent返回true了之后该view才会接收到接下来的action_move,action_up事件,也就是说只有事件被消费了才能接收接下来的事件。

总结

以上就是关于android中ontouch事件传递机制的详细分析,希望对各位android开发者们的学习或者工作能有一定的帮助,如果有疑问大家可以留言交流。