Android事件分发机制【最新、最全、最易懂、由浅入深】

tips:文章有点长,但是耐心看完一定有收获,部分内容为自己总结,部分内容 参考了其他人的博客,站在了巨人的肩膀上。图文丰富、详细解读。

一、基础概念

事件是什么

事件是指用户与应用程序交互时产生的动作或操作,例如点击屏幕、滑动手指、按下按键等。在安卓中,事件可以分为多种类型,包括触摸事件、按键事件、手势事件等。

四个基本动作:

  • MotionEvent.ACTION_DOWN:表示用户按下触摸屏幕或点击某个 View 的动作。
  • MotionEvent.ACTION_UP:表示用户抬起触摸屏幕或取消点击某个 View 的动作。
  • MotionEvent.ACTION_MOVE:表示用户在触摸屏幕上滑动手指的动作。
  • MotionEvent.ACTION_CANCEL:表示事件被取消,通常由于某些非人为的原因,导致当前事件序列的中断。

事件通常是由这四个基本动作组合而成的,但并不限于这四个动作。事件的组成可以更为复杂,可以包含更多的动作和状态。这四个动作是触摸事件的基础,但触摸事件还可能包含其他动作,例如双击、长按、双指缩放等。

事件序列:从手指按下屏幕开始,到手指离开屏幕所产生的一系列事件。

事件分发机制是什么

安卓事件分发机制是指在安卓系统中,如何处理和传递用户交互事件的一套机制

本质:当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理。这个事件传递的过程就是分发过程。

事件分发机制存在的意义

为什么会有事件分发机制?

简单讲:因为点击事件可能会发生冲突,所以用这个解决。

详细讲:在安卓的布局结构中,它是竖形结构,导致有些布局会重叠在一起,当我们用手指点击的地方在很多个布局范围之内时,应该让哪个View来响应呢?

这就是存在的意义:是为了确保用户在应用程序界面上的交互能够被正确捕获、传递和处理,从而实现用户与应用程序的有效交互。

事件在哪些对象之间进行传递?传递顺序?

Activity、ViewGroup、View。

事件传递的顺序:Activity -> ViewGroup -> View

在这里插入图片描述

AndroidUI界面由ActivityViewGroupView 及其派生类组成

  1. Activity:Activity 是安卓应用程序中的一个重要组件,通常负责管理用户界面和处理用户交互事件。事件首先被传递给当前处于活动状态的 Activity。
  2. ViewGroup:ViewGroup 是一种特殊的 View,用于包含其他 View。例如,LinearLayout、RelativeLayout 等布局容器都是 ViewGroup 的子类。如果事件发生在某个 ViewGroup 内部,那么事件将首先被传递给该 ViewGroup。
  3. View:View 是安卓界面的基本构建块,是所有UI组件的基类。用于显示用户界面上的各种元素。当事件发生在某个 View 上时,事件将被传递给该 View。一般Button、TextView等控件都是继承父类View

事件分发过程由哪些方法协作完成?

答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()

方法作用调用时刻
dispatchTouchEvent()这是 View 或 ViewGroup 中用于分发触摸事件的方法。当一个触摸事件到达一个 View 或 ViewGroup 时,首先调用该方法。该方法会根据事件的类型和目标 View 的状态,决定是在当前 View 处理事件还是将事件传递给子 View。
onInterceptTouchEvent()这是 ViewGroup 中的一个方法,用于拦截触摸事件,阻止它们继续传递给子 View。当一个 ViewGroup 的父 View 在 dispatchTouchEvent 方法中决定是否传递事件给它时,会先调用该方法。如果该方法返回 true,则表示拦截事件,停止传递给子 View;如果返回 false,则事件会继续传递给子 View。
onTouchEvent()这是 View 或 ViewGroup 中用于处理触摸事件的方法。当一个触摸事件被分发给一个 View 或 ViewGroup 时,如果该 View 或 ViewGroup 没有拦截该事件,或者拦截后又决定放弃拦截,那么该方法会被调用。在该方法中,开发人员可以实现自定义的触摸事件处理逻辑。

二、事件分发机制流程概述

Android事件分发流程 = Activity -> ViewGroup -> View

即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View

即:要想充分理解Android分发机制,本质上是要理解:

  1. Activity对点击事件的分发机制
  2. ViewGroup对点击事件的分发机制
  3. View对点击事件的分发机制

下面,按顺序讲解:Activity事件分发机制、ViewGroup事件分发机制、View事件分发机制

流程1:Activity的事件分发机制

Android事件分发机制首先会将点击事件传递到Activity中,具体是执行dispatchTouchEvent()进行事件分发。

源码总结

当一个点击事件发生时,从Activity的事件分发开始(Activity.dispatchTouchEvent()),流程总结如下:

在这里插入图片描述

三者介绍

  1. Activity:每个 Activity 都会包含一个或多个 Window,并通过 Window 来管理其显示内容。
  2. Window:它是抽象类,用于表示一个界面窗口,唯一的实现是PhoneWindow。每个 Activity 都包含一个 Window 对象,通过该 Window 对象来管理界面的布局、绘制和交互。
  3. DecorView:DecorView 是Activity的最顶层的View,它继承FrameLayout ,包含了整个界面的布局。

