文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android源码分析——ViewGroup的事件分发机制(二)

2022-06-06 13:49

关注

在这里插入图片描述
通过前一篇博客View的事件分发机制,从dispatchTouchEvent说起(一)的介绍相信大家对 Android View 事件的分发机制有了很深的理解。我们知道 Android 中 View 是存在于 Activity。 今天我们继续学习 Activity 到 ViewGroup 的事件分发机制。

一、Activity 分发到 ViewGroup

当我们手指触摸到屏幕时,最先接收到事件的肯定是

Activity
,首先调用的是
Activity
dispatchTouchEvent(event)
,那么我们下面先来看它的源码:

1、dispatchTouchEvent(event)
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
}

这里我们看到首先它判断了是不是 DOWN 事件,如果是的话调用了

onUserInteraction()
这个方法我们看到在
Activity
源码中是空的,它可以让子类去实现。我们这里不需要多做关注。

下面我们发现这里它继续调用了

getWindow().superDispatchTouchEvent(ev)
,这里我们后面再讲。我们看到如果前面返回时
false
的话,后面调用了
Activity
onTouchEvent(ev)

那么我们继续看

getWindow().superDispatchTouchEvent(ev)
,这里我们之前文章提到过,
getWindow()
返回的是 PhoneWindow 对象,那么我们继续看
PhoneWindow
,我们会发现它调用了
mDecor.superDispatchTouchEvent(event)
,也就是执行到 DecorView 的
superDispatchTouchEvent(event)
。我们继续追踪发现,最终掉的是 ViewGroup 的
diapatchTouchEvent(event)
方法。

那我们这里先总结下

dispatchTouchEvent

Activity==>PhoneWindow==>DecorView==>ViewGroup

2、onTouchEvent(event)

上面的具体到 ViewGroup 我们后面再看,这里我们还是回到之前的如果返回

false
那么就需要调用
Activity.onTouchEvent(ev)
,那么它的源码如下:

  public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }

这里只是判断了点击事件的边界条件,如果在边界内就直接返回了

false
,否则
finish
当前 Activity。

二、ViewGroup 的分发

我们上面看到

Activity.dispatchTouchEvent(ev)
最终调用到
ViewGroup.diapstchTouchEvent(ev)
那么下面我们继续看下它的源码:

1、ViewGroup.dispatchTouchEvent(ev)
  @Override
    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) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            // 检查是否拦截该事件
            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 {
                intercepted = true;
            }
             
            //检查是否取消
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //如果不拦截并且不取消则继续执行
            if (!canceled && !intercepted) {
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                   
                    final int childrenCount = mChildrenCount;
                    //newTouchTarget触摸目标链表里面存储有子View
                    if (newTouchTarget == null && childrenCount != 0) {
                       
                        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);
                           
                           //将当前子View添加到newTouchTarget
                            newTouchTarget = getTouchTarget(child);
                           
                           //这里去分发TouchEvent 
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                
                            }
             
        return handled;
    }

这个方法可以说是非常的多,看着就令人头大。

这里我们主要看核心代码,以下重要的代码我再里面都添加了注释。非核心代码我们这里略过。我们这里注意以下几点:

handled: 这个变量是标记触摸事件是否被处理的,默认是
false
代表未被消耗。 onFilterTouchEventForSecurity(ev): 这个方法我们看到是一个非常重要的方法,后面几乎所有的代码都在这个方法返回
true
之后才执行。那么这个方法是干啥的呢?它其实是判断当前窗口是否是模糊窗口?如果是的话则返回
false
,此时将不会在将事件分发给子 View. intercepted: 标记当前父布局要不要拦截当前事件,一般都是取
onInterceptTouchEvent(ev)
的方法。 onInterceptTouchEvent(ev):判断当前 ViewGroup 是否拦截当前事件,一般返回
flase
不拦截。我们可以在子类重写该方法。 cancled: 这个变量就是带判断当前手势是不是被取消。如果
cancled
intercepted
都是
false
才会去执行后面的分发。 newTouchTarget:这是一个单向链表,里面存储的是触摸目标的 View 。后面调用
getTouchTarget(child)
将子 View 的触摸目标添加到 newTouchTarget 。 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign): 这个方法就是去将触摸事件分发给对应子 View,下面我们就来看下这个方法。 I、ViewGroup.dispatchTransformedTouchEvent
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        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;
        }
      
        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);
                    //调用子 View 的dispatchTouchEvent(event)
                    handled = child.dispatchTouchEvent(event);
         
        return handled;
    }

这个方法的代码也不少我们来看看核心代码,首先前面判断当前事件是否是取消事件。如果是的话则执行子 View 的

dispatchTouchEvent
。 后面我们看到最终执行到
handled = child.dispatchTouchEvent(event);
来返回
handled
。到这里我们就发现最终这里调用的是 View 的
dispatchTouchEvent(event)
。所以这里我们可以用下面的伪代码来总结一下 ViewGroup 的事件分发机制。

