Android触摸屏事件派发机制详解与源码分析一(View篇)

public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener {

private LinearLayout mLayout;

private Button mButton;

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

mLayout = (LinearLayout) this.findViewById(R.id.mylayout);

mButton = (Button) this.findViewById(R.id.my_btn);

mLayout.setOnTouchListener(this);

mButton.setOnTouchListener(this);

mLayout.setOnClickListener(this);

mButton.setOnClickListener(this);

}

@Override

public boolean onTouch(View v, MotionEvent event) {

Log.i(null, “OnTouchListener–onTouch-- action=”+event.getAction()+" --"+v);

return false;

}

@Override

public void onClick(View v) {

Log.i(null, “OnClickListener–onClick–”+v);

}

}

2-2 现象

如上代码很简单,但凡学过几天Android的人都能看懂吧。Activity中有一个LinearLayout(ViewGroup的子类,ViewGroup是View的子类)布局,布局中包含一个按钮(View的子类);然后分别对这两个控件设置了Touch与Click的监听事件,具体运行结果如下:

  1. 当稳稳的点击Button时打印如下:

这里写图片描述

  1. 当稳稳的点击除过Button以外的其他地方时打印如下:

这里写图片描述

  1. 当收指点击Button时按在Button上晃动了一下松开后的打印如下:

这里写图片描述

机智的你看完这个结果指定知道为啥吧?

我们看下onTouch和onClick,从参数都能看出来onTouch比onClick强大灵活,毕竟多了一个event参数。这样onTouch里就可以处理ACTION_DOWN、ACTION_UP、ACTION_MOVE等等的各种触摸。现在来分析下上面的打印结果;在1中,当我们点击Button时会先触发onTouch事件(之所以打印action为0,1各一次是因为按下抬起两个触摸动作被触发)然后才触发onClick事件;在2中也同理类似1;在3中会发现onTouch被多次调运后才调运onClick,是因为手指晃动了,所以触发了ACTION_DOWN->ACTION_MOVE…->ACTION_UP。

如果你眼睛比较尖你会看见onTouch会有一个返回值,而且在上面返回了false。你可能会疑惑这个返回值有啥效果?那就验证一下吧,我们将上面的onTouch返回值改为ture。如下:

@Override

public boolean onTouch(View v, MotionEvent event) {

Log.i(null, “OnTouchListener–onTouch-- action=”+event.getAction()+" --"+v);

return true;

}

再次点击Button结果如下:

这里写图片描述

看见了吧,如果onTouch返回true则onClick不会被调运了。

2-3 总结结论

好了,经过这个简单的实例验证你可以总结发现:

  1. Android控件的Listener事件触发顺序是先触发onTouch,其次onClick。

  2. 如果控件的onTouch返回true将会阻止事件继续传递,返回false事件会继续传递。

对于伸手党码农来说其实到这足矣应付常规的App事件监听处理使用开发了,但是对于复杂的事件监听处理或者想自定义控件的码农来说这才是刚刚开始,只是个热身。既然这样那就继续喽。。。

3 Android 5.1.1(API 22) View触摸屏事件传递源码分析


3-1 写在前面的话

其实Android源码无论哪个版本对于触摸屏事件的传递机制都类似,这里只是选用了目前最新版本的源码来分析而已。分析Android View事件传递机制之前有必要先看下源码的一些关系,如下是几个继承关系图:

这里写图片描述

这里写图片描述

怎么样?看了官方这个继承图是不是明白了上面例子中说的LinearLayout是ViewGroup的子类,ViewGroup是View的子类,Button是View的子类关系呢?其实,在Android中所有的控件无非都是ViewGroup或者View的子类,说高尚点就是所有控件都是View的子类。

这里通过继承关系是说明一切控件都是View,同时View与ViewGroup又存在一些区别,所以该模块才只单单先分析View触摸屏事件传递机制。

3-2 从View的dispatchTouchEvent方法说起

在Android中你只要触摸控件首先都会触发控件的dispatchTouchEvent方法(其实这个方法一般都没在具体的控件类中,而在他的父类View中),所以我们先来看下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;

}

dispatchTouchEvent的代码有点长,咱们看重点就可以。前面都是设置一些标记和处理input与手势等传递,到24行的if (onFilterTouchEventForSecurity(event))语句判断当前View是否没被遮住等,接着26行定义ListenerInfo局部变量,ListenerInfo是View的静态内部类,用来定义一堆关于View的XXXListener等方法;接着if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句就是重点,首先li对象自然不会为null,li.mOnTouchListener呢?你会发现ListenerInfo的mOnTouchListener成员是在哪儿赋值的呢?怎么确认他是不是null呢?通过在View类里搜索可以看到:

/**

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

}

li.mOnTouchListener是不是null取决于控件(View)是否设置setOnTouchListener监听,在上面的实例中我们是设置过Button的setOnTouchListener方法的,所以也不为null;接着通过位与运算确定控件(View)是不是ENABLED 的,默认控件都是ENABLED 的;接着判断onTouch的返回值是不是true。通过如上判断之后如果都为true则设置默认为false的result为true,那么接下来的if (!result && onTouchEvent(event))就不会执行,最终dispatchTouchEvent也会返回true。而如果if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))语句有一个为false则if (!result && onTouchEvent(event))就会执行,如果onTouchEvent(event)返回false则dispatchTouchEvent返回false,否则返回true。

这下再看前面的实例部分明白了吧?控件触摸就会调运dispatchTouchEvent方法,而在dispatchTouchEvent中先执行的是onTouch方法,所以验证了实例结论总结中的onTouch优先于onClick执行道理。如果控件是ENABLE且在onTouch方法里返回了true则dispatchTouchEvent方法也返回true,不会再继续往下执行;反之,onTouch返回false则会继续向下执行onTouchEvent方法,且dispatchTouchEvent的返回值与onTouchEvent返回值相同。

所以依据这个结论和上面实例打印结果你指定已经大胆猜测认为onClick一定与onTouchEvent有关系?是不是呢?先告诉你,是的。下面我们会分析。

3-2-1 总结结论

在View的触摸屏传递机制中通过分析dispatchTouchEvent方法源码我们会得出如下基本结论:

  1. 触摸控件(View)首先执行dispatchTouchEvent方法。

  2. 在dispatchTouchEvent方法中先执行onTouch方法,后执行onClick方法(onClick方法在onTouchEvent中执行,下面会分析)。

  3. 如果控件(View)的onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的情况下会调运onTouchEvent,dispatchTouchEvent返回值与onTouchEvent返回一样。

  4. 如果控件不是enable的设置了onTouch方法也不会执行,只能通过重写控件的onTouchEvent方法处理(上面已经处理分析了),dispatchTouchEvent返回值与onTouchEvent返回一样。

  5. 如果控件(View)是enable且onTouch返回true情况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent方法。

上面说了onClick一定与onTouchEvent有关系,那么接下来就分析分析dispatchTouchEvent方法中的onTouchEvent方法。

3-3 继续说说View的dispatchTouchEvent方法中调运的onTouchEvent方法

上面说了dispatchTouchEvent方法中如果onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的情况下会调运onTouchEvent,所以接着看就知道了,如下:

public boolean onTouchEvent(MotionEvent event) {

final float x = event.getX();

final float y = event.getY();

final int viewFlags = mViewFlags;

if ((viewFlags & ENABLED_MASK) == DISABLED) {

if (event.getAction() == 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));

}

if (mTouchDelegate != null) {

if (mTouchDelegate.onTouchEvent(event)) {

return true;

}

}

if (((viewFlags & CLICKABLE) == CLICKABLE ||

(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

switch (event.getAction()) {

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) {

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

}

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

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;

}

我勒个去!一个方法比一个方法代码多。好吧,那咱们继续只挑重点来说明呗。

首先地6到14行可以看出,如果控件(View)是disenable状态,同时是可以clickable的则onTouchEvent直接消费事件返回true,反之如果控件(View)是disenable状态,同时是disclickable的则onTouchEvent直接false。多说一句,关于控件的enable或者clickable属性可以通过java或者xml直接设置,每个view都有这些属性。

接着22行可以看见,如果一个控件是enable且disclickable则onTouchEvent直接返回false了;反之,如果一个控件是enable且clickable则继续进入过于一个event的switch判断中,然后最终onTouchEvent都返回了true。switch的ACTION_DOWN与ACTION_MOVE都进行了一些必要的设置与置位,接着到手抬起来ACTION_UP时你会发现,首先判断了是否按下过,同时是不是可以得到焦点,然后尝试获取焦点,然后判断如果不是longPressed则通过post在UI Thread中执行一个PerformClick的Runnable,也就是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;

}

这个方法也是先定义一个ListenerInfo的变量然后赋值,接着判断li.mOnClickListener是不是为null,决定执行不执行onClick。你指定现在已经很机智了,和onTouch一样,搜一下mOnClickListener在哪赋值的呗,结果发现:

public void setOnClickListener(OnClickListener l) {

if (!isClickable()) {

setClickable(true);

}

getListenerInfo().mOnClickListener = l;

}

看见了吧!控件只要监听了onClick方法则mOnClickListener就不为null,而且有意思的是如果调运setOnClickListener方法设置监听且控件是disclickable的情况下默认会帮设置为clickable。

我勒个去!!!惊讶吧!!!猜的没错onClick就在onTouchEvent中执行的,而且是在onTouchEvent的ACTION_UP事件中执行的。

3-3-1 总结结论
  1. onTouchEvent方法中会在ACTION_UP分支中触发onClick的监听。

  2. 当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action。

到此上面例子中关于Button点击的各种打印的真实原因都找到了可靠的证据,也就是说View的触摸屏事件传递机制其实也就这么回事。

4 透过源码继续进阶实例验证


其实上面分析完View的触摸传递机制之后已经足够用了。如下的实例验证可以说是加深阅读源码的理解,还有一个主要作用就是为将来自定义控件打下坚实基础。因为自定义控件中时常会与这几个方法打交道。

4-1 例子

我们自定义一个Button(Button实质继承自View),如下:

public class TestButton extends Button {

public TestButton(Context context, AttributeSet attrs) {

super(context, attrs);

}

@Override

public boolean dispatchTouchEvent(MotionEvent event) {

Log.i(null, “dispatchTouchEvent-- action=” + event.getAction());

return super.dispatchTouchEvent(event);

}

@Override

public boolean onTouchEvent(MotionEvent event) {

Log.i(null, “onTouchEvent-- action=”+event.getAction());

return super.onTouchEvent(event);

}

}

其他代码如下:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”

android:orientation=“vertical”

android:gravity=“center”

android:layout_width=“fill_parent”

android:layout_height=“fill_parent”

android:id=“@+id/mylayout”>

<com.zzci.light.TestButton

android:id=“@+id/my_btn”

android:layout_width=“match_parent”

android:layout_height=“wrap_content”

android:text=“click test”/>

public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener {

private LinearLayout mLayout;

private TestButton mButton;

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

mLayout = (LinearLayout) this.findViewById(R.id.mylayout);

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

[外链图片转存中…(img-lazTJdIA-1714456071494)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-HsfCU6YQ-1714456071495)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值