在事件分发源码中,这些类之间的关系体现在事件从顶层到底层的传递过程中:

  1. Activity:作为应用程序中的一个窗口管理器,Activity 负责创建和管理 Window,并在需要时处理事件分发。Activity 会将触摸事件传递给它的 Window 对象。
  2. Window:作为 Activity 的窗口,负责创建 DecorView 并管理用户界面的显示和交互。Window 会将触摸事件传递给它的 DecorView。
  3. DecorView:作为整个界面布局的根节点,负责管理并响应用户的触摸事件。
  4. DecorView 的父类是FrameLayout,FrameLayout没有这个方法的实现,所以再去找FrameLayout的父类ViewGroup

每个Activity 都持有一个window对象,而window是一个抽象类,它唯一的实现就是PhoneWindo,PhoneWindo又持有

核心方法总结

Activity.dispatchTouchEvent()

  • 返回true:表示事件分发结束了,可能是下面两种之一
    • 事件被下层的ViewGroup消费了
    • 事件被Activity自己消费了
  • 返回false:表示事件分没有结束,表示事件没有被消费

Activity.OnTouchEvent

  • 返回true:表示事件分发结束了,表示Activity自己消费了这个事件
  • 返回false:表示事件分发结束了,表示Activity没有消费了这个事件

onTouchEvent()

请添加图片描述

源码分析

事件分发是从dispatchTouchEvent 开始的,我们直接重写一下,去定位这个方法:super.dispatchTouchEvent(ev);

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }
    //从这里开始分发
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }
    
}
1.dispatchTouchEvent

点击后来到Activity类的源码,如下图所示

在这里插入图片描述

上图源码分析

如果用户点击了

那就执行 onUserInteraction(),onUserInteraction()是一个空方法,如果你有需要自己重写。

将事件交给Activity所持有的Window

如果返回true,Activity的dispatchTouchEvent 也就返回true了,表示整个事件传递就结束了,事件已经被消费了

如果返回false,那么就会调用Activity的onTouchEvent(ev)方法

2.getWindow().superDispatchTouchEvent(ev)

所以我们 定位到getWindow().superDispatchTouchEvent(ev) 方法中去看看

直接来到了 Window类的源码,如下图所示

在这里插入图片描述

那么这个抽象方法的实现 在哪里呢?

经过查询,我们知道它的实现是在PhoneWindow这个类中,我们搜索到这个类,去看源码,在这个类中找到superDispatchTouchEvent这个方法,如下图所示

在这里插入图片描述

而这个方法里面是调用了 mDecor 的superDispatchTouchEvent 方法,PhoneWindow 持有DecorView的实例。

在这里插入图片描述

再来看DecorView里面的superDispatchTouchEvent 方法,如下图所示

在这里插入图片描述

可以看到,DecorView它直接调用了它父类的dispatchTouchEvent方法,我们直接定位这个方法,发现直接来到了ViewGroup类,如下图所示

在这里插入图片描述

所以我们看懂了,如果 ViewGroup 的dispatchTouchEvent 返回true,那么一些列的 那些dispatchTouchEvent 都返回true,最终Activity的dispatchTouchEvent 就返回了true,事件就结束了。如果 ViewGroup 的dispatchTouchEvent 返回false,那么最终 Activity就会调用Activity的onTouchEvent(ev)方法。

为什么DecorView 会直接跳到ViewGroup?

因为DecorView 的父类是FrameLayout
在这里插入图片描述

FrameLayout中没有dispatchTouchEvent这个方法,它的父类是ViewGroup,ViewGroup中有这个方法

在这里插入图片描述

所以走到了这里

这里就完成了 从 Activity - 》 ViewGroup

3.onTouchEvent

我们返回到Activity的源码中,去定位一下onTouchEvent

在这里插入图片描述

上图源码分析
先进行了一个判断,如果`mWindow.shouldCloseOnTouch(this, event)` 返回true, `finish()`那么就会关闭当前的Activity,最终返回true。
否则,返回false
无论返回什么事件都会结束

那我们就定位一下mWindow.shouldCloseOnTouch(this, event) ,定位不过去的话,我们直接去Window类里面搜索这个方法,因为mWindow是Window的一个实例。

在这里插入图片描述

上图源码分析
先定义了常量 isOutside
1.当变量mCloseOnTouchOutside为true的时候,说明当前Activity是支持点击空白部分让Activity消失的。
2.当peekDecorView() 不为空的时候,表示当前Window是持有DecorView的
3.当isOutside 为true,说明当前的点击事件是在Window所持有的DecorView外面的
当1 2 3全部为true时,才返回ture,否则返回false

总结:该方法就是判断点击事件是不是在边界且能关闭Activity
- 如果返回true:表示事件分发结束,判断了点击事件在Window边界外,事件被Activity消费
- 如果返回false:表示事件分发结束,判断了点击事件在Window边界内,事件不作处理,不算被Activity消费

那么,ViewGroupdispatchTouchEvent()什么时候返回true / false

请继续往下看ViewGroup事件的分发机制

流程2: ViewGroup的事件分发机制