2、伪代码总结ViewGroup的事件分发
public boolean dispatchTouchEvent(MotionEvent ev){
    boolean concume = false;
    //调用onInterceptTouchEvent判断是否拦截
    if(onInterceptTouchEvent(ev)){
      //如果拦截则调用自己的onTouchEvent(ev)消耗
      consume = onTouchEvent(ev);
    }else{
        //如果不拦截,则将事件分发给子View
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

总体来说就是首先判断 ViewGroup 自己拦截不拦截,如果拦截则调用自己的

onTouchEvent(ev)
,如果不拦截则调用子 View 的
dispatchTouchEvent(event)

三、总结测试

我们还是写一个简单的项目来测试一下,一个 Activity,一个ViewGroup ,一个View。我们分别重写我们上述提到的方法。
写之前我们先思考下面两个问题:

当所有View都不消耗该事件的时候,事件如何传递? 子View不处理事件时,UP 事件是跟 DOWN 事件一样传递的吗? 子 View 消耗完事件,ViewGroup 的
onTouchEvent
还会执行吗?
MainActivity.java
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new CustomLayout(this));
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("TAG_紫雾凌寒_MainActivity","===onTouchEvent===="+event.getAction());
        return super.onTouchEvent(event);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("TAG_紫雾凌寒_MainActivity","===dispatchTouchEvent===="+ev.getAction());
        return super.dispatchTouchEvent(ev);
    }
}
CustomLayout.java
class CustomLayout extends LinearLayout {
    public CustomLayout(Context context) {
        super(context);
        addView(new CustomView(context));
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e("TAG_紫雾凌寒_CustomLayout","===onInterceptTouchEvent===="+ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev){
        Log.e("TAG_紫雾凌寒_CustomLayout","===dispatchTouchEvent===="+ev.getAction());
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event){
        Log.e("TAG_紫雾凌寒_CustomLayout","===onTouchEvent===="+event.getAction());
        return super.onTouchEvent(event);
    }
}
CustomView.java
class CustomView extends View {
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e("TAG_紫雾凌寒_CustomView__","===dispatchTouchEvent===="+event.getAction());
        return super.dispatchTouchEvent(event);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStrokeWidth(5f);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(300,300,280,paint);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("TAG_紫雾凌寒_CustomView__","===onTouchEvent===="+event.getAction());
        return super.onTouchEvent(event);
    }
    public CustomView(Context context) {
        super(context);
    }
}
1、子View也不处理触摸事件

我们回到之前我们的问题,当子 View 也不处理触摸事件的时候,那么触摸事件该如何传递?
对于这样的问题我们模拟一下,上面的代码就是子View对事件没有消耗,我们点击自定义 View 的区域,我们看到日志如下所示:
在这里插入图片描述
我们通过日志看到,当子View不处理时,我们看到事件的传递是会返回到 Activity 执行 Activity 的 onTouchEvent(ev).

同样我们可以看到,当执行完 DOWN 事件后,UP 事件只在 Activity 层,并没有传递至 ViewGroup 和 View。

2、子View处理触摸事件

那么我们让子View处理触摸事件,也就是

View.onTouchEvent(ev)
返回
true
.我们再来看下日志:
在这里插入图片描述
这里我们看到当View如果处理触摸事件时,DOWN 和 UP事件的传递是一样的。消耗完之后就不会再返回上一层了。我们看到子 View 处理完事件后,ViewGroup 的 onTouchEvent 没再执行了。

3、父布局拦截事件

当我们让 ViewGroup 拦截触摸事件时我们看看日志是不是跟我们前面分析的一样?
在这里插入图片描述
我们看到这里跟我们分析的一样,当 ViewGroup 拦截后,就不会传递到 View 。

4、总结

我们回顾一下本文我们主要讲的 Activity 的触摸事件的分发机制:手指按下屏幕==>Activity==>PhoneWindow==>DecorView==>ViewGroup==View。下面我们还是通过流程图来总结一下它的传递机制:
Activity事件的分发

欢迎在评论区留下你的观点大家一起交流,一起成长。如果今天的这篇文章对你在工作和生活有所帮助,欢迎转发分享给更多人。

同时欢迎大家扫描左侧二维码关注我的公众号和加入我组建的大前端学习交流群,群里大家一起学习交流 Android、Flutter等知识。从这里出发我们一起讨论,一起交流,一起提升。

群号:872749114

往期推荐
View的事件分发机制,从dispatchTouchEvent说起(一)
Android源码分析——View是如何被添加到屏幕的?
Android热修复——深入剖析AndFix热修复及自己动手实现
深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)
深入理解HashMap原理(二)——手写HashMap


作者:紫雾凌寒


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