Android View点击事件分发原理,源码解读

View点击事件分发原理,源码解读

  • 前言
  • 1. 原理总结
    • 2.1 时序图总结
    • 2.2 流程图总结
  • 2. 源码解读
    • 2.1 Activity到ViewGroup
    • 2.2 ViewGroup
      • 事件中断
      • 逆序搜索
      • 自己处理点击事件
      • ViewGroup总结
    • 2.3 View
      • OnTouchListener
      • onTouchEvent
  • 3. 附录:时序图uml代码

前言

两年前我曾经写过一篇点击事件的原理博客,在今年重新翻看的时候发现文章的结构不好,且没有总结,让人不容易理解,所以重新整理了一下再写一次。

1. 原理总结

注意:正文中虽然说的都是点击事件,实际上他并不是指我们常用语境中的onClick或者onLongClick,而是任意类型的事件,只是用点击事件来形容比较让人容易理解,实际上视图的事件分发是包括按下,抬起,移动这三个部分的。

MotionEvent.ACTION_DOWN按下View(所有事件的开始)
MotionEvent.ACTION_UP抬起View(与DOWN对应)
MotionEvent.ACTION_MOVE移动View
MotionEvent.ACTION_CANCEL结束事件(非人为原因)

再我看完点击事件分发的原理之后,我会用三个词来形容点击事件的全部原理:

  1. View树:

    首先我们需要知道的是,在Android中我们所写的视图代码,无论是xml还是通过代码手动添加的,其数据结构展现出来的就是一个树,一个一对多的存储关系的集合。

    在代码中我们表现这个树状数据结构的方式:在ViewGroup中设置了一个子View的List,通过让最上层的父节点DevorView(他也是一个ViewGroup)—持有好几个ViewGroup,里面的ViewGroup中每个又持有多个ViewGroup,层层嵌套到最底层的View为止。

  2. 深度搜索优先dfs:

    在用户触发任意一个点击事件的时候,我们是通过深度搜索优先的方式去寻找可以消费该点击事件的视图,从树的最深层开始处理点击事件。

    这个代表了什么意思呢?如果一个ViewGroup和它的子View同时都设置了OnClickListener,那么我们在点击它们之间重合的部分时,只会触发子View的点击事件而不会触发父View的点击事件。

    在代码表现这个深度搜索的方式:处理分发事件的时候,会先用for循环把所有的子View遍历,尝试调用子View的方法来处理该点击事件,只有确认所有子View都不能处理该点击事件之后,才会调用自己的点击事件处理方法。

  3. 逆序遍历:

    触发点击事件搜索子View的时候,总有个搜索的顺序,这个顺序是逆序,也就是从最后一个添加的子View开始查找和处理点击事件。

    其原理和添加VIew是相关联的,我们知道,在一个ViewGroup的两个子View中,如果这两个子View有重合的部分,那么一般而言总是后添加的视图会覆盖掉前面添加的视图的部分。

    点击事件也是同理,当用户点击他们重合的部分时,一般而言用户总是希望点击到用户本身可以看见的那个视图。所以我们的点击事件分发就和添加视图的顺序相反,从最后添加的视图开始遍历。
    在这里插入图片描述

2.1 时序图总结

为了方便我们看完源码之后以后复习方便,我先将点击事件分发的全部流程放到这里,在看的过程中有需要可以翻回来看:
在这里插入图片描述

2.2 流程图总结

由于时序图一般而言没有办法很好的展示我们深度优先搜索的思想,所以我额外又补充了一张流程图,这个流程图也画出了点击事件分发的原理:
在这里插入图片描述

2. 源码解读

2.1 Activity到ViewGroup

任何的事件源头都是从我们底层的SurfaceFlinger进程来的,他直接管理着用户可以看到的窗口,但是我们这里不用去深究那么底层的原理。只要知道从底层来的点击事件第一个触发的是Activity的DispatchTouchEvent就足够了。

public class Activity extends ContextThemeWrapperimplements LayoutInflater.Factory2,Window.Callback, KeyEvent.Callback,OnCreateContextMenuListener, ComponentCallbacks2,Window.OnWindowDismissedCallback,ContentCaptureManager.ContentCaptureClient {/*** 调用以处理触摸屏事件。您可以重写此方法,在将所有触摸屏事件发送到窗口之前拦截它们。* 请确保为应该正常处理的触摸屏事件调用此实现。* * @param ev 点击事件本体* @return boolean 如果事件被消费了会return true*/public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}// 重点是这行if (getWindow().superDispatchTouchEvent(ev)) {return true;}return onTouchEvent(ev);}
}