从上面Activity的事件分发机制可知,在Activity.dispatchTouchEvent()实现了将事件从Activity->ViewGroup的传递,ViewGroup的事件分发机制从dispatchTouchEvent()开始。

源码总结

Android事件分发传递到Acitivity后,总是先传递到ViewGroup、再传递到View。流程总结如下:(假设已经经过了Acitivity事件分发传递并传递到ViewGroup)

在这里插入图片描述

核心方法总结

ViewGroup.dispatchTouchEvent

  • 返回true:表示事件分发结束了,可能是下面两种之一
    • 事件被ViewGroup消费了
    • 事件被View消费了
  • 返回false:表示事件没有被消费,向上层传递给Activity

ViewGroup.OnTouchEvent

  • 返回true:表示ViewGroup自己消费了这个事件
  • 返回false:表示ViewGroup没有消费了这个事件

OnInterceptTouchEvent

上图中展示的很清楚,起到拦截的作用

在这里插入图片描述

源码分析

那我们就分析一下 ViewGroup 的 dispatchTouchEvent 做了什么

这个方法的源码很长,有两百行,可以自己去阅读一下。

这个方法主要做了三件事情:

  • 去判断是否需要拦截事件
  • 在当前ViewGroup中找到用户真正点击的View
  • 分发事件到View上
/**
  * 源码分析:ViewGroup.dispatchTouchEvent()
  */ 
  public boolean dispatchTouchEvent(MotionEvent ev) { 

  // 仅贴出关键代码
  ... 

  if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
  // 分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
    // 判断值1-disallowIntercept:是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
    // 判断值2-!onInterceptTouchEvent(ev) :对onInterceptTouchEvent()返回值取反
        // a. 若在onInterceptTouchEvent()中返回false,即不拦截事件,从而进入到条件判断的内部
        // b. 若在onInterceptTouchEvent()中返回true,即拦截事件,从而跳出了该条件判断
        // c. 关于onInterceptTouchEvent() ->>分析1

  // 分析2
    // 1. 通过for循环,遍历当前ViewGroup下的所有子View
    for (int i = count - 1; i >= 0; i--) {  
        final View child = children[i];  
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                || child.getAnimation() != null) {  
            child.getHitRect(frame);  

            // 2. 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
            if (frame.contains(scrolledXInt, scrolledYInt)) {  
                final float xc = scrolledXFloat - child.mLeft;  
                final float yc = scrolledYFloat - child.mTop;  
                ev.setLocation(xc, yc);  
                child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  

                // 3. 条件判断的内部调用了该View的dispatchTouchEvent()
                // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面章节介绍的View事件分发机制)
                if (child.dispatchTouchEvent(ev))  { 

                // 调用子View的dispatchTouchEvent后是有返回值的
                // 若该控件可点击,那么点击时dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                // 即该子View把ViewGroup的点击事件消费掉了

                mMotionTarget = child;  
                return true; 
                      }  
                  }  
              }  
          }  
      }  
    }  

  ...

  return super.dispatchTouchEvent(ev);
  // 若无任何View接收事件(如点击空白处)/ViewGroup本身拦截了事件(复写了onInterceptTouchEvent()返回true)
  // 会调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
  // 因此会执行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick(),即自己处理该事件,事件不会往下传递
  // 具体请参考View事件分发机制中的View.dispatchTouchEvent()

  ... 

}

/**
  * 分析1:ViewGroup.onInterceptTouchEvent()
  * 作用:是否拦截事件
  * 说明:
  *     a. 返回false:不拦截(默认)
  *     b. 返回true:拦截,即事件停止往下传递(需手动复写onInterceptTouchEvent()其返回true)
  */
  public boolean onInterceptTouchEvent(MotionEvent ev) {  
    
    // 默认不拦截
    return false;

  } 
  // 回到调用原处

实例分析

1. 布局说明

在这里插入图片描述

2. 测试代码

布局文件:activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/my_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:focusableInTouchMode="true"
        android:orientation="vertical">

<Button
        android:id="@+id/button1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="按钮1" />

<Button
        android:id="@+id/button2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="按钮2" />

</LinearLayout>

核心代码:MainActivity.java

public class MainActivity extends AppCompatActivity {

    Button button1,button2;
    ViewGroup myLayout;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button1 = (Button)findViewById(R.id.button1);
        button2 = (Button)findViewById(R.id.button2);
        myLayout = (LinearLayout)findViewById(R.id.my_layout);

        // 1.为ViewGroup布局设置监听事件
        myLayout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "点击了ViewGroup");
            }
        });

        // 2. 为按钮1设置监听事件
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "点击了button1");
            }
        });

        // 3. 为按钮2设置监听事件
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "点击了button2");
            }
        });
    }
}

3. 测试结果

// 点击按钮1,输出如下
点击了button1

// 点击按钮2,输出如下
点击了button2

// 点击空白处,输出如下
点击了ViewGroup

4. 结果分析

  • 点击Button时,因为ViewGroup默认不拦截,所以事件会传递到子View Button,于是执行Button.onClick()。

