17.源码阅读(Touch事件分发)
有这样一个布局,Activity中有一个ViewGroup,ViewGroup中又放了一个View,我重写了Activity的dispatchTouchEvent和onTouchEvent,重写了ViewGroup的dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent,重写了View的dispatchTouchEvent和onTouchEvent,然后触摸一下TestTouchView,看看控制台打印了什么结果
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".test.source.TestViewTouchActivity"> <com.app.rzm.test.view.TestTouchViewGroup android:layout_width="match_parent" android:background="#f00" android:layout_height="400dp"> <com.app.rzm.test.view.TestTouchView android:layout_width="200dp" android:background="#0f0" android:id="@+id/touch" android:layout_height="200dp" /> </com.app.rzm.test.view.TestTouchViewGroup> </LinearLayout>
1.默认没有任何拦截和事件处理的情况下打印结果
05-28 08:07:49.305 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:0 05-28 08:07:49.306 1590-1590/com.app.rzm D/ViewGroup: dispatchTouchEvent:0 onInterceptTouchEvent:0 05-28 08:07:49.306 1590-1590/com.app.rzm D/View: dispatchTouchEvent:0 05-28 08:07:49.307 1590-1590/com.app.rzm D/View: onTouchEvent:0 05-28 08:07:49.307 1590-1590/com.app.rzm D/ViewGroup: onTouchEvent:0 05-28 08:07:49.309 1590-1590/com.app.rzm D/Activity: onTouchEvent:0 05-28 08:07:49.327 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2 onTouchEvent:2 05-28 08:07:49.344 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2 onTouchEvent:2 05-28 08:07:49.361 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2 onTouchEvent:2 05-28 08:07:49.377 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2 05-28 08:07:49.378 1590-1590/com.app.rzm D/Activity: onTouchEvent:2 05-28 08:07:49.394 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2 05-28 08:07:49.395 1590-1590/com.app.rzm D/Activity: onTouchEvent:2
可以看到在没有进行事件拦截的情况下,打印的顺序如上,我们可以简单分析以下事件传递流程:
有一点需要注意,事件的按下,滑动和抬起三种类型事件是一个一个进行传递的
1.当手指触摸屏幕时,按下事件收到响应,Activity的dispatchTouchEvent收到回调,将按下事件传递到了ViewGroup中的dispatchTouchEvent方法
2.ViewGroup收到按下事件时,调用onInterceptTouchEvent判断是否拦截按下事件,因为没有设置拦截,所以按下事件又被传递到View的dispatchTouchEvent方法中
3.View收到按下事件,传递到它的onTouchEvent方法去处理,由于这个View并没有设置处理这个事件,所以又将它传递到父布局ViewGroup的onTouchEvent中,看它是不是需要处理
4.ViewGroup的onTouchEvent收到这个事件,仍没有进行处理,所以又将事件传递到了Activity的onTouchEvent方法
5.Activity同样不处理,所以这个事件因为没人处理,所以就不了了之
6.ACTION_DOWN事件没有被处理,ACTION_MOVE事件就一直在Activity的dispatchTouchEvent和onTouchEvent方法间传递,直到ACTION_UP事件响应
源码中事件的传递,首先来到Activity的dispatchTouchEvent中
/** * Called to process touch screen events. You can override this to * intercept all touch screen events before they are dispatched to the * window. Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { //这个方法是提供出来进行重写的,不用管 onUserInteraction(); } //开始分发触摸事件 if (getWindow().superDispatchTouchEvent(ev)) { return true; } //如果事件没有被处理,则执行onTouchEvent方法 return onTouchEvent(ev); }
来到PhoneWindow中的superDispatchTouchEvent,可以看到它调用了DecorView中的方法,通过之前看setContentView的源码(https://www.jianshu.com/p/2f87ebe77f4e)我们已经认识了DecorView,接下来进入DecorView源码中
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
可以看到DecorView又调用了它父类的方法
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
进入ViewGroup中的dispatchTouchEvent方法,这里是事件分发的源头。我们可以看到在这个方法中,做了许多的条件判断,而最终都会执行方法dispatchTransformedTouchEvent,区别在于方法中传递的参数会根据场景而有所不同
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ...... boolean handled = false; //过滤掉一些不安全的劫持式的触摸事件 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; //新的按下事件产生时就初始化所有之前的触摸状态 //检测事件是否被拦截 if (!canceled && !intercepted) { //获取到当前得到焦点的view View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); //获取到所有可以接收这个触摸事件的view封装到集合中 final ArrayList<View> preorderedList = buildTouchDispatchChildList(); ...... 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; } ...... if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ...... break; } ....... } ....... } if (newTouchTarget == null && mFirstTouchTarget != null) { ...... } } } ...... return handled; }
我们来到方法dispatchTransformedTouchEvent中,这个方法的作用就是将触摸事件传递到当前的view中,如果当前view为null,就传递到它的上一层的ViewGroup,最终的是携带的MotionEvent参数,可以看到当存在按下滑动等事件时,这些事件会被封装到MotionEvent中传递到view
/** * Transforms a motion event into the coordinate space of a particular child view, * filters out irrelevant pointer ids, and overrides its action if necessary. * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. */ 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(); //如果当前事件是取消,则将取消参数设置给MotionEvent传递到view中 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; //回调位置信息到下一级的view的dispatchTouchEvent方法 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; }
事件从ViewGroup分发到了View的dispatchTouchEvent方法,我们进入View的这个方法中,这里涉及到onTouch,onTouchEvent和onClick方法的回调顺序问题,可以在代码中看到,在onClickListener 和onTouchListener都设置了的情况下,最先执行的会是onTouch,并且如果onTouch方法返回值设置为true,那么将不会再往下回调onTouchEvent和onClick方法,事件就被onTouch拦截了;当其返回值为false,则会继续向下开始执行onTouchEvent,onClick方法是在onTouchEvent之后执行的,所以三者的执行顺序,在没有拦截的情况下,可见是onTouch > onTouchEvent > onClick
/** * 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) { ...... boolean result = false; ...... final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture //按下后停止view的滑动 stopNestedScroll(); } //安全性过滤 if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //ListenerInfo 对象封装了所有的view的监听相关的信息 //例如onTouchListener onClickListener等,只要给view设置 //了监听,这个对象就会被创建,可以在下一个代码块中 //看到ListenerInfo 的结构类型 ListenerInfo li = mListenerInfo; //这里会决定onTouch方法是否执行,如果给view设置了 //onTouchListener,那么ListenerInfo 对象存在,li != null满足 //li.mOnTouchListener != null也满足, //(mViewFlags & ENABLED_MASK) == ENABLED //用来判断这个view是否可用,如果被设置为enable false,那么 //是无法处理事件的,条件都满足了,就会回调onTouch方法了 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; }
ListenerInfo
static class ListenerInfo { /** * Listener used to dispatch focus change events. * This field should be made private, so it is hidden from the SDK. * {@hide} */ protected OnFocusChangeListener mOnFocusChangeListener; /** * Listeners for layout change events. */ private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners; protected OnScrollChangeListener mOnScrollChangeListener; /** * Listeners for attach events. */ private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners; /** * Listener used to dispatch click events. * This field should be made private, so it is hidden from the SDK. * {@hide} */ public OnClickListener mOnClickListener; /** * Listener used to dispatch long click events. * This field should be made private, so it is hidden from the SDK. * {@hide} */ protected OnLongClickListener mOnLongClickListener; /** * Listener used to dispatch context click events. This field should be made private, so it * is hidden from the SDK. * {@hide} */ protected OnContextClickListener mOnContextClickListener; /** * Listener used to build the context menu. * This field should be made private, so it is hidden from the SDK. * {@hide} */ protected OnCreateContextMenuListener mOnCreateContextMenuListener; private OnKeyListener mOnKeyListener; private OnTouchListener mOnTouchListener; private OnHoverListener mOnHoverListener; private OnGenericMotionListener mOnGenericMotionListener; private OnDragListener mOnDragListener; private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener; OnApplyWindowInsetsListener mOnApplyWindowInsetsListener; OnCapturedPointerListener mOnCapturedPointerListener; }
进入onTouchEvent方法,这个方法处理的比较复杂,我们删除一些非核心的代码来看.setOnClickListener的onClick方法最后是在抬起手指的时候执行的,所以这里有一个问题,如果你自定义的View重写了onTouchEvent,并且没有调用父类的super.onTouchEvent方法,那么你设置的onClickListener将会失效
/** * 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) { //检查是否可点击 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { //在手势抬起时执行onClick方法 case MotionEvent.ACTION_UP: ...... if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); ...... break; case MotionEvent.ACTION_DOWN: ...... break; case MotionEvent.ACTION_CANCEL: ...... break; case MotionEvent.ACTION_MOVE: ...... break; } return true; } return false; }
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); notifyEnterOrExitForAutoFillIfNeeded(true); return result; }
总结一下,到这里touch事件传递机制基本上简单的走了以下流程。事件开始的起点在所触摸的Activity的dispatchTouchEvent方法,通过这个方法将事件通过PhoneWindow传递到DecorView,然后又传递到ViewGroup中,在ViewGroup中进行事件的分发,首先获取到所有可以接收这个事件的View集合,然后将事件传递到当前获取焦点的View,一层一层的传递,经过ViewGroup的dispatchTouchEvent onInterceptTouchEvent方法进入View的dispatchTouchEvent方法,最后进入onTouchEvent方法进行事件的处理

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Android深度定制化TabLayout:圆角,渐变色,背景边框,基于Android原生TabLayout
Android深度定制化TabLayout:圆角,渐变色,背景边框,基于Android原生TabLayout 如今UI设计已经不再满足于下方只有一个下划线,切换后能改变和表示选中颜色的TabLayout了。设计对于TabLayout这种非常常见的切换控制条提出了更高的设计要求,比如,背景颜色渐变,有一定圆角弧度的背景边框线,等等,比如下面的的一个自定义TabLayout: 实现纯色且具有圆角弧度的Tab切换卡很多,但是要求具有一定渐变的实现,如上图被选中的选项卡颜色渐变则很少见,这样的设计要求原生或者即便第三方开源的项目也很少实现(至少截止发文时间)。现在说明如何上图的TabLayout,基于Android原生的TabLayout。 毫无疑问得继承自Android原生到的TabLayout,重写里面的相关方法,MyTabLayout.java: package zhangphil.test; import android.content.Context; import android.graphics.Color; import android.support.design.widge...
- 下一篇
从源码角度分析Activity、Window和DecorView的关系
前言 最近想出一篇Android事件分发机制的文章,但是根据很多小伙伴反馈在理解Android事件分发机制之前都不是很明白Activity、Window和DecorView之间的关系,导致在学习Android事件分发机制上理解很费劲,本文将从源码角度带你分析Activity、Window和DecorView之间的关系,让你彻彻底底搞明白。 Activity、Window和DecorView之间的关系图 关系图.png 看图来说就是一种简单的包含关系(图画得比较糙),Activty里面持有一个Window,这个Window有一个唯一实现子类PhoneWindow,而PhoneWindow对象里面又持有一个DecorView对象,这个DeCorView继承自FrameLayout,FragmentLayout是ViewGroup的子类,也就是说DecorView是ViewGroup的间接子类。先有一个大体的概念,下面我会从源码角度带着大家一起分析。 源码分析 每一个Activity里面都持有一个Window对象 public class Activity extends ContextTh...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- MySQL8.0.19开启GTID主从同步CentOS8