点击事件就这样从Activity手上分出去了,接下来看看Window类是如何处理的,顺便一提,Window本身是一个抽象类,作为他承载的实体一般而言是PhoneWindow类。

public class PhoneWindow extends Window implements MenuBuilder.Callback {private DecorView mDecor;@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);}
}

事件就这样直接转到了DecorView。

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {final Window.Callback cb = mWindow.getCallback();return cb != null && !mWindow.isDestroyed() && mFeatureId < 0? cb.dispatchTouchEvent(ev) : /* 重点看这部分 */super.dispatchTouchEvent(ev);}
}

DecorView作为最上层的特殊View,他在处理事件的时候会有特殊的窗口判断,但是一般而言是不会触发的,我们不去理他,重点看super.dispatchTouchEvent(ev),这个就代表了点击事件的真正起点。

2.2 ViewGroup

接下来就进入到我们真正的主角,ViewGroup了,我会将他的源码切成好几段一点点的说明。

事件中断

在正式开始点击事件之前,ViewGroup会通过onInterceptTouchEvent这个方法对点击事件做一个中断判断,如果被中断了就不会处理后续的流程了

onInterceptTouchEvent这个方法一般而言都是会返回false,也就是不中断。如果你有业务上的需求需要中断的话,可以返回true。这样事件就不会往下面的View分发,只会由自己进行处理。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {boolean handled = false;// 检查事件是否被中断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);} else {intercepted = false;}} else {intercepted = true;}// 如果被拦截,就会跳过分发的流程if (!canceled && !intercepted) {// ...正式分发点击事件}// 自己处理点击事件return handled;}public boolean onInterceptTouchEvent(MotionEvent ev) {if (ev.isFromSource(InputDevice.SOURCE_MOUSE)&& ev.getAction() == MotionEvent.ACTION_DOWN&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)&& isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) {return true;}return false;}
}

逆序搜索

开始正式处理点击事件,会先逆序遍历所有的子View,然后进行一些判断

  1. 该子View能否点击。canReceivePointerEvents
  2. 用户点击的位置是否和该View重合。isTransformedTouchPointInView

两个判断条件都符合后,就会尝试在该子View中处理点击事件
注意,子View也有可能是一个ViewGroup,所以调用子View的dispatchTouchEvent后,有可能会实际上调用的还是ViewGroup.dispatchTouchEvent。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {boolean handled = false;// 检查事件是否被中断final boolean intercepted;TouchTarget newTouchTarget = null;if (!canceled && !intercepted) {// ...正式处理点击事件final int childrenCount = mChildrenCount;for (int i = childrenCount - 1; i >= 0; i--) {// 不用关注他的原理,我们只需要他掏出了一个View即可。final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);// 对View做合法性判断,如果合法就可以继续点击if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}newTouchTarget = getTouchTarget(child);// 转换为点击事件,注意child这个入参我们是有传值的if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//...处理了一些逻辑break; //然后直接跳出循环}}// 下面又开始处理其他逻辑}return handled;}/*** 分发转换为点击事件*/ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {event.setAction(MotionEvent.ACTION_CANCEL);// 注意child这个入参,我们此时传入的child不为空,所以走下面if (child == null) {handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}event.setAction(oldAction);return handled;}}
}

自己处理点击事件