此时ViewGroup. dispatchTouchEvent()会直接返回true,所以ViewGroup自身不会处理该事件,于是ViewGroupLayout的dispatchTouchEvent()不会执行,所以注册的onTouch()不会执行。即onTouchEvent() -> performClick() -> onClick()整个链路都不会执行,所以最后不会执行ViewGroup设置的onClick()里。

  • 点击空白区域时,ViewGroup. dispatchTouchEvent()里遍历所有子View希望找到被点击子View时找不到,所以ViewGroup自身会处理该事件,于是执行onTouchEvent() -> performClick() -> onClick(),最终执行ViewGroupLayout的设置的onClick()

流程3:View的事件分发机制

从上面ViewGroup事件分发机制知道,View事件分发机制从dispatchTouchEvent()开始

源码总结

在这里插入图片描述
在这里插入图片描述

这里需要特别注意的是,onTouch()的执行 先于 onClick()

核心方法总结

主要包括:dispatchTouchEvent()、onTouchEvent()

dispatchTouchEvent()

返回true,表示事件分发结束了,事件被消费了

返回false,表示事件没有被消费,将事件传给上层

onTouchEvent()

若该控件可点击,就返回true,表明事件被当前这个View消费

若该控件不可点击,就返回false,表明事件没有被消费,将事件上传给上层ViewGroup的onTouchEvent处理

在这里插入图片描述

源码分析

dispatchTouchEvent()

展示核心逻辑:

在这里插入图片描述

上图分析:

  1. 判断触摸事件是否符合安全策略
  2. 判断当前的事件是否是通过鼠标进行滚动条的拖动,是的话就返回true,表示事件被消费了
  3. 判断是否注册了事件,也就是判断当前的View有没有注册TouchListener,如果注册了,那就不往下走了,说明事件已经被消费了,如果没注册,那就继续往下走,执行onTouchEvent()来消费这个事件。
    1. 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true,因为result默认是false的;否则执行onTouchEvent()。 1. (mViewFlags & ENABLED_MASK) == ENABLED 2. mOnTouchListener != null 3. mOnTouchListener.onTouch(this, event)

    2. 具体分析可以看下面

  4. 经过1 2 后 result依然是false,并且 onTouchEvent() 返回true,那就表示事件被消费了。
    1. 事件也就是从这里进入到 onTouchEvent() 方法里面去了

三个条件的分析

/**
 * 条件1:(mViewFlags & ENABLED_MASK) == ENABLED
 * 说明:
 *    1. 该条件是判断当前点击的控件是否enable
 *    2. 由于很多View默认enable,故该条件恒定为true(除非手动设置为false)
 */

/**
 * 条件2:mOnTouchListener != null
 * 说明:
 *   1. mOnTouchListener变量在View.setOnTouchListener()里赋值
 *   2. 即只要给控件注册了Touch事件,mOnTouchListener就一定被赋值(即不为空)
 */
public void setOnTouchListener(OnTouchListener l) {

        mOnTouchListener = l;

        }

/**
 * 条件3:mOnTouchListener.onTouch(this, event)
 * 说明:
 *   1. 即回调控件注册Touch事件时的onTouch();
 *   2. 需手动复写设置,具体如下(以按钮Button为例)
 */
        button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {

        return false;
        // 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
        // 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)
        // onTouchEvent()源码分析 -> 分析1
        }
        });
onTouchEvent()

这个方法代码量也很多,但是关于事件分发的逻辑不多

总结就是:

若该控件可点击,就返回true,表明事件被当前这个View消费

若该控件不可点击,就返回false,表明事件没有被消费,将事件上传给上层ViewGroup的onTouchEvent处理

/**
 * 源码分析:View.dispatchTouchEvent()
 */
public boolean dispatchTouchEvent(MotionEvent event) {


        if ( (mViewFlags & ENABLED_MASK) == ENABLED &&
        mOnTouchListener != null &&
        mOnTouchListener.onTouch(this, event)) {
        return true;
        }

        return onTouchEvent(event);
        }


/**
 * 分析1:onTouchEvent()
 */
public boolean onTouchEvent(MotionEvent event) {

        ... // 仅展示关键代码

        // 若该控件可点击,则进入switch判断中
        if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

        // 根据当前事件类型进行判断处理
        switch (event.getAction()) {

        // a. 事件类型=抬起View(主要分析)
        case MotionEvent.ACTION_UP:
        performClick();
        // ->>分析2
        break;

        // b. 事件类型=按下View
        case MotionEvent.ACTION_DOWN:
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
        break;

        // c. 事件类型=结束事件
        case MotionEvent.ACTION_CANCEL:
        refreshDrawableState();
        removeTapCallback();
        break;

        // d. 事件类型=滑动View
        case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();

        int slop = mTouchSlop;
        if ((x < 0 - slop) || (x >= getWidth() + slop) ||
        (y < 0 - slop) || (y >= getHeight() + slop)) {
        removeTapCallback();
        if ((mPrivateFlags & PRESSED) != 0) {
        removeLongPressCallback();
        mPrivateFlags &= ~PRESSED;
        refreshDrawableState();
        }
        }
        break;
        }

        // 若该控件可点击,就一定返回true
        return true;
        }
        // 若该控件不可点击,就一定返回false
        return false;
        }

实例分析

在本示例中,将分析两种情况:

  1. 注册Touch事件监听 且 在onTouch()返回false
  2. 注册Touch事件监听 且 在onTouch()返回true
