深入 Flutter 和 Compose 的 PlatformView 实现对比,它们是如何接入平台控件

在上一篇《深入 Flutter 和 Compose 在 UI 渲染刷新时 Diff 实现对比》发布之后,收到了大佬的“催稿”,想了解下 Flutter 和 Compose 在 PlatformView 实现上的对比,恰好过去写过不少 Flutter 上对于 PlatformView 的实现,这次恰好可以用来和 Compose 做个简单对比:

Flutter

其实 Flutter 在 Android 上的 PlatformView 实现过去已经聊过好多次了,Flutter 作为完全脱离平台渲染树的独立 UI 库,它在混合开发的 PlatformView 实现可以说是“历经沧桑” 。

既然前面我们讲过很多次,这里主要就是简单介绍下,方便和 Compose 做个对比,感兴趣的可以去看后面的详细链接。

在 Flutter 上是通过 AndroidView 接入平台控件,目前活跃在 Android 平台的 PlatformView 支持主要有以下三种:

  • Virtual Display (VD)
  • Hybrid Composition (HC)
  • Texture Layer Hybrid Composition (TLHC)

为什么会有这么多不同模式支持?因为主要是随着技术推进和适配场景,PlatformView 的适配需求都在更新,但是新来的又不能完全提前之前的方案,所以就导致实现都并存下来。

VD

VD简单来说就是使用 VirtualDisplay 渲染原生控件到内存,然后利用 id 在 Flutter 界面上占用一个相应大小的位置,最后通过 id 关联到 Flutter Texture 里进行渲染。

问题也很明显,因为控件不会真实存在渲染的位置,可以不严谨理解,它只是内存里 UI 的“镜像”显示,或者说“副屏镜像”,所以此时的点击和对原生控件的操作,其实都是需要由 Flutter 这个 View 进行二次转发到原生再回到 Flutter 。

另外因为控件是渲染在内存里,所以和键盘交互需要通过二级代理处理,容易产生各种键盘输入和交互的异常问题,特别是 WebView 场景。

当然,现在的 VD 已经比初始的时候好很多,并且还在兼容“服役”。

HC

1.2 版本开始支持 HC 模式,这个版本就是直接把原生控件「覆盖」在 FlutterView 上进行堆叠,简单来说就是 HC 模式会直接把原生控件通过 addView 添加到 FlutterView 上 。如果出现 Flutter Widget 需要渲染在 Native Widget 上,就采用新的 FlutterImageView 来承载新图层。

比如在 Layout Inspector,HC 模式可以看出来各种原生布局的边界绘制:

而如下图所示,其中蓝色的文本是原生的 TextView ,红色的文本是 Flutter 的 Text 控件,在中间 Layout Inspector 的 3D 图层下可以清晰看到:

  • 两个蓝色的 TextView 是被添加在 FlutterView 之上,并且把没有背景色的红色 RE 遮挡住了
  • 最顶部有背景色的红色 RE 也是 Flutter 控件,但是因为它需要渲染到 TextView 之上,所以这时候多一个 FlutterImageView ,它用于承载需要显示在 Native 控件之上的纹理,从而达 Flutter 控件“真正”和原生控件混合堆叠的效果。

这里的 FlutterImageView ,其实还有一个作用,就是为了解决动画同步和渲染

当然,这样带来了一个问题,因为此时原生控件是直接渲染,所以需要在原生的平台线程上执行,纯在 Flutter 的 UI 线程就存在线程同步问题,所以在此之前一些场景下会有画面闪烁 bug 。

虽然这个问题最后也通过类似线程同步实现解决,但是也带来一定程度的性能开销,另外在 Android 10 之前还会存在 GPU->CPU->GPU的性能损耗,所以 HC 属于会性能开销较大,又需要原生控件特性的场景

TLHC

3.0 版本开始支持 TLHC 模式,最初的目的是取代上面这两种模式,可惜最终共存下来,该模式下控件虽然在还是布局在该有的位置上,但是其实是通过一个 FrameLayout 代理 onDraw 然后替换掉 child 原生控件的 Canvas 来实现混合绘制。

所以看到此时上图 TextView 里没有了内容,因为 TextView 里的 Canvas 被替换成 Flutter 在内存里创建的 Canvas

其实 TLHC 流程上和 VD 基本一样,简单对比 VirtualDisplayTextureLayer 的实现差异,可以看到主要还是在于原生控件纹理的提取方式上

