关键类 | 路径 |
---|---|
Activity.java | frameworks/base/core/java/android/app/Activity.java |
DecorView.java | frameworks/base/core/java/com/android/internal/policy/DecorView.java |
PhoneWindow.java | frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java |
View.java | frameworks/base/core/java/android/view/View.java |
ViewGroup.java | frameworks/base/core/core/java/android/view/ViewGroup.java |
Window.java | frameworks/base/core/java/android/view/Window.java |
Android “View” 虽然不是四大组件,但其并不比四大组件的地位低。而 View 的核心知识点 “事件分发机制” 则是很多刚入门童鞋的拦路虎(一、项目中到处遇到事件分发机制;二、面试管最喜欢说起的问题)。java
在实际项目的开发过程当中,ScrollView 嵌套 RecyclerView(或者 ListView)的滑动冲突这种老大难问题的理论基础就是“事件分发机制”。android
首先,咱们了解一下“事件分发机制”中的基础知识点!面试
事件分发的对象:点击事件(Touch事件)app
1. 定义ide
当用户触摸屏幕时(View 或 ViewGroup 派生的控件),将产生点击事件(Touch事件)函数
Touch 事件的相关细节(发生触摸的位置、时间等)被封装成 MotionEvent 对象。
2. 事件类型布局
事件类型 | 具体动做 |
---|---|
MotionEvent.ACTION_DOWN | 按下 View(全部事件的开始) |
MotionEvent.ACTION_UP | 抬起 View(与DOWN对应) |
MotionEvent.ACTION_MOVE | 滑动 View |
MotionEvent.ACTION_CANCEL | 结束事件(非人为缘由) |
3. 事件序列post
所谓事件序列就是指:从手指接触屏幕至手指离开屏幕,这个过程产生的一系列事件。测试
通常状况下,事件列都是以 DOWN 事件开始、UP 事件结束,中间有无数的 MOVE 事件。ui
以下图:
即当一个点击事件(MotionEvent )产生后,系统需把这个事件传递给一个具体的 View 去处理。
本质: 将点击事件(MotionEvent)传递到某个具体的 View 并处理的整个过程。
即:事件传递的过程 = 分发过程
对象: Activity、ViewGroup、View
Android 的 UI 界面由 Activity、ViewGroup、View 及其派生类组成。
以下图:
顺序: Activity -> ViewGroup -> View
即:1个点击事件发生后,事件先传到 Activity、再传到 ViewGroup、最终再传到 View。
事件分发的顺序流程图以下:
三个重要方法: dispatchTouchEvent() 、onInterceptTouchEvent() 和 onTouchEvent()。
dispatchTouchEvent(事件分发):当监听到有触发事件时,首先由 Activity 进行捕获,而后事件就进入事件分发的流程。Activity 自己没有事件拦截,从而将事件传递给最外层的 View 的 dispatchTouchEvent(MotionEvent ev),该方法将对事件进行分发。
返回值:表示是否消费了当前事件。多是 View 自己的 onTouchEvent() 消费,也多是子 View 的 dispatchTouchEvent() 中消费。返回 true 表示事件被消费,本次的事件终止。返回 false 表示 View 以及子 View 均没有消费事件,将调用父 View 的 onTouchEvent()。
onInterceptTouchEvent(事件拦截):当一个 ViewGroup 在接到 MotionEvent 事件序列时候,首先会调用此方法判断是否须要拦截。特别注意,这是 ViewGroup 特有的方法,View 并无拦截方法。
返回值:是否拦截事件传递,返回 true 表示拦截了事件,那么事件将再也不向下分发而是调用 View 自己的 onTouchEvent()。返回 false 表示不作拦截,事件将向下分发到子 View 的 dispatchTouchEvent()。
onTouchEvent(事件响应):真正对 MotionEvent 进行处理或者说消费的方法,在 dispatchTouchEvent() 中进行调用。
返回值:返回 true 表示事件被消费,本次的事件终止。返回 false 表示事件没有被消费,将调用父 View 的 onTouchEvent 方法,直到返回 true 为止。
咱们能够作个总结:
Activity 的点击事件事实上是调用它内部的 ViewGroup 的点击事件,能够直接当成 ViewGroup 处理。
ViewGroup 的相关事件方法有三个:onInterceptTouchEvent
、dispatchTouchEvent
、onTouchEvent
。
View 的相关事件方法只有两个:dispatchTouchEvent
、onTouchEvent
。
要想充分理解 Android 事件分发机制,本质上是要理解如下三个部分:
✯ Activity - Touch 事件分发 ✯ ViewGroup - Touch 事件分发 ✯ View - Touch 事件分发
当一个点击事件发生时,事件最早传到 Activity 的 dispatchTouchEvent() ,进行事件分发。
public boolean dispatchTouchEvent(MotionEvent ev) { // 通常事件列开始都是 DOWN 事件,即按下事件,因此此处基本是 true if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); // 1 } if (getWindow().superDispatchTouchEvent(ev)) { // 2 return true; } return onTouchEvent(ev); // 3 }
Activity 的 dispatchTouchEvent 方法很简单,主要涉及三个方法:onUserInteraction
、superDispatchTouchEvent
、onTouchEvent
。
/** * 说明: * a. 该方法为空方法,主要实现屏保功能 * b. 当此 activity 在栈顶时,触屏点击按 home,back,menu 键等都会触发此方法 */ public void onUserInteraction() { }
/** * getWindow():获取 Window 类的对象,Window 类是抽象类, * 其惟一实现类是 PhoneWindow 类;即此处的 Window 类对象 = PhoneWindow 类对象。 */ if (getWindow().superDispatchTouchEvent(ev)) { return true; }
Window 类的 superDispatchTouchEvent() 是一个抽象方法,由子类 PhoneWindow 类实现。
public class PhoneWindow extends Window implements MenuBuilder.Callback { ... ... // This is the top-level view of the window, containing the window decor. private DecorView mDecor; @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } ... ... }
咱们发现,Activity 的 superDispatchTouchEvent 方法最终会走到 DecorView 的 superDispatchTouchEvent 方法。
/** * public class DecorView extends FrameLayout, * -- DecorView 继承自 FrameLayout,是全部界面的父类。 * public class FrameLayout extends ViewGroup, -- FrameLayout 是 ViewGroup 的子类,故 DecorView 的间接父类是 ViewGroup。 */ public boolean superDispatchTouchEvent(MotionEvent event) { // 调用父类的方法:ViewGroup 的 dispatchTouchEvent(), // 即:将事件传递到 ViewGroup 去处理,咱们在 ViewGroup 的事件分发机制继续讨论。 return super.dispatchTouchEvent(event); }
因此不难发现,Activity 的 dispatchTouchEvent 方法最终会走到 ViewGroup 的 dispatchTouchEvent 方法。
public boolean onTouchEvent(MotionEvent event) { // 通常返回 true,除非 Touch 事件在 Window 边界外,因此这边咱们再也不继续跟踪。 if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
至此,咱们分析了 Activity 对点击事件的分发机制处理流程,咱们不难发现,Activity 的事件走到了 ViewGroup 进行处理,那么接下来就是分析 ViewGroup 对点击事件的分发机制了。
上面咱们说过,Activity 的 dispatchTouchEvent 方法最终会走到 ViewGroup 的 dispatchTouchEvent 方法。
ViewGroup 对点击事件的分发流程就复杂不少了,咱们来细细研究:
// First touch target in the linked list of touch targets. private TouchTarget mFirstTouchTarget; public boolean dispatchTouchEvent(MotionEvent ev) { ... ... // 这个变量用于记录事件是否被处理完 boolean handled = false; // 过滤掉一些不合法的事件 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 判断是否是 Down 事件,若是是的话,就要作初始化操做 if (actionMasked == MotionEvent.ACTION_DOWN) { // 若是是 Down 事件,就要清空掉以前的状态, cancelAndClearTouchTargets(ev); resetTouchState(); } // 是否拦截事件 final boolean intercepted; // 若是当前是 Down 事件,或者已经有处理 Touch 事件的目标了 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { /** * disallowIntercept:是否禁用事件拦截的功能,默认为 false, * 咱们也能够经过调用 requestDisallowInterceptTouchEvent 方法 * 对这个值进行修改。 */ final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // disallowIntercept 默认为 false 因此会走如下流程 if (!disallowIntercept) { // 每次事件分发时,都需调用 onInterceptTouchEvent() 询问是否拦截事件 intercepted = onInterceptTouchEvent(ev); // 从新恢复 Action,以避免 Action 在上面的步骤被人为地改变了 ev.setAction(action); // restore action in case it was changed } else { // 若是禁用了事件拦截功能,则 intercepted 确定为 false intercepted = false; } } else { // 若是说,事件已经初始化过了,而且没有子 View 被分配处理, // 那么就说明,这个 ViewGroup 已经拦截了这个事件。 intercepted = true; } if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // Check for cancelation,标志着取消事件. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // 若是须要(不取消,也没有被拦截),那么在触摸 Down 事件的时候更新触摸目标列表 // split:表明当前的 ViewGroup 是否是支持分割 MotionEvent 到不一样的 View 当中 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; // 新的触摸对象 TouchTarget newTouchTarget = null; //是否把事件分配给了新的触摸 boolean alreadyDispatchedToNewTouchTarget = false; ★ ★ ★ ★ ★ ★ ★ ★ 重点方法 ★ ★ ★ ★ ★ ★ ★ ★ // 若是事件不是取消事件,也没有拦截,那么进入此函数 if (!canceled && !intercepted) { View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; /* * 若是是个全新的 Down 事件,或者是有新的触摸点,或者是光标来回移动事件 */ if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { // 事件的索引,Down 事件的 index:0 final int actionIndex = ev.getActionIndex(); // always 0 for down // 获取分配的 ID 的 bit 数量 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // 清理以前触摸这个指针标识,以防它们的目标变得不一样步 removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; // 若是新的触摸对象为 null & 当前 ViewGroup 有子元素 if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; // 经过 for 循环,遍历了当前 ViewGroup 下的全部子 View for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } ... ... // 可有可无的代码咱们这边暂且省略 // 派发事件到子 View 处理 if (dispatchTransformedTouchEvent(ev, ...)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { 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; // 若是子 View 处理了事件,则 break, // ViewGroup 不在处理事件(事件被拦截)。 break; } ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } ... ... } } ... ... } return handled; }
其实 ViewGroup 的 dispatchTouchEvent 处理流程,咱们只须要关注两个重点方法:onInterceptTouchEvent
和 dispatchTransformedTouchEvent
。
public boolean onInterceptTouchEvent(MotionEvent ev) { // 一堆判断,咱们只须要知道一点,通常 Touch 事件默认返回 false 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; }
dispatchTransformedTouchEvent 方法的做用,主要就是把事件下发给子 View 进行处理。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; ... ... final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { ... ... // dispatchTouchEvent() 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 { ... ... // dispatchTouchEvent() handled = child.dispatchTouchEvent(transformedEvent); } ... ... // Done. transformedEvent.recycle(); return handled; }
你有没有发现?当存在子 View 的时候,会调用 View.dispatchTouchEvent 方法,若是没有则会向上调用 super.dispatchTouchEvent 方法。
Layout 层次:
Layout 代码:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/my_layout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button_01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button_01" tools:layout_editor_absoluteX="94dp" tools:layout_editor_absoluteY="106dp" /> <Button android:id="@+id/button_02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button_02" tools:layout_editor_absoluteX="94dp" tools:layout_editor_absoluteY="211dp" /> </android.support.constraint.ConstraintLayout>
Activity 代码:
package com.example.marco.myapplication; import android.support.constraint.ConstraintLayout; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.Button; public class MainActivity extends AppCompatActivity { private Button button_01; private Button button_02; private ViewGroup myLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button_01 = (Button) findViewById(R.id.button_01); button_02 = (Button) findViewById(R.id.button_02); myLayout = (ConstraintLayout) findViewById(R.id.my_layout); // 1. ViewGroup: myLayout 布局设置监听事件 myLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "点击了ViewGroup"); } }); // 2. View: button_01 设置监听事件 button_01.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "点击了button_01"); } }); // 3. View: button_02设置监听事件 button_02.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "点击了button_02"); } }); } }
测试结果:
09-26 16:17:51.877 16250 16250 D TAG : 点击了button_01 // 点击按钮 button_01 09-26 16:17:53.875 16250 16250 D TAG : 点击了button_02 // 点击按钮 button_02 09-26 16:17:54.758 16250 16250 D TAG : 点击了ViewGroup // 点击空白处
结果说明:
结论:Button 的 onClick() 将事件消费掉了,所以事件不会再继续向下传递。
这边咱们先作个说明:
其实,只要你触摸了任何控件,就必定会调用该控件的 dispatchTouchEvent
方法!
严格一点来讲:当你点击了某个控件,首先会去调用该控件所在布局
的 dispatchTouchEvent
方法,而后在布局
的 dispatchTouchEvent
方法中找到被点击的相应控件
,再去调用该控件
的 dispatchTouchEvent
方法。
public boolean dispatchTouchEvent(MotionEvent event) { ... ... if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; ★ ★ ★ ★ ★ ★ ★ ★ 重点方法 ★ ★ ★ ★ ★ ★ ★ ★ /* * 注意:只有如下3个条件都为真,dispatchTouchEvent() 才返回 true * 1. li != null & li.mOnTouchListener != null * 2. (mViewFlags & ENABLED_MASK) == ENABLED * 3. mListenerInfo.mOnTouchListener.onTouch(this, event) */ if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } ★ ★ ★ ★ ★ ★ ★ ★ 分别分析以上几个条件 ★ ★ ★ ★ ★ ★ ★ ★ // 若是上面没有返回 true,那么执行 onTouchEvent() if (!result && onTouchEvent(event)) { // 下面再分析 result = true; } } ... ... return result; }
条件 1:li != null & li.mOnTouchListener != null
mOnTouchListener 这个变量是在哪里赋值的呢?
/** * 条件 1:mListenerInfo.mOnTouchListener != null * 说明:mOnTouchListener 变量在 View.setOnTouchListener() 方法里赋值 */ public void setOnTouchListener(OnTouchListener l) { // 只要咱们给控件注册了 Touch 事件,mOnTouchListener 就必定被赋值(不为空) getListenerInfo().mOnTouchListener = l; }
条件 2:(mViewFlags & ENABLED_MASK) == ENABLED
/** * 条件 2:(mViewFlags & ENABLED_MASK) == ENABLED * 说明: * a. 该条件是判断当前点击的控件是否 enable * b. 通常 View 默认 enable,故该条件恒定为 true */
条件 3:mListenerInfo.mOnTouchListener.onTouch(this, event)
mListenerInfo.mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册 touch 事件时的 onTouch 方法。
也就是说若是咱们在 onTouch 方法里返回 true,就会让这三个条件所有成立,从而整个方法直接返回 true。若是咱们在 onTouch 方法里返回 false,就会再去执行 onTouchEvent(event) 方法。
/** * 条件 3:mOnTouchListener.onTouch(this, event) * 说明:回调控件注册 Touch 事件时的 onTouch() */ button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return false; } });
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; ... ... // 若该控件可点击,则进入 switch 判断中 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { // a. 若当前的事件 = 抬起 View case MotionEvent.ACTION_UP: ... ... boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { ... ... if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); // 重点分析函数 } } } ... } break; // b. 若当前的事件 = 按下 View case MotionEvent.ACTION_DOWN: ... ... break; // c. 若当前的事件 = 结束事件(非人为缘由) case MotionEvent.ACTION_CANCEL: ... ... break; // d. 若当前的事件 = 滑动 View case MotionEvent.ACTION_MOVE: ... ... break; } // 若该控件可点击,就必定返回true return true; } // 若该控件不可点击,就必定返回false return false; }
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; /* * 只要咱们经过 setOnClickListener() 为控件 View 注册1个点击事件, * 那么就会给 li.mOnClickListener 变量赋值(即不为空), * 则会往下回调 onClick(),performClick() 返回 true。 */ if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; }
Layout代码:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/my_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <Button android:id="@+id/button_01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button_01" tools:layout_editor_absoluteX="94dp" tools:layout_editor_absoluteY="106dp" /> </LinearLayout>
Activity代码:
package com.example.marco.myapplication; import android.support.constraint.ConstraintLayout; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; public class MainActivity extends AppCompatActivity { private Button button_01; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button_01 = (Button) findViewById(R.id.button_01); /** * 结论验证1:在回调 onTouch() 里返回 false */ // 1. 经过 OnTouchListener() 复写 onTouch(),从而手动设置返回 false button_01.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("TAG", "run onTouch(), action:" + event.getAction()); return false; } }); // 2. 经过 OnClickListener()为控件设置点击事件, // 为 mOnClickListener 变量赋值(即不为空),从而往下回调 onClick() button_01.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "run onClick()"); } }); /** * 结论验证2:在回调 onTouch() 里返回 true */ // 1. 经过 OnTouchListener()复写 onTouch(),从而手动设置返回 true button_01.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("TAG", "run onTouch(), action:" + event.getAction()); return true; } }); // 2. 经过 OnClickListener()为控件设置点击事件, // 为 mOnClickListener 变量赋值(即不为空),从而往下回调 onClick() button_01.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "run onClick()"); } }); } }
测试结果:
// 经过 OnTouchListener() 复写 onTouch(),从而手动设置返回 false 09-26 18:14:19.299 23350 23350 D TAG : run onTouch(), action:0 // ACTION_DOWN 09-26 18:14:19.327 23350 23350 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:14:19.343 23350 23350 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:14:19.383 23350 23350 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:14:19.384 23350 23350 D TAG : run onTouch(), action:1 // ACTION_UP 09-26 18:14:19.385 23350 23350 D TAG : run onClick()
// 经过 OnTouchListener() 复写 onTouch(),从而手动设置返回 true 09-26 18:16:29.758 23847 23847 D TAG : run onTouch(), action:0 // ACTION_DOWN 09-26 18:16:29.773 23847 23847 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:16:29.856 23847 23847 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:16:29.858 23847 23847 D TAG : run onTouch(), action:1 // ACTION_UP
✒ 一、Android 事件分发机制主要由【“事件分发” —> “事件拦截” —> “事件响应”】这三步来进行逻辑控制的。
✒ 二、ViewGroup 默认不拦截任何事件。
✒ 三、onInterceptTouchEvent 返回 true 表示事件拦截,onTouchEvent 返回 true 表示事件消费。
✒ 四、点击事件的分发过程以下:dispatchTouchEvent —> onTouchListener 的 OnTouch 方法 —> onTouchEvent —> onClickListener 的 onClick 方法。从而也能够看出 onTouch 是优先于 onClick 执行,事件传递的顺序是先通过 onTouch,再传递到 onClick。
✒ 五、Android 中的事件 onClick、onLongClick、onScroll 等,都是由多个 Touch 事件(一个 ACTION_DOWN,多个 ACTION_MOVE,一个 ACTION_UP)组成。
✒ 六、子 View 能够经过使用 getParent().requestDisallowInterceptTouchEvent(true) ,阻止 ViewGroup 对其 MOVE 或 UP 事件进行拦截。
✒ 七、MotionEvent 对象的四种状态:MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间
MotionEvent.ACTION_MOVE:手指在屏幕上移动
MotionEvent.ACTION_UP:手指离开屏幕瞬间
MotionEvent.ACTION_CANCEL:取消手势
✒ 八、点击某个控件,首先会去调用该控件所在布局的 dispatchTouchEvent方法,而后在布局的 dispatchTouchEvent 方法中找到被点击的相应控件,再去调用该控件的 dispatchTouchEvent方法。
✒ 九、若是 View 没有消费 ACTION_DOWN 事件,则以后的 ACTION_MOVE 等事件都不会再接收。
✒ 十、事件在从 Activity.dispatchTouchEvent 往下分发的过程当中:
若是中间的 ViewGroup 都不拦截,进入最底层的 View 后,由 View.onTouchEvent 处理,若是 View 也没有消费事件,最后会返回到 Activity.onTouchEvent。 若是中间任何一层 ViewGroup 拦截事件,则事件再也不往下分发,交由拦截的 ViewGroup 的 onTouchEvent 来处理。