分析1:注册Touch事件监听 且 在onTouch()返回false

代码示例

// 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false
button.setOnTouchListener(new View.OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
        System.out.println("执行了onTouch(), 动作是:" + event.getAction());
        return false;
        }
        });

// 2. 注册点击事件OnClickListener()
        button.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
        System.out.println("执行了onClick()");
        }
        });

测试结果

执行了onTouch(), 动作是:0
执行了onTouch(), 动作是:1
执行了onClick()

测试结果说明

  • 点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次onTouch();
  • 因为onTouch()返回了false,所以事件无被消费,会继续往下传递,即调用View.onTouchEvent();
  • 调用View.onTouchEvent()时,对于抬起View事件,在调用performClick()时,因为设置了点击事件,所以会回调onClick()。
分析2:注册Touch事件监听 且 在onTouch()返回true

代码示例

// 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false
button.setOnTouchListener(new View.OnTouchListener() {

        @Overridepublic boolean onTouch(View v, MotionEvent event) {
            System.out.println("执行了onTouch(), 动作是:" + event.getAction());return true;}});// 2. 注册点击事件OnClickListener()
button.setOnClickListener(new View.OnClickListener() {

        @Overridepublic void onClick(View v) {
            System.out.println("执行了onClick()");}});

测试结果

执行了onTouch(), 动作是:0
执行了onTouch(), 动作是:1

测试结果说明

  • 点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次onTouch();
  • 因为onTouch()返回了true,所以事件被消费,不会继续往下传递,View.dispatchTouchEvent()直接返回true;
  • 所以最终不会调用View.onTouchEvent(),也不会调用onClick()。

至此,关于Android事件分发机制的内容已经讲解完毕,即Activity、ViewGroup、View的事件分发机制。

即:ActivityViewGroupView 的事件分发机制

四、常见事件分发场景

提示:【四】 这里 可以搭配下面的 【五】来看,方便理解

下面,我将通过实例说明常见的事件传递情况 & 流程