从上图我们可以得知:

  • 从 VD 到 TLHC, Plugin 的实现是可以无缝切换,因为主要修改的地方在于底层对于纹理的提取和渲染逻辑

  • 以前 Flutter 中会将 AndroidView 需要渲染的内容绘制到 VirtualDisplays ,然后在 VirtualDisplay 对应的内存中,绘制的画面就可以通过其 Surface 获取得到;现在 AndroidView 需要的内容,会通过 View 的 draw 方法被绘制到 SurfaceTexture 里,然后同样通过 TextureId 获取绘制在内存的纹理

从这个简单流程上看,这里面的关键就在于 super.draw(surfaceCanvas); ,给 Android 的 View “模拟” 出来工作环境,然后通过“替换” Canvas 让 View 绘制需要的 Surface 上合成:

那 TLHC 有什么问题?因为它是通过“替换” Canvas 来得到 UI ,但是这种实现天然不支持 SurfaceView等场景,因为 SurfaceView 是自己独立的 Surface 和 Canvas,所以通过 parent 替换 Canvas 的实现并不支持。

所以目前的 PlatformVIew 支持上的结果:

  • 默认会是 TLHC 模式,如果发现接入的 View 是 SurfaceView ,那么就会“降级”使用 VD 来适配
  • 可以通过 initExpensiveAndroidView 接口强行使用 HC

详细链接:

https://blog.csdn.net/ZuoYueLiang/article/details/131800717
https://blog.csdn.net/ZuoYueLiang/article/details/124577097
https://blog.csdn.net/ZuoYueLiang/article/details/124805110

Compose

Compose 的 PlatformView 原理这里可以详细聊聊,这个目前的资料不多,比较有聊的价值。

众所周知,Jetpack Compose 虽然是 Android 平台的全新 UI 开发框架,但是它的 UI 渲染树和「传统 xml View 控件」是“不直接兼容”的,Compose 属于独立的 UI 库,它的 UI 模式更接近 Flutter ,但是 @Composable 函数又不是和 Flutter 一样 return ,在实际工作中,Compose 代码在编译时会给 @Composable 函数添加 Composer 参数 ,而实际的 UI Node Tree 等的创建,都是从“隐藏”的 Composer 开始:

详细可见:https://blog.csdn.net/ZuoYueLiang/article/details/145105060?spm=1001.2014.3001.5501

所以,一旦你需要在 Jetpack Compose 里接入一个原生控件,你就需要用到 PlatformView 的相关实现,PlatformView 本质上就是把「传统 xml View 控件」渲染进 Compose 渲染树里,而在 Compose 在 Android 平台,使用的就是 AndroidView

@Composable
fun CustomView() {var selectedItem by remember { mutableStateOf("Hello from View") }// Adds view to ComposeAndroidView(modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI treefactory = { context ->// Creates viewTextView(context).apply {text = "Hello from View"textSize = 30ftextAlignment = TextView.TEXT_ALIGNMENT_CENTER}}, update = { view ->// View's been inflated or state read in this block has been updated// Add logic here if necessary// As selectedItem is read here, AndroidView will recompose// whenever the state changes// Example of Compose -> View communicationview.text = selectedItem})
}

如上代码所示,通过 AndroidView 我们可以把一个 Android 传统的 TextView 添加到 Compose 里,当然这没什么实际意义,只是作为一个简单例子。

渲染之后,我们可以看到在 Layout Inspector 的 Component tree 里并没有 TextView ,因为它只是被渲染到 Compose 里,但是它其实并不是 “直接” 存在于 Compose 的 LayoutNode ,它只是“依附”在 AndroidView

想知道 AndroidView 的工作原理,我们需要看它的 factory 实现,从源码我们可以看到,它主要是通过 ViewFactoryHolder 创建了一个代理 layoutNode 来“进入” Compose 渲染树:

而 Android 上 ViewFactoryHolder 的实现主要在它基类 AndroidViewHolder ,这里可以看到 AndroidViewHolder 那可是一个“实实在在”的传统 ViewGroup 实现

我们可以假设,我们前面的传统 TextView ,在 AndroidView 内部实际上就是被添加到 AndroidViewHolder 这个 ViewGroup 里 ,而且这里还有一个 Owner ,从命名上也很“关键”。

带着这两个问题,我们继续看,首先我们在 AndroidViewHolder 里可以看到有 layoutNode 的实现,也就是其实这个 Holder ,它既是传统 ViewGroup ,又具备 Compose 里的 layoutNode 实现