在遍历了所有子View都没有处理掉该事件之后,ViewGroup会尝试自己来处理该事件。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {boolean handled = false;TouchTarget newTouchTarget = null;if (!canceled && !intercepted) {// ...正式处理点击事件TouchTarget newTouchTarget = null;for (int i = childrenCount - 1; i >= 0; i--) {// 刚才的for循环,用来表示代码的相对位置}// 如果点击事件之前被子View给处理了,// 那么代码到这里之后newTouchTarget就不为空,或者mFirstTouchTarget不为空if (newTouchTarget == null && mFirstTouchTarget != null) {newTouchTarget = mFirstTouchTarget;while (newTouchTarget.next != null) {newTouchTarget = newTouchTarget.next;}newTouchTarget.pointerIdBits |= idBitsToAssign;}}// 这里和上面是连着的,如果mFirstTouchTarget== null其实就代表着点击事件没有被子View给处理if (mFirstTouchTarget == null) {handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {// 处理其他逻辑}return handled;}/*** 分发转换为点击事件*/ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {event.setAction(MotionEvent.ACTION_CANCEL);// 注意child这个入参,我们此时传入的child为空,所以走上面if (child == null) {handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}event.setAction(oldAction);return handled;}}
}

ViewGroup总结

要不然就是通过某个子View层层遍历,走到最深层的某个View的dispatchTouchEvent
要不然就是没有子View,调用自己的super,dispatchTouchEvent,还是View的dispatchTouchEvent

总而言之,代码就会通过这两种方式走到View这个类里面,
ViewGroup的dispatchTouchEvent这个方法的功能也很明显了:找一个可以处理该点击事件的View(可能是自己),将点击事件(TouchEvent)分发(Dispatch)给它(View)。

2.3 View

OnTouchListener

点击事件到View之后,入口还是DispatchTouchEvent,他会先检查是否有TouchListener,有的话先执行它。

这里有两个点,第一个点就是在View里面,OnClickListener和OnTouchListener是完全不同的东西。
第二个点就是OnTouchListener这个方法的return是有开发者自己控制的,换句话说,开发者可以自行控制事件是否要停在onTouch这里

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {public boolean dispatchTouchEvent(MotionEvent event) {boolean result = false;// 这个if一般而言都是通过的if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}ListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED// 重点看这里&& li.mOnTouchListener.onTouch(this, event)) {result = true;}// 如果事件没有被onTouch处理掉,就会进入事件处理流程if (!result && onTouchEvent(event)) {result = true;}}return result;}public interface OnTouchListener {boolean onTouch(View v, MotionEvent event);}
}

onTouchEvent

这里就是单个事件真正的处理方法,但是对于我们而言我们反而不需要太关注这个方法的处理逻辑。
第一是本文主要关注事件时如何分发到这里的。
第二是该方法无非就是对我们常用的一些逻辑,如focus,onClickListener,onLongClick等内容进行判断,有的话这个方法就会return true,没有的话就return false

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {public boolean onTouchEvent(MotionEvent event) {final int action = event.getAction();if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {switch (action) {case MotionEvent.ACTION_UP:// 一堆判断之后会走到这里,我们就不看这些判断了performClickInternal();break;case MotionEvent.ACTION_DOWN:break;case MotionEvent.ACTION_CANCEL:break;case MotionEvent.ACTION_MOVE:break;}return true;}return false;}private boolean performClickInternal() {notifyAutofillManagerOnClick();return performClick();}public boolean performClick() {notifyAutofillManagerOnClick();final boolean result;final ListenerInfo li = mListenerInfo;if (li != null && li.mOnClickListener != null) {playSoundEffect(SoundEffectConstants.CLICK);// 这里就是我们设置的点击事件了,OnClickListenerli.mOnClickListener.onClick(this);result = true;} else {result = false;}sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);notifyEnterOrExitForAutoFillIfNeeded(true);return result;}
}

3. 附录:时序图uml代码

@startumlparticipant Activity
participant PhoneWindow
participant DecorView
participant ViewGroup as vg
participant View as v
participant TouchListener as tl
participant "子View\nViewGroup" as cvgactivate Activity
Activity -> PhoneWindow : dispatchTouchEvent
activate PhoneWindowPhoneWindow -> DecorView : dispatchTouchEventactivate DecorViewDecorView -> vg : dispatchTouchEvent\n进入View树处理点击事件activate vg vg -> vg : onInterceptTouchEvent\n判断事件是否被拦截activate vg vg --> vg : return booleandeactivate vgalt truevg --> vg : return\n不进行任何点击事件的处理\n流程结束endloop 逆序遍历子Viewvg -> cvg : isTransformedTouchPointInView\n判断是否可以点击activate cvgcvg --> vg : return boolean\ntrue代表可以点击deactivate cvgalt 不能点击vg -> vg : continue\n搜索下一个子View	else 可以点击vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件activate vgvg -> cvg : dispatchTouchEvent\n重复该ViewGroup的行为,继续往下分发activate cvgbreak 点击事件被消费cvg --> vg : return boolean\n告知点击事件是否被消费enddeactivate cvgdeactivate vgendendalt 点击事件没被消费vg -> vg : dispatchTransformedTouchEvent\n将分发事件转变为点击事件activate vgvg -> v :dispatchTouchEventactivate valt 该View有TouchListenerv -> tl : onTouchactivate tltl --> v : return boolean\n告知是否继续往下处理deactivate tlend alt return truev --> vg : return true\n告知点击事件已经被处理else return falsev -> v :onTouchedactivate vv --> v : return boolean\n告知是否处理了点击事件deactivate vv --> vg : return boolean\n告知是否处理了点击事件enddeactivate vdeactivate vgendvg --> DecorView : return boolean\n告知是否处理了点击事件deactivate vgDecorView --> PhoneWindow : return boolean\n告知是否处理了点击事件deactivate DecorViewPhoneWindow --> Activity : return boolean\n告知是否处理了点击事件
deactivate PhoneWindow
deactivate Activity@enduml

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/32640.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nginx Proxy Manager反向代理Jackett

1 说明 最近折腾nas&#xff0c;发现npm反向代理Jackett后出现无法访问的问题&#xff0c;是因为外网访问jackett (例如https://domain.com:7373/jackett/UI/Dashboard)时&#xff0c;url会被重定向到https://domain.com/jackett/UI/Login?ReturnUrl%2Fjackett%2FUI%2FDashbo…

ubuntu链接mysql

C链接mysql 报错 sudo apt-get update sudo apt-get install libmysqlclient-dev 指令编译 g -o mysql_example mysql_example.cpp -I/usr/include/mysql -lmysqlclient g mysql_test.cpp mysql_config --cflags --libs 安装mysql sudo apt updatesudo apt install mysql-…

Java程序之动物声音“模拟器”

题目&#xff1a; 设计一个“动物模拟器”&#xff0c;希望模拟器可以模拟许多动物的叫声和行为&#xff0c;要求如下&#xff1a; 编写接口Animal&#xff0c;该接口有两个抽象方法cry()和getAnimalName()&#xff0c;即要求实现该接口的各种具体的动物类给出自己的叫声和种类…

尹会生:从零开始部署翻译助手【总结】

安装docker安装dify 工具准备 Docker 简介&#xff1a;可以在不同电脑上运行相同的容器&#xff0c;类似于把软件装在便携箱子里&#xff0c;随身携带。 优点&#xff1a;安装Docker可以简化部署过程&#xff0c;避免安装许多依赖性软件。 网址&#xff1a;https://www.docke…

【TOOL】ceres学习笔记(二) —— 自定义函数练习

文章目录 一、曲线方程1. 问题描述2. 实现方案 一、曲线方程 1. 问题描述 现有数学模型为 f ( x ) A e x B s i n ( x ) C x D f(x)Ae^xBsin(x)Cx^D f(x)AexBsin(x)CxD &#xff0c;但不知道 A A A 、 B B B 、 C C C 、 D D D 各参数系数&#xff0c;实验数据中含有噪声…

基于Java作业管理系统设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;…

Java——集合(一)

前言: Collection集合&#xff0c;List集合 文章目录 一、Collection 集合1.1 集合和数组的区别1.2 集合框架1.3 Collection 集合常用方法1.4 Collction 集合的遍历 二、List 集合2.1 List 概述2.2 List集合的五种遍历方式2.3 List集合的实现类 一、Collection 集合 1.1 集合和…

Vitis Accelerated Libraries 学习笔记--OpenCV 安装指南

目录 1. 简介 2. 安装过程 2.1 安装准备 2.2 编译并安装 XRT 2.2.1 下载 XRT 源码 2.2.2 安装依赖项 2.2.3 构建 XRT 2.2.4 打包 DEB 2.2.5 安装 XRT 2.3 编译并安装 OpenCV 2.3.1 下载 OpenCV 源码 2.3.2 创建目录 2.3.3 设置环境变量 2.3.4 构建 opencv 3. 总…

ping命令返回结果实例分析

测试在各相关情况下ping命令回复信息。 网络环境搭建如下图所示&#xff1a; 【1】R1、R2、PC1和PC2没有配置&#xff0c;测试ping命令回复 在路由器没有配置端口IP地址和路由&#xff0c;PC没有配置IP地址、子网掩码和网关的情况下&#xff0c;PC2 ping 192.168.1.1。 在PC没…

加速鸿蒙生态共建,蚂蚁mPaaS助力鸿蒙原生应用开发创新

6月21日-23日&#xff0c;2024华为开发者大会&#xff08;HDC 2024&#xff09;如期举行。在22日的【鸿蒙生态伙伴SDK】分论坛中&#xff0c;正式发布了【鸿蒙生态伙伴SDK市场】&#xff0c;其中蚂蚁数科旗下移动开发平台mPaaS&#xff08;以下简称&#xff1a;蚂蚁mPaaS&#…

QtCreator/VS中制作带有界面的动态库

1、首先创建动态库项目 class UNTITLED25_EXPORT Untitled25 {public:Untitled25(); };2、直接右键创建同名窗口类进行覆盖 3、引入global头文件并添加到处宏</

【SSM】

Spring常见面试题总结 Spring 基础 什么是 Spring 框架? Spring 是一款开源的轻量级 Java 开发框架&#xff0c;旨在提高开发人员的开发效率以及系统的可维护性。 我们一般说 Spring 框架指的都是 Spring Framework&#xff0c;它是很多模块的集合&#xff0c;使用这些模块…

转让神州开头的无区域科技公司需要多少钱

您好&#xff0c;我公司现有2家无区域神州名称的公司转让。所谓无区域名称是公司名称中不带有行政区划、及行业特点的公司名称&#xff0c;都是需要在工商总,局核准名称的&#xff0c;对于民营企业来说也比较喜欢这种名称名称很大气&#xff0c;现在重核更严格了&#xff0c;所…

Docker如何安装redis

目录 1. 拉取redis的镜像文件 2. 创建redis的容器卷 3. 准备reids的配置文件 4. 以配置文件启动redis 1. 拉取redis的镜像文件 # 默认安装最新版本 如果需要指定版本 docker pull redis:版本号 docker pull redis 详细版本请看dockerhub的官网&#xff1a; hub.docker…

MySQL中的ibd2sdi—InnoDB表空间SDI提取实用程序

ibd2sdi 是一个用于从 InnoDB 表空间文件中提取序列化字典信息&#xff08;Serialized Dictionary Information, SDI&#xff09;的实用程序。这个实用程序可以用于提取存储在持久化 InnoDB 表空间文件中的 SDI 数据。 可以对以下类型的表空间文件使用 ibd2sdi&#xff1a; 每…

DDS信号的发生器(验证篇)——FPGA学习笔记8

前言&#xff1a;第一部分详细讲解DDS核心框图&#xff0c;还请读者深入阅读第一部分&#xff0c;以便理解DDS核心思想 三刷小梅哥视频总结&#xff01; 小梅哥https://www.corecourse.com/lander 一、DDS简介 DDS&#xff08;Direct Digital Synthesizer&#xff09;即数字…

OneNote for Windows 10 下载

OneNote for Windows 10 安装 1.在浏览器中输入地址&#xff1a;https://apps.microsoft.com/detail/9wzdncrfhvjl?hlzh-cn&glUS2OneNote for Windows 10 - 在 Windows 上免费下载并安装 |Microsoft StoreOneNote 是用于在设备上捕获和组织你的一切内容的数字笔记本。快速…

BUG cn.bing.com 重定向的次数过多,无法搜索内容

BUG cn.bing.com 重定向的次数过多&#xff0c;无法搜索内容 环境 windows 11 edge浏览器详情 使用Microsoft Edge 必应搜索显示"cn.bing.com"重定向次数过多&#xff0c;无法进行正常的检索功能 解决办法 检查是否开启某些科_学_上_网&#xff08;翻_墙&#xf…

轻松重命名Windows用户Users目录下的文件夹名称

设置系统还原点 为避免设置失败&#xff0c;需提前准备好系统还原点以备份恢复系统。 打开系统属性&#xff1a; 在“系统保护”选项卡中&#xff0c;选择你想要保护的系统驱动器&#xff08;通常是C:驱动器&#xff09;。 点击“配置”按钮。 在弹出的窗口中&#xff0c;选…

【Python机器学习】NMF——将NMF应用于模拟信号数据

假设我们对一个信号感兴趣&#xff0c;它是由三个不同信号源合成的&#xff1a; import matplotlib.pyplot as plt import mglearnSmglearn.datasets.make_signals() plt.figure(figsize(6,1)) plt.plot(S,-) plt.xlabel(Time) plt.ylabel(Signal) plt.show()不幸的是&#xff…