一般的事件传递场景有:

  • 默认情况
  • 处理事件
  • 拦截DOWN事件
  • 拦截后续事件(MOVEUP

背景描述

讨论的布局如下:

在这里插入图片描述

情景:

  1. 用户先触摸到屏幕上View C上的某个点(图中黄区)

Action_DOWN事件在此处产生

  1. 用户移动手指
  2. 最后离开屏幕

一般的事件传递情况

一般的事件传递场景有:

  • 默认情况
  • 处理事件
  • 拦截DOWN事件
  • 拦截后续事件(MOVEUP

场景1:默认场景

  • 即不对控件里的方法(dispatchTouchEvent()onTouchEvent()onInterceptTouchEvent())进行重写 或 更改返回值
  • 那么调用的是这3个方法的默认实现:调用下层的方法 & 逐层返回
  • 事件传递情况:(呈U型)
  1. 从上往下调用dispatchTouchEvent()

Activity A ->> ViewGroup B ->> View C

  1. 从下往上调用onTouchEvent()

View C ->> ViewGroup B ->> Activity A

在这里插入图片描述

注:虽然ViewGroup BonInterceptTouchEvent()对DOWN事件返回了false,但后续的事件(MOVE、UP)依然会传递给它的onInterceptTouchEvent() 这一点与onTouchEvent()的行为是不一样的:不再传递 & 接收该事件列的其他事件

同一个事件序列:后续事件

同一个事件序列,如果子View(ViewGroup)没有处理改事件(没有消费这个事件)

那么后续事件就不会再传递到子View中,直接调用Activity的onTouchEvent方法

场景2:处理事件

View C希望处理该点击事件,即:设置View C为可点击的(Clickable) 或 复写其onTouchEvent()返回true

最常见的:设置Button按钮来响应点击事件

事件传递情况:(如下图)

  • DOWN事件被传递给C的onTouchEvent方法,该方法返回true,表示处理该事件
  • 因为View C正在处理该事件,那么DOWN事件将不再往上传递给ViewGroup B 和 Activity AonTouchEvent()
  • 该事件列的其他事件(Move、Up)也将传递给View ConTouchEvent()

在这里插入图片描述

会逐层往dispatchTouchEvent() 返回,最终事件分发结束

场景3:拦截DOWN事件

假设ViewGroup B希望处理该点击事件,即ViewGroup B复写了onInterceptTouchEvent()返回trueonTouchEvent()返回true 事件传递情况:(如下图)

  • DOWN事件被传递给ViewGroup BonInterceptTouchEvent(),该方法返回true,表示拦截该事件,即ViewGroup自己处理该事件(事件不再往下传递)
  • 调用自身的onTouchEvent()处理事件(DOWN事件将不再往上传递给Activity AonTouchEvent()
  • 该事件列的其他事件(Move、Up)将直接传递给ViewGroup BonTouchEvent()

注:

  1. 该事件列的其他事件(Move、Up)将不会再传递给ViewGroup BonInterceptTouchEvent();因:该方法一旦返回一次true,就再也不会被调用
  2. 逐层往dispatchTouchEvent() 返回,最终事件分发结束

在这里插入图片描述

场景4:拦截DOWN的后续事件

如果一个ViewGroup在DOWN事件时没有拦截(即返回false),但在后续的MOVE事件中拦截了事件(返回true),则会发生以下情况:

  1. DOWN事件被首个接收到它的View处理,通常是一个子View。这意味着DOWN事件将传递给该子View的onTouchEvent()方法进行处理。
  2. 当MOVE事件到达ViewGroup时,ViewGroup会拦截该事件,不再将其传递给子视图。这是因为在MOVE事件中,ViewGroup已经表明了它有意拦截并处理后续的事件。
  3. 一旦ViewGroup拦截了MOVE事件,它将负责处理后续的触摸事件,而不会再将其传递给内部的子视图。这意味着在MOVE事件之后,ViewGroup的onTouchEvent()方法将被调用来处理触摸事件。

这种行为通常被用来实现一些特定的交互效果,例如拖拽操作或滑动手势,其中在一开始由子视图处理DOWN事件,但是在后续的MOVE事件中被ViewGroup拦截以实现整体的滑动或拖动效果。

结论

  • ViewGroup 拦截了一个半路的事件(如MOVE),该事件将会被系统变成一个CANCEL事件 & 传递给之前处理该事件的子View
  • 该事件不会再传递给ViewGrouponTouchEvent()
  • 只有再到来的事件才会传递到ViewGrouponTouchEvent()

场景描述 ViewGroup B 无拦截DOWN事件(还是View C来处理DOWN事件),但它拦截了接下来的MOVE事件

DOWN事件传递到View ConTouchEvent(),返回了true

实例讲解

  • 在后续到来的MOVE事件,ViewGroup BonInterceptTouchEvent()返回true拦截该MOVE事件,但该事件并没有传递给ViewGroup B ;这个MOVE事件将会被系统变成一个CANCEL事件传递给View ConTouchEvent()
  • 后续又来了一个MOVE事件,该MOVE事件才会直接传递给ViewGroup BonTouchEvent()

后续事件将直接传递给ViewGroup BonTouchEvent()处理,而不会再传递给ViewGroup BonInterceptTouchEvent(),因该方法一旦返回一次true,就再也不会被调用了。

  • View C再也不会收到该事件列产生的后续事件

在这里插入图片描述

至此,关于Android常见的事件传递情况 & 流程已经讲解完毕。

为什么第一个move没有被拦截?

问题回顾

这个问题是关于【场景4:拦截DOWN的后续事件】

就是当我们ViewGroup的onInterceptTouchEvent中,选择了只拦截MOVE事件,我们手指点击View处,再平移一段距离,再抬起,会发生下面三点

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG,"MyViewGroup: onInterceptTouchEvent");
        super.onInterceptTouchEvent(ev);
        // 在MOVE事件时拦截事件
        if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
            return true; // 拦截MOVE事件
        }
        return false; // 不拦截DOWN事件
    }
  • DOWN事件,没有被拦截,传到了MyView消费了
  • 第一个MOVE事件,onInterceptTouchEvent 检测到了,但是没有拦截,还是传到了MyView
  • 后续的MOVE事件,不再传到MyView,MyViewGroup直接消费了。

为什么第一个MOVE不拦截呢?

原因:

简单讲:如果DOWN事件传给了子View,但是父ViewGroup要拦截后续的MOVE事件,那么第一个MOVE事件变成了CANLE事件传给了子VIEW。

详细讲:

当 当前view消费了ACTION_DOWN之后,在后续的ACTION_MOVE过程中,父容器拦截了事件,这时候,就会先将当前这个ACTION_MOVE转变成ACTION_CANCEL事件,发送给之前消费ACTION_DOWN事件的View,同时会将mFirstTouchTarget置为null,这样的话,在下一个ACTION_MOVE到来时,就不会走拦截和分发流程,直接将ACTION_MOVE事件交给父容器处理。

源码分析

ViewGroup关于这部分的源码我分成三部分来展示

第一部分:

判断事件是否需要拦截,如果父容器拦截,那么intercepted = true

     //第一部分,判断事件是否需要拦截,disallowIntercept的值可由子view调用requestDisallowInterceptTouchEvent
        //来改变,可以看出子类调用requestDisallowInterceptTouchEvent(false)时,disallowIntercept置为false,
        //允许父容器拦截,onInterceptTouchEvent返回true时,intercepted = true,
        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 {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }

第二部分

intercepted为true时,调用了这个dispatchTransformedTouchEvent 这个方法,给它的参数cancel传递了true

        final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
        //intercepted为true时,cancelChild为true,dispatchTransformedTouchEvent调用时,传入true
        if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
            handled = true;
        }
        //消费ACTION_CANCEL之后,将mFirstTouchTarget 置空,因此在后面的MOVE事件来到时,不会走入第一部分的拦截判断
        //会走入else分支,intercepted = true;将后续MOVE交给自己处理。
        if (cancelChild) {
          mFirstTouchTarget 置空
        }
        //cancel为true

第三部分

dispatchTransformedTouchEvent这个方法做了什么?