通过查看 layoutNode 的实现,我们可以看到:

  • layoutNodeonAttach 到 Compose 布局里的时候,会执行 addAndroidView ,其实这里的 addAndroidView 内部,就是一个 ViewGroupaddView 操作
  • 另外,在 layoutNode 被 onDetach 时执行 removeAndroidView ,内部也就是 ViewGroupremoveViewInLayout
  • 另外还有通过 MeasurePolicy 处理布局,简单说就是将 Compose 的布局状态同步到 AndroidViewHolder 这个 ViewGroup 去布局,给 「传统 XML View」“模拟” 布局环境。

所以我们可以看到,AndroidViewHolder 类似一个“中转站”,它将 Compose UI 的生命周期和测绘布局状态同步到传统 ViewGroup 控件,从而给添加进来的 TextView “模拟” 出布局和绘制环境,大概可以总结:

AndroidViewHolder 类似于 Compose 代理 Node,它 Compose 中的 UI 环境“模拟”到 ViewGroup 中,通过控制 ViewGroup 的绘制与布局来控制我们的「传统 xml View 控件」

那么 AndroidViewHolder 肯定就是从 onAttach 开始进入 Compose 的 LayoutNode 体系工作,这里关键在于 AndroidComposeView 的这个操作:

(owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)

这里又冒出来一个新对象 AndroidComposeView ,它就是我们前面所说的 owner ,那它又是什么?

我们看 AndroidComposeView 的源码,可以看到 AndroidComposeView 同样是一个 ViewGroup ,它的内部主要是有一个 AndroidViewsHandlerViewGroup 在处理 AndroidViewHolder ,比如前面的 addAndroidView 就是将 Holder 添加到 Handler

那到这里就有三个东西:

  • AndroidComposeView
  • AndroidViewsHandler
  • AndroidViewHolder

它们都是传统 ViewGroup 的实现,且关系大概如下所示:

那么到这里,流程上我们应该就清晰了,我们只需要搞清楚 AndroidComposeView 是什么,来自哪里,然后往下,大概就可以理清它的实现。

我们通过 AndroidComposeView 内部有个 root 节点的实现,可以猜测它应该是一个顶层节点,所以我们直接从顶部开始找:

我们从 Activity 开始往下找,经过几个简单调整,就可以在 AbstractComposeView.setContent 找到创建 AndroidComposeView 的地方:

因为 AbstractComposeView 的实现是 ComposeView ,所以可以看到:

AndroidComposeView 是在初始时被 ComposeView 创建并 addView ,然后 Composition 里 UiApplier 的 root 节点就是 AndroidComposeView

所以这就是为什么前面我们那个 owner 为什么是 AndroidComposeView 的来源,然后往下就是 AndroidViewsHandler ,它主要就是持有所有 Holder ,然后根据调用给它的 children 执行各种布局和绘制操作:

所以我们就知道了:

  • 在初始化的时候,Compose 就会创建一个顶层 ViewGroup 节点 AndroidComposeView ,它是一个 root LayoutNode
  • AndroidComposeView 内部的 AndroidViewsHandler 会通过一个 hashMap 去触发和管理 children Holder 的布局和重绘
  • AndroidViewHolder 是一个代理 LayoutNode ,同时它将 Compose UI 的生命周期和测绘布局状态同步到传统 ViewGroup 控件

大概会是下面这样的结构,但是它虽然被 addViewViewGroup 里,但是它并不会直接渲染在 ViewGroup 里 ,而是「被代理渲染」到 LayoutNode 对应的 Scope 里

比如我们接入了两个 SurfaceView 到 Compose ,如果我们打印传统布局结构,大概可以看到这样的一个结果,:

这里举例的 SurfaceView 后面会顺便聊聊 。

最后就是绘制,知道流程后,我们直接看回 AndroidViewHolder 里的 layoutNode 实现,在这里有一个来自 drawBehindcanvas

一般情况下,drawBehind 修饰符可以想任何可组合函数后面绘制内容时,例如:

Text("Hello Compose!",modifier = Modifier.drawBehind {drawRoundRect(Color(0xFFBBAAEE),cornerRadius = CornerRadius(10.dp.toPx()))}.padding(4.dp)
)