主要是下面黄底的那两行:

  • 如果cancle为ture,或者当前的事件是CANCLE事件
  • 那么就把事件设置为了CANCLE事件
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // cancel为true时,event.setAction(MotionEvent.ACTION_CANCEL),将MOVE事件变成ACTION_CANCEL事件。
        // child为消费ACTION_DOWN的view。child不为空,即消费ACTION_CANCEL。
        // 需要注意的是,手指移动时ACTION_MOVE会多次触发,这里消费的是当前转换来的MOVE事件,
        // 当下一个MOVE事件来的时候,mFirstTouchTarget已经为空了,所以会将后续的ACTION_MOVE事件交给父容器处理。
        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);
            }
            //转换为CANCEL事件并消费后,重新设置回ACTION_MOVE类型。
            event.setAction(oldAction);
            return handled;
        }
     }

总结

所以,看了源码就解释了,为什么第一个MOVE会成“漏网之鱼”,那是因为,DOWN事件传下去了,但是MOVE被拦截了,那么第一个MOVE事件会被变成CANCLE事件,被消费,当下一个MOVE事件来的时候,mFirstTouchTarget已经为空了,所以会将后续的ACTION_MOVE事件交给父容器处理。

参考博客:Android事件分发机制

参考视频:彻底搞懂Android事件分发机制_哔哩哔哩_bilibili

五、Demo验证

验证1:默认场景

场景解释:默认场景,不做任何改动。

我们在MainActivity、MyView、MyViewGroup里面重写了 事件分发的主要的方法

代码

MainActivity

public class MainActivity extends AppCompatActivity {
    public static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG,"MainActivity: dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG,"MainActivity: onTouchEvent");
        return super.onTouchEvent(event);
    }
}

MyView

public class MyView extends View {
    public static final String TAG = "MainActivity";
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG,"MyView: onTouchEvent");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(TAG,"MyView: dispatchTouchEvent");
        return super.dispatchTouchEvent(event);
    }
}

MyViewGroup

为什么要继承FrameLayout,而不去继承ViewGroup? 因为默认情况下ViewGroup中没有子View的布局,为了避免重新去写布局,这就直接继承FrameLayout,因为FrameLayout继承了ViewGroup

public class MyViewGroup extends FrameLayout {
    public static final String TAG = "MainActivity";
    public MyViewGroup(@NonNull Context context) {
        super(context);
    }

    public MyViewGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public MyViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG,"MyViewGroup: dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG,"MyViewGroup: onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG,"MyViewGroup: onTouchEvent");
        return super.onTouchEvent(event);
    }
}

activity_main.xml

外层用了MyViewGroup ,内层用了MyView,简单设置了颜色,外层蓝色,内层红色

<?xml version="1.0" encoding="utf-8"?>
<com.example.eventdistributiondemo.MyViewGroup 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/design_default_color_primary_variant"
    tools:context=".MainActivity"
    >

    <com.example.eventdistributiondemo.MyView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="@color/design_default_color_error"
        ></com.example.eventdistributiondemo.MyView>
    
</com.example.eventdistributiondemo.MyViewGroup>

效果

模拟用户: 触摸View -> 抬起

在这里插入图片描述

输出:

可以看到,上面6行 是 Down事件,下面2行是Up事件

MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent
MyViewGroup: onTouchEvent
MainActivity: onTouchEvent
MainActivity: dispatchTouchEvent
MainActivity: onTouchEvent

验证了:

  • 呈 U 形 的传递
  • 同一个序列的后续事件是不会往下传递的

验证2:处理事件

场景解释:让View去处理这个事件。

代码

只需要将上面【验证1】的代码的MyView 的 onTouchEvent 方法 返回true,表示MyView处理了这个事件

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG,"MyView: onTouchEvent");
//        return super.onTouchEvent(event);
        super.onTouchEvent(event);
        return true;
    }

效果

模拟用户: 触摸View -> 抬起

在这里插入图片描述

输出:

可以看到,DOWN 和 UP 事件都传到了MyView

MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent

验证3:拦截DOWN场景

场景解释:让ViewGroup拦截事件,并去处理这个事件。

只需要将上面【验证1】的代码的 MyViewGroup 改一下,让onInterceptTouchEvent()返回trueonTouchEvent()返回true,表示MyViewGroup 处理这个事件,不会再向下传递到MyView了。

代码

MyViewGroup 代码更改:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG,"MyViewGroup: onInterceptTouchEvent");
//        return super.onInterceptTouchEvent(event);
        super.onInterceptTouchEvent(ev);
        return true;//返回true,直接拦截
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG,"MyViewGroup: onTouchEvent");
//        return super.onTouchEvent(event);
        super.onTouchEvent(event);
        return true;//返回true,表示事件有MyViewGroup来处理
    }

效果

模拟用户: 触摸View -> 抬起

在这里插入图片描述

输出:

可以看到,确实是没有经过MyView

MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyViewGroup: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onTouchEvent

验证4:拦截DOWN的后续事件

场景解释:让ViewGroup只拦截MOVE事件,其他事件不拦截,并且View和ViewGroup都能处理这个事件,就看会事件是怎么分发的了。

代码

在【验证1】的基础上改:

MyViewGroup

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG,"MyViewGroup: onInterceptTouchEvent");
        super.onInterceptTouchEvent(ev);
        // 在MOVE事件时拦截事件
        if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
            return true; // 拦截MOVE事件
        }
        return false; // 不拦截DOWN事件
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG,"MyViewGroup: onTouchEvent");
//        return super.onTouchEvent(event);
        super.onTouchEvent(event);
        return true;//返回true,表示事件有MyViewGroup来处理
    }

MyView

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG,"MyView: onTouchEvent");
//        return super.onTouchEvent(event);
        super.onTouchEvent(event);
        return true;
    }

效果

用户 DOWN -》MOVE-》MOVE-》MOVE … -》UP

在这里插入图片描述

输出

MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onTouchEvent
.......

讲解:

1-5行:DOWN事件,没有被拦截,传到了MyView消费了

6-10行:第一个MOVE事件,onInterceptTouchEvent 检测到了,但是没有拦截,还是传到了MyView

11-13以及后续的:后续的MOVE事件,不再传到MyView,MyViewGroup直接消费了。

六、Down事件和其他事件的区别

上面的Demo的验证1里面,验证了一个结论:同一个序列的后续事件是不会往下传递的

也就是说Down事件传递了下去,最终结束,而同一个序列的其他事件是不会再全部走的,而是直接在对应位置结束了事件分发。

可以比喻成:Down事件是探路的先锋,后续事件根据Down事件的路程,直接走了捷径。

嵌套ViewGroup的问题

上面我们的例子中都是一个ViewGroup,下面我们多嵌套一个,最后的结论是一样的,只是三层的结构变成了四层,规律不变,可以根据下面的图,来分析。

情况1:各层的dispatchTouchEvent 返回true

首先要明确:dispatchTouchEvent 返回true,就是事件分发结束了

这种情况的总结:红色的down事件 和 蓝色的其他事件 走的路径相同

在ViewGroup1 的dispatchTouchEvent 方法返回true

这里多嵌套了一个ViewGroup

红色的箭头代表ACTION_DOWN 事件的流向 蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

在这里插入图片描述

在ViewGroup2 的dispatchTouchEvent 返回true

在这里插入图片描述

同理在View的dispatchTouchEvent 返回true

与上图效果类似,不做图了

情况2:各层的的onTouchEvent 返回true消费这次事件

首先要明确:onTouchEvent 返回true,就是事件分发结束了,且被当前的View\ViewGroup\Activity消费了

这种情况的总结:红色的down事件是走了全程,而蓝色的其他事件是走了“捷径”

View的的onTouchEvent 返回true消费这次事件

在这里插入图片描述

在ViewGroup 2 的onTouchEvent 返回true消费这次事件

在这里插入图片描述

从这里就能看出区别了,红色的down事件是走了全程,而蓝色的其他事件是走了捷径

在ViewGroup 1 的onTouchEvent 返回true消费这次事件

在这里插入图片描述

Activity 的onTouchEvent 返回true消费这次事件

在这里插入图片描述

情况3:各层的dispatchTouchEvent 返回false

首先要明确:

Activity:dispatchTouchEvent 返回false,事件分发结束,事件没有被消费

ViewGroup:dispatchTouchEvent 返回false,会将事件回传到上层Activity.OnTouchEvent处,表示事件没有被ViewGroup消费。

View:dispatchTouchEvent 返回false,会将事件回传到上层ViewGroup.OnTouchEvent处,表示事件没有被View消费。

这种情况的总结:红色的down事件是走了全程,而蓝色的其他事件是走了“捷径”

我们在View的dispatchTouchEvent 返回false并且Activity的onTouchEvent 返回true消费这次事件

在这里插入图片描述

在View的dispatchTouchEvent 返回false并且ViewGroup 1 的onTouchEvent 返回true消费这次事

在这里插入图片描述

在View的dispatchTouchEvent 返回false并且在ViewGroup 2 的onTouchEvent 返回true消费这次事件

在这里插入图片描述

在ViewGroup2的dispatchTouchEvent 返回false并且在ViewGroup1 的onTouchEvent返回true消费这次事件

在这里插入图片描述

情况4:ViewGroup的onInterceptTouchEvent返回true

首先要明确:

onInterceptTouchEvent返回true,就是ViewGroup要拦截这个事件,事件不会再向下传递

这种情况的总结:红色的down事件是走了全程,而蓝色的其他事件是走了“捷径”

在ViewGroup2的onInterceptTouchEvent 返回true拦截此次事件并且在ViewGroup 1 的onTouchEvent返回true消费这次事件。

在这里插入图片描述

我在上面验证1里只展示了一种例子,如果想看更多的例子,可以看这个博客,结论是一样的

具体示例可以参考https://www.jianshu.com/p/e99b5e8bd67b

参考

总结部分:可以去看参考文章【1】里面的总结

参考文章:

【1】Carson带你学Android:手把手带你深入分析事件分发机制!

【2】 图解 Android 事件分发机制 - 简书 (jianshu.com)

【3】 Android事件分发机制–拿来吧你 - 简书 (jianshu.com)

参考视频:

【1】 Android事件分发机制-慕课网 (imooc.com)

【2】 彻底搞懂Android事件分发机制_哔哩哔哩_bilibili

  • 37
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值