而这里的 Canvas 是来自 DrawScopeDrawScope 属于一个针对 Canvas 接口的高级封装,内部 Canvas 的底层支持还是原生平台的 Canvas ,因为 Compose 有多平台支持,而 Android 平台对应的就是 AndroidCanvas 对象,这里是通过 canvas.nativeCanvas 获取到的,就是 android.graphics.Canvas 对象,也就是传入了一个 Android 原生 Canvas

流程上如下图所示,这里的核心其实就是:将 Compose 里 drawBehind 的 Canvas 传递给「传统 XML View」,这样在绘制时用的就是来自 Compose 体系 drawBehind 的 Canvas 链条

所以这里可以看到,在绘制的时候,采用的其实就是通过 AndroidViewHolder 这个 ViewGroup 作为 Parent 来 “替换” 掉作为 child 的传统 View 的 Canvas ,让 View 的内容通过 Compose 的 Canvas 绘制到它所在的 LayoutNode 上

另外, pointerInteropFilter 也会处理手势事件,用户在当前 LayoutNode 交互的手势,会被发送到 AndroidViewHolder 这个 ViewGroup ,从而触发传统 Androd 控件的点击等效果。

最后,在 navigate 切换的时候, AndroidViewHolder 也会相对应的被 add/remove 。

从这角度看,Compose 的 PlatformView 实现和 Flutter 的 TextureLayer 理念很接近,都是通过“替换” Canvas 和“模拟”布局环境来实现 View 接入,但是,它们又有本质不同,这个不同就体现在 SurfaceView

因为 SurfaceView 是有自己独立的 Surface 和 Canvas ,所以它是无法被 Parent 的 Canvas “替换” ,这也是 Flutter 里 TLHC 的问题,但是在 Compose 里,你会发现 SurfaceViewAndroidView 里可以正常工作:

@Composable
fun ContentExample() {Box() {ComposableSurfaceView(Modifier.size(100.dp))Text("Compose", modifier = Modifier.drawBehind {drawRoundRect(color = Color(0x9000FFFF), cornerRadius = CornerRadius(10.dp.toPx()))}.padding(vertical = 30.dp))}
}@Composable
fun ComposableSurfaceView(modifier: Modifier = Modifier) {AndroidView(factory = { context ->SurfaceView(context).apply {layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)holder.addCallback(MySurfaceCallback())//添加回调}}, modifier = modifier)
}class MySurfaceCallback : SurfaceHolder.Callback {private var _canvas: Canvas? = nulloverride fun surfaceCreated(p0: SurfaceHolder) {_canvas = p0.lockCanvas()_canvas?.drawColor(android.graphics.Color.GRAY)//设置背景颜色_canvas?.drawCircle(100f, 100f, 50f, Paint().apply {color = android.graphics.Color.YELLOW})//绘制一个红色的图像p0.unlockCanvasAndPost(_canvas)}
}

可以看到,上面代码的 SurfaceView 灰色的背景和黄色的圆都被渲染出来,另外 TextCompose 文本也正常带着背景色覆盖显示在 SurfaceView 上:

有没有觉得奇怪,为什么 SurfaceViewCanvas 没有被替换,但是 SurfaceView 的内容和层级却又正常渲染在了 Compose UI 树里?

其实道理很简单,虽然 Compose 和「传统 XML View」 是两套 UI 框架,但是 Compose 的本质还是 Android 里面的 View ,也就是它依旧在 View 体系的范畴内

依赖 Android 的 Surface、Window、SurfaceFlinger 体系去渲染。

我们简单回忆下 SurfaceView 是怎么工作的?

  • Android 里控件基本都是以 View 为基类,所有可见 View 对象都会渲染到一个 Surface ,这个 Surface 来自 SurfaceFlinger ,也就是当前 Window 下。

  • 尽管 SurfaceView 继承自类View,但是它有自己独立的 Surface,是直接提交到 SurfaceFlinger

这也是 SurfaceView 会有自己独立 Canvas 的原因,简单说它是一个可以绘制到 Surface 并直接输出到 SurfaceFlinger 的视图。

一般情况下, SurfaceView 在其 Window 层上始终是一个透明的 Rect,类似于**SurfaceView 在其窗口中打了一个洞**, 并且默认情况下,SurfaceView 的 Z 顺序始终低于其附加的 Window 层,也就是 SurfaceView 的 Surface 是在默认 Surface 的下面。

而最终渲染时,SurfaceFlinger 会将 SurfaceView 的图像层和 Window 的图像层叠加在一起

那么回到 Compose,Compose 的底层还是一个传统的 View ,所以它还是依赖 View 的 Surface 和 SurfaceFlinger,也就是:

Compose 和「传统 View」 共用同一个 Window 和 DecorViewAndroidView 作为一个桥接节点,将「传统 View」 “插入” 到 Compose 的布局树中,虽然 SurfaceView 绘制内容是独立的,但在屏幕上是共享一个 WindowSurfaceFlinger 依然会统一管理窗口合成。

如给上方 SurfaceView 的代码加上 setZOrderOnTop(true),就会看到 Compose 的 Text 看不到了,因为此时的 Z 层面发生了变化:

这就是 Compose 和 Flutter 在 AndroidView 上最大的区别:

Flutter 是完全脱离了渲染体系,但是 Compose 还是在 View 体系内,所以 SurfaceView 不会是问题,甚至官方还推出了 SurfaceView 对应的 Compose 封装 AndroidExternalSurfaceScope

只是说,在 「传统 XML View」 体系中,每个 View 会有一个 RenderNode,而 Compose 中“一般”只有 ComposeView 一个 RenderNode,也就是传说的单页面状态,而 Compose 内部最终就是将自己的 LayoutNode 通过 Composer 组合完成后塞到 RenderNode 里面。

最后

可以看到,在 Android 平台上, Flutter 和 Compose 在最终实现思路很接近,大家都叫 AndroidView理念都是“模拟”环境和“替换” Canvas ,但是在 Android 平台上 Compose 有着原生 View 体系的优势,所以它对 SurfaceView 的支持更友好。

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

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

相关文章

winfrom项目,引用EPPlus.dll实现将DataTable 中的数据保存到Excel文件

最近研究不安装office也可以保存Excel文件,在网上查询资料找到这个方法。 第一步:下载EPPlus.dll文件(自行去网上搜索下载) 第二步:引用到需要用的项目中,如图所示: 第三步:写代码…

暑期实习准备:C语言

1.局部变量和全局变量 局部变量的作用域是在变量所在的局部范围,全局变量的作用域是整个工程;局部变量的生命周期是作用域内,全局变量的生命周期是整个程序的生命周期,当两者命名冲突时,优先使用的是局部变量。 2.C语言…

OGG 19C 集成模式启用DDL复制

接Oracle19C PDB 环境下 OGG 搭建(PDB to PDB)_cdb架构 配置ogg-CSDN博客,给 pdb 环境 ogg 配置 DDL 功能。 一个报错 SYShfdb1> ddl_setup.sqlOracle GoldenGate DDL Replication setup scriptVerifying that current user has privile…

【动态规划】落花人独立,微雨燕双飞 - 8. 01背包问题

本篇博客给大家带来的是01背包问题之动态规划解法技巧. 🐎文章专栏: 动态规划 🚀若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅🚀 要开心要快乐顺便…

FluentCMS:基于 ASP.NET Core 和 Blazor 技术构建的开源CMS内容管理系统

推荐一个基于 ASP.NET Core 和 Blazor 技术构建的、功能完善的开源CMS内容管理系统。 01 项目简介 FluentCMS 是一个基于强大的 ASP.NET Core 和创新的 Blazor 技术构建的现代内容管理系统(CMS)。 FluentCMS 设计为快速、灵活且用户友好,它…

[创业之路-262]:《向流程设计要效率》-2-职能型组织、项目型组织、流程型组织的异同比较

目录 一、职能型组织与流程化组织的比较 1.1、定义与结构 1.2、关注焦点与运作方式 1.3、优势与局限性 1.4、转型与发展 二、职能型组织、项目型组织、流程型组织的比较 2.1、定义与特点 2.2、优势与局限性 2.3、适用场景与选择建议 三、项目型组织、流程型组织的异同…

5G网络下移动机器人的图像和指令传输用于远程操作

论文标题 **英文标题:**Image and Command Transmission Over the 5G Network for Teleoperation of Mobile Robots **中文标题:**5G网络下移动机器人的图像和指令传输用于远程操作 作者信息 Thiago B. Levin,, Joo Miguel Oliveira,, Ricardo B. Sou…

云计算、AI与国产化浪潮下DBA职业之路风云变幻,如何谋破局启新途?

引言 在近日举办的一场「云和恩墨大讲堂」直播栏目中,云和恩墨联合创始人李轶楠、副总经理熊军和欧冶云商数据库首席薛晓刚共同探讨了DBA的现状与未来发展。三位专家从云计算、人工智能、国产化替代等多个角度进行了深入的分析和探讨,为从业者提供了宝贵…

30天开发操作系统 第 17 天 -- 命令行窗口

前言 今天一开始,请大家先回忆一下任务A的情形。在harib13e中,任务A下面的LEVEL中有任务因此FIFO为空时我们可以让任务A进入休眠状态。那么,如果我们并未启动任务B0~ B0~ B2, B2的话,任务A又将会如何呢? 首先&#xf…

R语言学习笔记之开发环境配置

一、概要 整个安装过程及遇到的问题记录 操作步骤备注(包含遇到的问题)1下载安装R语言2下载安装RStudio3离线安装pacman提示需要安装Rtools4安装Rtoolspacman、tidyfst均离线安装完成5加载tidyfst报错 提示需要安装依赖,试错逐步下载并安装…

数据结构 链表2

目录 前言: 一,反转一个链表(迭代) 二,打印一个链表(递归) 三,反转一个链表(递归) 四,双向链表 总结 前言: 我们根据 [文章 链表1] 可以知道链表相比较于数组的优缺点和计算机…

考研408笔记之数据结构(五)——图

数据结构(五)——图 1. 图的基本概念 1.1 图的定义 1.2 有向图和无向图 在有向图中,使用圆括号表示一条边,圆括号里元素位置互换没有影响。 在无向图中,使用尖括号表示一条边,尖括号里元素位置互换则表示…

游戏设备升级怎么选?RTX4070独显,ToDesk云电脑更具性价比

过新年、添喜气!正逢节期来临不知道各位是否都跟小编一样在考虑购置生活中的各样所需呐? 25年可谓是3A游戏大作之年,例如《GTA6》《文明7》《死亡搁浅2》《刺客信条:影》下半年落地的《塞尔达传说:新篇章》《生化危机9…

C语言初阶牛客网刷题——HJ73 计算日期到天数转换【难度:简单】

1. 题目描述——HJ73 计算日期到天数转换 牛客网OJ题链接 描述 每一年中都有 12 个月份。其中,1,3,5,7,8,10,12 月每个月有 31 天; 4,6,9,11 月每个月有 30 天;而对于 2 月,闰年时有29 天,平年时有 28 天。 现在&am…

【深度学习基础】多层感知机 | 权重衰减

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈PyTorch深度学习 ⌋ ⌋ ⌋ 深度学习 (DL, Deep Learning) 特指基于深层神经网络模型和方法的机器学习。它是在统计机器学习、人工神经网络等算法模型基础上,结合当代大数据和大算力的发展而发展出来的。深度学习最重…

实现酷炫粒子背景效果

使用 particles.vue3 实现酷炫粒子背景效果 在这篇博客中,我们将介绍如何使用 particles.vue3 实现动态粒子背景,并详细讲解其配置参数和常见问题的解决方法。通过本文,你可以轻松在项目中应用并自定义粒子效果。 什么是 particles.vue3&am…

ubuntu16.04 VSCode下cmake+clang+lldb调试c++

VSCode下cmakeclanglldb调试c Ubuntu16.04 安装OpenCV4.5.4 文章目录 VSCode下cmakeclanglldb调试c1.安装clangclangdcmake2、打开VSCode,安装扩展插件3、编译4、Debug4.1 创建launch.json。4.2 配置setting.json 5. vscode安装配置clang-format插件5.1 Linux系统安…

在vue3中使用datav完整引入时卡在加载页面的解决方法

文件修改 文件:node_modules/dataview\datav-vue3/package.json // "module": "./es/index.js","module": "./es/index.mjs", // 修改后使用完整引入,需要为datav配置文件添加相应方法 文件:node…

AI agent 在 6G 网络应用,无人机群控场景

AI agent 在 6G 网络应用,无人机群控场景 随着 6G 时代的临近,融合人工智能成为关键趋势。借鉴 IT 行业 AI Agent 应用范式,提出 6G AI Agent 技术框架,包含多模型融合、定制化 Agent 和插件式环境交互理念,构建了涵盖四层结构的框架。通过各层协同实现自主环境感知等能力…

跨境电商SEO起步:关键词研究方法

SEO的重要性和必要性不言而喻,而在SEO的各大流程中,关键词研究同样重要,因为它在网站内容优化、产品标题和描述优化等方面都发挥重要作用。 一、从消费者视角出发 SEO是为了增加让消费者看到自己产品的可能性,因此要从消费者搜索…