HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

死磕四年,投资数亿,uni-app做出了比原生渲染还快的跨平台框架

性能 uni-app x

> 原文是首发于开源中国公众号的访谈,各级标题为主持人问题,原文详见:​https://mp.weixin.qq.com/s/erfEFl4ZNKMkoa7MViFCpA

背景:

uni-app x 蒸汽模式,发表了比原生渲染还快的benchmark。这让业内大为震惊。

本期请来了DCloud的CEO王安,从设计者的角度聊聊,为什么做这个产品、为什么这么做而不那么做、性能提升的关键在哪里?

1. 跨平台框架赛道,并非当下VC的关注领域,DCloud投入4年,花了几个亿搞 uni-app x,这种执着和冒险的底层逻辑是什么?

我做跨平台已经二十多年了,老骨灰了。我做的产品,虽然也有很大的市场体量,但也一直被开发者们吐槽。

uni-app的第一代产品,在小程序领域是市占率第一的,不管大厂小厂都在用。因为用uni-app开发小程序不会比原生小程序性能差,还能跨平台。

但在App平台,市占率只有百分之十几,主要集中在中小开发者。原因就是App平台的性能不能满足中大开发者的需求。

所以App平台的性能问题,对我们一直是如鲠在喉。

2022年,我们内部讨论 uni-app x 的立项问题,能不能让app像小程序那样,做到与原生开发相比没有性能损失。

其实团队内部,对 uni-app x 的立项是有反对声音的。如果我们守住原有盘子,做好服务和商业化,能让团队成员的日子过的更好点。

但一旦要立项解决跨平台框架的性能问题,这注定不是一个短期能解决的问题,投入肯定不少,机会风险很大。

当时觉得就是不服气,觉得这辈子不能就一直解决不了跨平台的性能问题,活得太憋屈了,想赌一把。

其实当时我也不知道这个项目一做就是4年,我以为2年能搞定。当初如果知道要4年,那承受的压力会非常大,不一定能下得了决心。

2. 这4年中间,有过犹豫和后悔吗?

中间是走了很多弯路啊。有的路走了很久才发现不通,肯定是非常郁闷的。
但并不会因为走错路而后悔。你在登一个没人登过的山,走错路是正常的。
要说后悔的话,从开始就应该建立一个更高效的探路机制,能更快的验证路线的正确性。
这个机制,我们直到2025年才完善,这是我最大的教训吧。

中间有段时间非常焦虑,大约25年初的时候,当时觉得搞了快3年了,不达预期怎么办。
幸好几个月后,25年夏天,一系列技术突破后,终于重拾信心,知道自己搞对了。

3. 你们建的这个高效的探路机制,是什么?

类似实验室。
就是别着急做产品,别着急提代码。
就是先做实验,体系化的、严谨的做实验。
但是然后呢,你又会发现还不能着急做实验,为了能严谨的输出各种实验报告,其实需要做很多基建,你要先投入大量资源去建实验室的基建。

我们以前吃了不少亏,调研报告不准确,误导了技术决策。这都需要一套良好的实验基建、严谨的实验规范流程才能解决。

有很长一段时间,我看不到产品里提交代码,我也不关注这个指标。我每天只看实验报告。

这个过程可能很多公司都没有,大多数公司的技术考核,可能是产品需求的实现工期、完成度、bug数量。
但对于uni-app x蒸汽模式这种产品来讲,实验室机制是它最终能做出来的核心原因之一。

4. 非常延迟满足啊。不做产品先做实验,不做实验先搞批量实验的基建。那这注定不是短期投入。然后我有点好奇,当初是怎么说服团队成员同意这个指标的:比原生渲染还快。

没有。当初如果定一个比原生渲染快2~3倍的目标,别说团队成员了,我自己都不信。

当初的目标,就是参考小程序,uni和原生小程序没有性能差别。然后app也做到和原生app一样,没有性能差别就行。

就这,当时团队成员都不同意,说做不到,最多能做到15%的性能差距,就是比原生慢15%。为了这15%拉扯了半天。

但后来发现,实现到15%的性能差距,仍然困难重重。只能一路往底层追,从系统组件源码、追到系统底层渲染原理,往下又追到编程语言原理,又往下追到硬件层操作原理。

直到你把整个事情都搞明白了,这时候你才发现,传统的原生渲染一层一层套轮子,叠了太多层了,性能优化空间太大了。
不要说达到原生渲染的性能了,比原生渲染快几倍都没问题。

以前如果我diss一个团队成员,说你做的这个东西性能太差了,怎么连原生的同类组件的性能都拼不过。这个人肯定会觉得这个老板有病。
但现在我可以随便diss,在我们团队,做到比原生快并不难,已经是团队共识了。
当然一个新入职的人,还需要一段时间才能适应这种文化。

5. 原生渲染那么多年的优化,真的是流程很长吗?能举个例子吗?

新的挑战者打破旧的,心里是需要这个世界都是草台班子的信念的。虽然这个新的挑战者也会被后来的挑战者视为草台班子。

计算机工业发展了这么多年,长成了庞然大物,轮子一层叠一层,减肥的空间其实挺多的。

我们公司有个面试题,手机左上角的那个返回箭头,怎么绘制是性能最高的。

很多人在项目中使用字体图标,矢量缩放不失真,暗黑能变色,跨平台都能用,不同平台渲染结果一致,听着挺好。

但是一个字体图标,从你写代码加载字体那刻,到屏幕上最终显示出来,经过了非常多环节。

你可能觉得你就写了几行代码,还好吧。但实际上,

  • 首先系统需要从IO里加载你的字体库到内存里,
  • 然后需要构造字体管理对象,解析字体头文件,确定字体的很多参数。
  • 然后要去字体中的众多字里匹配,你想要显示哪个字符。
  • 然后要做文字塑型,确定连字等字间距处理。
  • 然后做换行、双向文本流(有从左到右的文字、也有从右到左的,还有混合的)。
  • 然后做文字测量,把高宽定下来。
  • 然后做光栅化或三角形化。
  • 然后提交给GPU。
  • 最后整体合成上屏。

这里面提到的每个步骤,都有专业的库负责。这些库的源码如果你看过的话,逻辑都非常多。

在Android和鸿蒙上,这些逻辑还涉及原生的kt/arkts和c库的反复交互。

你只看到你写的几行代码,但实际上背后是几十年的文字系统各种库的协作配合。

你知道了背后的流程多长,你肯定想跳过一些流程,比如就一个返回箭头图标,它理应不涉及换行和双向文本流,但这里又有另一个坑。
不同os,上述流程的可控性是不一样的,有的os提供了专门的api可以跳过换行和双向文本流流程,有的os没有提供。

如果你对文字系统有研究兴趣,可以去看看harfbuzz、freetype、icu这些c库的源码,都是几十年的老库了,每个库都是不同的顶级大牛写的。
但作为一个应用开发者,想优化这块,真没有ROI。

5.1. 所以uni蒸汽模式中的返回箭头、文字渲染,都比原生自带的要更快?

是的。首先要投入大量精力研究每个系统的底层,然后优化,然后再给开发者暴露简单易用的跨平台写法。

6. 你们提供了一个benchmark,一个界面中创建了4050个view和text,渲染耗时比原生短2~3倍。这个测试例让很多人不解、甚至不信。不知道这个访谈中,王老师能否透露一些细节原因。比如这个测试例,是因为uni蒸汽的文字系统更快吗?有些人觉得这可能是你们的测试例定向优化,能解释下吗?[^1]

没有定向优化。是反过来的,如何测试一个渲染系统在处理view、text这种基础组件时速度更快?带着这个问题,我们才在一个屏幕内大量创建view和text来做实验。

你想创建多少个、怎么排布,都可以,文字是数字还是汉字都可以,结论都是 uni-app x 蒸汽模式更快。

这种快,来源于很多方面。uni的文字系统真的更快,如果你把4050的例子改一下,在里面放置更多text组件,你会发现uni比原生快了不止2~3倍,会快更多,界面上文字越多,uni的优势越明显。

除了文字系统快,整个template和Style区域,执行的都比原生快。在4050的例子里,是模板里的v-for创建这些view和text,这些代码都是编译成字节码/机器码的,这些高度优化的二进制码性能更高。

uni蒸汽的排版系统也更快,不管是什么组件,view、text还是其他组件,都要排版。uni蒸汽统一是flex排版,这套排版系统的性能也比原生提供的那些排版布局系统快。并且这套排版也不会为4050这种示例做定向优化。

4050这个例子有拍平和非拍平2套测试数据,都是uni蒸汽快。有些人认为uni蒸汽的4050快是拍平的功劳。并不是的,这2套数据之差,已经说明了拍平在这里能提供的价值。

当然拍平也是我们给开发者提供的一个挺好的性能优化方案,让开发者可以方便构造更高性能的UI。一个简单的flatten属性,这种开发方式比原生高效非常多。

7. 有的原生工程师表示不服,他们也可以在原生的基础上优化代码,跑出更快的原生版数据。你怎么看?

我们需要牢记初心,这个测试例比的是view和text的创建速度,不是比4千多个方格子固定排放后如何快速渲染。

uni提供的就是标准的view和text组件,没有定向优化。原生的对比测试例,也不能为了这个4050的测试例而定向优化。

有的原生工程师说,我用draw的方式,只创建1个view,在上面自绘出4050个方格子出来,不测量不排版,所有格子等宽。
那这就是原生为这个测试例定向优化了。这就没法比较原生的view、text,和uni的view、text到底谁快了、排版系统到底谁快了。

如果你基于draw,自己实现文字测量、布局排版,能布任何界面,那其实就是自己做了一套渲染系统。其实这就是 Compose UI 在Android上做的事情。
首先真的做出来这套系统是非常难的,其次做出来性能也很难匹敌uni蒸汽。

Compose UI 的view和text同屏4050测试例,我们是在benchmark里公开过的,比Android原生view还慢,更比不过uni蒸汽。[^1]

如果某个原生工程师有兴趣钻研此道,自己一个人搞是不行的,这是大工程,还是加入DCloud一起干吧,哈哈。

8. 我有个疑惑,为什么操作系统那么多专业的人,不去把原生UI优化的更快呢?给三方这么大的超越空间

我不是操作系统团队的人,我也不知道他们怎么想的。我可以推测下,随便聊了。

可能有的系统厂商,觉得自己已经够快了,没有竞争压力给他们。因为要动这些底层的东西,代价不低,立项时总会有人问花这么多钱能带来什么财务回报?

很多领先的厂商,有一个产业义务,就是推进用户买新设备。这对用户来说听着有点恶心。但产业链的兄弟们持续有钱赚也很重要,大家有钱可以持续创新,推进产业进步。

然后就是组织大了,跨部门协调也难。
比如Android,原生的kotlin是一拨人,各种c库是另一拨人,现有的框子是一层一层叠起来的,想压穿,需要协调非常多人。

还有的技术栈喜欢自举,比如java/kotlin,很多重要的库是java写的、而不是c或汇编写的。当你的产品哲学是自举优先而不是性能优先,就很难把性能做到极致。
你看v8的源码,那不止是优秀的c代码,很多地方要上汇编的,比如json解析,这就是性能优先。

当然了,如果操作系统的人真的愿意好好优化,肯定是可以反超uni蒸汽现在的benchmark数据的。

不过呢,其实uni蒸汽,也还可以再提升性能,只不过我们也一样,觉得快了2~3倍已经足以。现在立项投很多人力财力再让uni蒸汽快4倍5倍,没有性价比。
如果原生优化的更快了,我们也会立项再追。

还是那句话,现代计算机工业这么多年,这个庞然大物减肥的空间挺多的。

9. AI时代,跨平台框架的价值是否要重新看待了。最近shopify放弃react native,转原生。这是不是未来的趋势?你们搞了4年多,这个跨平台框架是不是在AI时代生不逢时了?

AI刚来时,我也有点焦虑,担心AI会颠覆掉跨平台框架。
但后来实践、观察、深入思考,心就定了。

首先看数据,AI Coding后,uni的应用数量进一步增长,并不是下降。
然后看我们内部的实践,我们除了框架,也做应用,比如uni-ai、uni-im,一个是开源的AI聊天客户端,一个是开源的IM系统,我们和我们的开发者沟通,都是在uni-im上。

如果没有 uni-app x 和uniCloud,这些项目在我们内部根本无法立项。是因为实际这2个项目,只有一个前端工程师。

如果要用原生来做,2个应用系统,5个客户端平台,Android、iOS、鸿蒙、web、微信小程序,客户端+服务器+测试,即使每个人都在用ai,最少也是十几个人。

除了开发,还要考虑。不同平台,不但代码不同,测试例、测试工具链也不同。但在uni下,不止代码统一了、测试例也是一套。

uni这边,真的从前到后,从开发到测试,所有平台,只有一个人。他用着AI并行开发,快速完成了2个系统。这生产效率和成本,其他方案真的比不了。

主持人:有的人说,我不要6个人,我一个人就让ai干活,写5个平台的原生代码加服务器和测试例,不行吗?

AI只是工具,它需要人用,需要人背锅。工具不背锅。进度延期、线上出bug,你无法甩锅给AI。只有懂AI写的代码,才能hold住,才能担起责任。

尤其是面向广大用户的、涉及重要权益的系统,AI写的代码有问题,造成线上损失,是需要你担责的。

做一些压力不大的系统,比如自用的、有限人使用的内部管理只读报表,可以不看代码。但真要上线,要面向用户承担责任,那人得把关。

回到上一个问题,我们做的uni-ai,它的性能比原生开发的AI客户端都好,和ChatGPT、豆包、deepseek相比帧率都更稳。

如果使用原生开发,每个平台都达到这个性能水平,那我刚才说的十几个人都还不够,那投资得非常大了。

所以uni系产品带来的价值,生产力,非常大。

AI虽然是个工具,但你让它干活时,它也想用好工具,它不会用好工具它就不是个好AI,对吧。
在AI时代,写代码并不是不需要框架,只是需要好框架。
比如ts、vue,这些都是框架,你也可以让AI去写原始的js,但人们为什么不这么做呢?
因为明显是用ts、vue这些好框架,写代码更轻松、更容易保障性能和质量,也更省时间和token。

在AI时代,并不是跨平台框架没有价值了。而是需要更好的跨平台框架。

一个跨平台框架,只跨2个平台,性能比原生差了不少,那么shopify认为AI降低了我的成本,原生带来了更高性能,这ROI是合适的,就会转原生。
假使,我说的是假使,react native的渲染性能比原生更快,那shopify怎么会切原生去?闲的慌啊。

所以AI时代,那些问题比较多的跨平台框架,不管是性能、质量、跨平台数量,如果做的不好,就会没落。
我个人觉得,AI Coding时代,在App平台,可能未来只有原生和uni-app x。其他跨平台框架估计要走下坡路。
这会和小程序领域类似,拼到最后只剩原生和uni-app了。

我这话说的,听起来有点满,其实我是深入思考过的。
新的编程框架规范,对AI非常不友好,很难冒出来。
老的这几家,比如flutter、react native、CMP,如果能解决做到即跨平台、又不比原生性能差,那自然也不可能被AI淘汰。

但flutter的自渲染,包括CMP在iOS上也是自渲染,这种在混合原生渲染时有硬伤。这个在另一篇文章专门讨论过自渲染和原生渲染的问题。[^2]

而react native,从meta挪到基金会后,这个机制有好有坏,不好的就是很难找到一个强力的人做大改,但如果不大改,它解决不了比原生性能差的问题。
shopify本来是基金会的重要赞助者,出力做了不少关键库,更是给基金会出钱。它的撤出,对react native的打击很大。

当然难归难,也不是绝对的,不知道谁家会天降猛人改变局面。

说回来,我认为,AI会淘汰一些简易、低质量的轮子。但复杂、高质量的轮子,永远都受欢迎。AI想要好轮子。

10. uni-app x 刚出来的时候推的是uts语言,现在蒸汽模式又改成了普通的js/ts。为什么会这样调整?怎么考量的?

uni-app x 立项的时候,是参考我们在小程序上的做法,小程序就是把uni-app的写法,编译成了小程序的原生写法,重编译轻运行时。

那么怎么把uni-app x也编译成各个平台的原生应用?比如在Android上,把uni的写法就编译成一个纯的kotlin应用。

这就需要先做一个基建,跨平台的编译型语言,这就是uts,一个语言可以编译成kotlin、Swift、ArkTS。
一个语言,做出来难,做好更难。

但是语言搞定后,我们发现渲染系统做的不好,拖累了整体表现,又开始研究渲染系统。
语言是一个大关,渲染系统又是一个大关,我们连续磕了2个大难关。

当初立项新版渲染系统时,搭配的语言还是uts。因为当时没有预期蒸汽渲染系统这么快。
新版渲染系统出来后,结果是大幅超过我们预期的。我也没想到这块的优化余地这么大,在这么多测试场景中,都能比原生能快2~3倍。[^1]

然后这个时候AI来了,uts语言作为一个新语言,国外模型不熟悉,也没有模型为uts单独做强化学习,给我们带来了巨大的压力。
其实AI在某种程度上,是不利于创新的。不止uts,其他新的编程语言也一样难。

这逼着我们思考,既然渲染系统能快这么多,那么把语言改回到普通ts/js,合体版本是不是也可以做到比原生快?是不是这样的产品才是AI时代更适合开发者的。
这个时候我们团队内部是转不过来弯的,辛辛苦苦搞了几年uts,投入大量心血,真的要尝试换回js吗?自己的孩子,怎么能不心疼?
我自己也给自己做了很多心理建设。

最后是分期立项,优化跨语言通信。先在鸿蒙和iOS上,确认js+蒸汽渲染仍然可以做到比原生渲染快,然后才全体坚定信心,大力推进Android的js更换。
外部看不到,内部其实经历了一个过渡期才把弯转过来。

优化跨语言通信,这个立项最初目标是js+蒸汽渲染系统,能和原生一样快就行。
最终这个项目的结果,也超过了当初立项的预期。
也得益于这几年对语言底层的持续研究,搞明白很多原理,优化后,跨语言通信只损失了百分之几,但蒸汽渲染系统比原生渲染快200%~300%,
最后整合的效果,基本没变,那百分之几在各种评测实验数据上几乎看不出来,还是比原生快2~3倍。[^1]

跨语言通信有个极限例子,就是canvas,在js中绘制碰撞的小球,绘制逻辑都是在js里。最终这个测试例大家也看到benchmark里的数据了,uni蒸汽下,可以绘制几十万个小球同屏碰撞而不掉帧。

最后js+蒸汽渲染引擎这个方案就定下来了,AI熟悉、开发者门槛低、还能热更新,还比原生渲染快,这不就是最佳方案了吗?夫复何求,对吧。

有最佳方案就行,你过去投入的心血,不要遮蔽住你为用户寻找最佳方案的眼睛。

11. 那uts以后怎么办呢?不管是uts语言,还是你们的HBuilder,都是国产自主创新的代表,最近你们基于vscode搞了HBuilderV,感觉这些国产创新的东西好不容易做出来,要被丢掉吗?

国产化,除非是政府资助,对于一个市场化的产品来讲,还是需要在市场中让用户买单。

我们也会觉得可惜,但以国产创新的理由做技术自嗨,得不到用户的认可,没用。
反正我们自己从来不说:flutter、react native不是国产的,请大家支持国产的uni-app。

uts语言不会消失,我们内部也有大量代码是基于uts的,而且我们内部的uts代码还在增加,它仍然是一个不错的跨平台语言。
会的人可以继续用,不会的人,不学也行,原生插件开发也可以使用原生混编。

其实我们还研究过uts直接编译成机器码,发现可行。只是,做出来容易,但想做好,又得几年、好几亿的投入。
除非是政府主导,一定要搞国产化语言,对于市场主体来讲,这事没有性价比。

编译机器码不难,但编译出的机器码非常高效,这特别难。
我们不能假设只要编译成机器码,性能就一定高。
很多语言都尝试过这事,kotlin也有k/n方案,但同样是机器码,它还得处理gc,它的机器码优化程度,和c语言差太多了。

我们也可以很快拿出一个uts编译机器码的方案,但性能想达到第一梯队,就是达到c、zig的水平,需要很长时间和很多资金投入。

至于HBuilderV,这个产品得感谢AI时代,没有AI迁移,这个产品很难立项。
我们的逻辑是这样,给习惯vscode的开发者提供一种选择,有助于大家更接受uni蒸汽,且在AI的帮助下这个产品的成本可接受,那就做了。

目前HBuilderX和HBuilderV会一起演进。

至于未来,HBuilderX是基于QT的,QT以后不会再用了。
其实我们掌握了优秀的渲染优化技术,在PC上也是一样的,后续我们会出uni蒸汽的pc版,届时也会是最快的跨PC平台渲染引擎,体积也会很小。
在uni蒸汽的pc版出来后,我们会再重构HBuilder。
当然也可能换名字。未来AI持续发展,IDE的传统功能都不一定适用了。可能会是一个全新形态。

11.1 那很期待。至于HBuilderV,你们为什么不做vscode插件,而是和cursor一样整体套壳修改?

大家可能对vscode的插件api的丰富度有过高预期。
当初cursor之所以基于vscode套壳二开,就是因为vscode的插件api不够用,AI所需要的功能,插件无法实现,只能整体二开。

对于HBuilderV也一样,HBuilderX里很简单的一个常用需求,就是同时运行多个平台,在底部开一堆控制台,这功能插件就做不了。
还有各处弹出的界面,比如运行、打包,以及我们也有一个AI产品,叫uni-agent,大量的功能需求,用vscode插件无法实现。只能整体二开。

12. 你刚才提到了c、zig,没有提到rust,现在rust风生水起,bun也从zig迁移到rust了。你们的组件不用rust,还使用c语言吗?

rust在pc上发展的更顺利些,手机上用的要少一些。手机系统的c api、ndk编译链,都是围绕c、c++构建的。

不管是c、zig、rust,核心差别是内存谁来管。
其实放大看,所有语言都有这个终极问题,内存到底怎么管。用这点,可以把所有编程语言放在一个坐标体系里看待。

最开始的c语言,典型的手动管理内存。
但人们觉得,我就是开发业务,你让我研究内存怎么管,还经常泄漏,这门槛太高了,不利于编程普及。
然后就出现了gc,就是垃圾回收,java、js,这些语言凭借一点,你别再操心内存了,专注你的业务就完事,帮助这个世界产生了更多的程序员。
然而gc从来都是有代价的,会影响性能。于是有人又想出点子,能不能让编译器来管内存,运行时不要gc了,然后就设计了一套复杂的借用机制,在编译阶段确保内存安全,这就是rust。
但这套机制,学习门槛很高,为了高性能有时还得用unsafe。
于是又有人想,那能不能还是人手动管理内存,但管理的方便点。zig就出现了。
当然zig不止这一个优势,但通过这个对比,可以简单的明白这些编程语言的定位差异。

就我个人喜好,其实我喜欢zig。当然zig的成熟度、生态确实不如rust。
bun的压力来自于基于它的claude code,用户量巨大,带来了很多边界问题。
但bun的问题,并非zig下不能解。
只不过在zig下,需要投很多人来解。但对于一个AI模型公司来讲,能让AI解更符合他们的飞轮。

当然站在第三方来看,折腾这事干嘛,想求稳,应该直接把bun换成nodejs,而不是把bun从zig改成rust。

13. 你也提到一个成熟的产品,需要处理掉各种边界问题,才能获得开发者的认可。那么uni蒸汽刚出来,是否短期内也面临成熟度问题?

这是肯定的。所有新产品都有成熟过程。对于uni蒸汽来讲,就要想各种办法加速它的成熟。
我可以分享下我们怎么做的。

uni-app x 从2023年起第一个版本就开发了自动化测试系统。
并且 uni-app x 坚持一个原则,修复的每个开发者报的issues,都要配套自动化测试例。每个版本发布,都要过测所有测试例才能发版。
我们专门做了一套issues系统,开发人员在提测issues的时候,表单里就有自动化测试例地址。

这几年来,已经积累几万个自动化测试例,包括功能、性能、UI截图对比、内存泄漏、崩溃等多种维度。
每天晚上,在我们内部的持续集成系统里,都要跑几万个测试例,在各种手机os、各种版本上跑,几十万次,做断言比对、截图对比。。。

uni-app x 蒸汽模式 虽然是2026年推出的,但它每次发版,都得通过 uni-app x 积累的所有自动化测试例。

uni蒸汽的质量管理压力,其实对我们来讲,轻了很多。
以前在vdom模式时,我们还要操心uts语言的质量,现在语言的稳定性交给js引擎处理了。
我们就可以集中精力在渲染系统的质量上。
uni蒸汽还有一个特点:不同平台的组件内部实现代码基本都是一套,都是跨平台的c或uts。所以它的平台差异很小。这也让我们的质量管理压力变小。

精力不分散后,我们也有更多人力投在响应开发者反馈的bug上,现在uni蒸汽的修bug速度还是很快的。

在AI时代,AI其实也在帮助我们把控质量,分析隐患、生成更多自动化测试例。这些都让一个新产品的成熟度比以往更快。

我不能说uni蒸汽已经成熟了,但我认为它的质量是可商用的。

好的,感谢王总的坦诚分享。与王总交流,总能感受到一些新的观点和角度。相信读者们,除了更深度的了解uni相关产品外,还可以从这个访谈中学到一些其他东西,比如一个复杂项目如何立项、如何管理、如何提升性能和质量。更多问题,大家可以留言,我们尽量协调王总回答哈。

没问题。

关联阅读

​1. uni-app x 蒸汽模式 性能测试基准报告 Benchmark【鸿蒙版】
​2. uni-app x 蒸汽模式性能测试基准报告 Benchmark【 iOS版】
​3. uni-app x 蒸汽模式 性能测试基准报告 Benchmark【Android版】

  1. 一文讲透原生渲染和自渲染
继续阅读 »

> 原文是首发于开源中国公众号的访谈,各级标题为主持人问题,原文详见:​https://mp.weixin.qq.com/s/erfEFl4ZNKMkoa7MViFCpA

背景:

uni-app x 蒸汽模式,发表了比原生渲染还快的benchmark。这让业内大为震惊。

本期请来了DCloud的CEO王安,从设计者的角度聊聊,为什么做这个产品、为什么这么做而不那么做、性能提升的关键在哪里?

1. 跨平台框架赛道,并非当下VC的关注领域,DCloud投入4年,花了几个亿搞 uni-app x,这种执着和冒险的底层逻辑是什么?

我做跨平台已经二十多年了,老骨灰了。我做的产品,虽然也有很大的市场体量,但也一直被开发者们吐槽。

uni-app的第一代产品,在小程序领域是市占率第一的,不管大厂小厂都在用。因为用uni-app开发小程序不会比原生小程序性能差,还能跨平台。

但在App平台,市占率只有百分之十几,主要集中在中小开发者。原因就是App平台的性能不能满足中大开发者的需求。

所以App平台的性能问题,对我们一直是如鲠在喉。

2022年,我们内部讨论 uni-app x 的立项问题,能不能让app像小程序那样,做到与原生开发相比没有性能损失。

其实团队内部,对 uni-app x 的立项是有反对声音的。如果我们守住原有盘子,做好服务和商业化,能让团队成员的日子过的更好点。

但一旦要立项解决跨平台框架的性能问题,这注定不是一个短期能解决的问题,投入肯定不少,机会风险很大。

当时觉得就是不服气,觉得这辈子不能就一直解决不了跨平台的性能问题,活得太憋屈了,想赌一把。

其实当时我也不知道这个项目一做就是4年,我以为2年能搞定。当初如果知道要4年,那承受的压力会非常大,不一定能下得了决心。

2. 这4年中间,有过犹豫和后悔吗?

中间是走了很多弯路啊。有的路走了很久才发现不通,肯定是非常郁闷的。
但并不会因为走错路而后悔。你在登一个没人登过的山,走错路是正常的。
要说后悔的话,从开始就应该建立一个更高效的探路机制,能更快的验证路线的正确性。
这个机制,我们直到2025年才完善,这是我最大的教训吧。

中间有段时间非常焦虑,大约25年初的时候,当时觉得搞了快3年了,不达预期怎么办。
幸好几个月后,25年夏天,一系列技术突破后,终于重拾信心,知道自己搞对了。

3. 你们建的这个高效的探路机制,是什么?

类似实验室。
就是别着急做产品,别着急提代码。
就是先做实验,体系化的、严谨的做实验。
但是然后呢,你又会发现还不能着急做实验,为了能严谨的输出各种实验报告,其实需要做很多基建,你要先投入大量资源去建实验室的基建。

我们以前吃了不少亏,调研报告不准确,误导了技术决策。这都需要一套良好的实验基建、严谨的实验规范流程才能解决。

有很长一段时间,我看不到产品里提交代码,我也不关注这个指标。我每天只看实验报告。

这个过程可能很多公司都没有,大多数公司的技术考核,可能是产品需求的实现工期、完成度、bug数量。
但对于uni-app x蒸汽模式这种产品来讲,实验室机制是它最终能做出来的核心原因之一。

4. 非常延迟满足啊。不做产品先做实验,不做实验先搞批量实验的基建。那这注定不是短期投入。然后我有点好奇,当初是怎么说服团队成员同意这个指标的:比原生渲染还快。

没有。当初如果定一个比原生渲染快2~3倍的目标,别说团队成员了,我自己都不信。

当初的目标,就是参考小程序,uni和原生小程序没有性能差别。然后app也做到和原生app一样,没有性能差别就行。

就这,当时团队成员都不同意,说做不到,最多能做到15%的性能差距,就是比原生慢15%。为了这15%拉扯了半天。

但后来发现,实现到15%的性能差距,仍然困难重重。只能一路往底层追,从系统组件源码、追到系统底层渲染原理,往下又追到编程语言原理,又往下追到硬件层操作原理。

直到你把整个事情都搞明白了,这时候你才发现,传统的原生渲染一层一层套轮子,叠了太多层了,性能优化空间太大了。
不要说达到原生渲染的性能了,比原生渲染快几倍都没问题。

以前如果我diss一个团队成员,说你做的这个东西性能太差了,怎么连原生的同类组件的性能都拼不过。这个人肯定会觉得这个老板有病。
但现在我可以随便diss,在我们团队,做到比原生快并不难,已经是团队共识了。
当然一个新入职的人,还需要一段时间才能适应这种文化。

5. 原生渲染那么多年的优化,真的是流程很长吗?能举个例子吗?

新的挑战者打破旧的,心里是需要这个世界都是草台班子的信念的。虽然这个新的挑战者也会被后来的挑战者视为草台班子。

计算机工业发展了这么多年,长成了庞然大物,轮子一层叠一层,减肥的空间其实挺多的。

我们公司有个面试题,手机左上角的那个返回箭头,怎么绘制是性能最高的。

很多人在项目中使用字体图标,矢量缩放不失真,暗黑能变色,跨平台都能用,不同平台渲染结果一致,听着挺好。

但是一个字体图标,从你写代码加载字体那刻,到屏幕上最终显示出来,经过了非常多环节。

你可能觉得你就写了几行代码,还好吧。但实际上,

  • 首先系统需要从IO里加载你的字体库到内存里,
  • 然后需要构造字体管理对象,解析字体头文件,确定字体的很多参数。
  • 然后要去字体中的众多字里匹配,你想要显示哪个字符。
  • 然后要做文字塑型,确定连字等字间距处理。
  • 然后做换行、双向文本流(有从左到右的文字、也有从右到左的,还有混合的)。
  • 然后做文字测量,把高宽定下来。
  • 然后做光栅化或三角形化。
  • 然后提交给GPU。
  • 最后整体合成上屏。

这里面提到的每个步骤,都有专业的库负责。这些库的源码如果你看过的话,逻辑都非常多。

在Android和鸿蒙上,这些逻辑还涉及原生的kt/arkts和c库的反复交互。

你只看到你写的几行代码,但实际上背后是几十年的文字系统各种库的协作配合。

你知道了背后的流程多长,你肯定想跳过一些流程,比如就一个返回箭头图标,它理应不涉及换行和双向文本流,但这里又有另一个坑。
不同os,上述流程的可控性是不一样的,有的os提供了专门的api可以跳过换行和双向文本流流程,有的os没有提供。

如果你对文字系统有研究兴趣,可以去看看harfbuzz、freetype、icu这些c库的源码,都是几十年的老库了,每个库都是不同的顶级大牛写的。
但作为一个应用开发者,想优化这块,真没有ROI。

5.1. 所以uni蒸汽模式中的返回箭头、文字渲染,都比原生自带的要更快?

是的。首先要投入大量精力研究每个系统的底层,然后优化,然后再给开发者暴露简单易用的跨平台写法。

6. 你们提供了一个benchmark,一个界面中创建了4050个view和text,渲染耗时比原生短2~3倍。这个测试例让很多人不解、甚至不信。不知道这个访谈中,王老师能否透露一些细节原因。比如这个测试例,是因为uni蒸汽的文字系统更快吗?有些人觉得这可能是你们的测试例定向优化,能解释下吗?[^1]

没有定向优化。是反过来的,如何测试一个渲染系统在处理view、text这种基础组件时速度更快?带着这个问题,我们才在一个屏幕内大量创建view和text来做实验。

你想创建多少个、怎么排布,都可以,文字是数字还是汉字都可以,结论都是 uni-app x 蒸汽模式更快。

这种快,来源于很多方面。uni的文字系统真的更快,如果你把4050的例子改一下,在里面放置更多text组件,你会发现uni比原生快了不止2~3倍,会快更多,界面上文字越多,uni的优势越明显。

除了文字系统快,整个template和Style区域,执行的都比原生快。在4050的例子里,是模板里的v-for创建这些view和text,这些代码都是编译成字节码/机器码的,这些高度优化的二进制码性能更高。

uni蒸汽的排版系统也更快,不管是什么组件,view、text还是其他组件,都要排版。uni蒸汽统一是flex排版,这套排版系统的性能也比原生提供的那些排版布局系统快。并且这套排版也不会为4050这种示例做定向优化。

4050这个例子有拍平和非拍平2套测试数据,都是uni蒸汽快。有些人认为uni蒸汽的4050快是拍平的功劳。并不是的,这2套数据之差,已经说明了拍平在这里能提供的价值。

当然拍平也是我们给开发者提供的一个挺好的性能优化方案,让开发者可以方便构造更高性能的UI。一个简单的flatten属性,这种开发方式比原生高效非常多。

7. 有的原生工程师表示不服,他们也可以在原生的基础上优化代码,跑出更快的原生版数据。你怎么看?

我们需要牢记初心,这个测试例比的是view和text的创建速度,不是比4千多个方格子固定排放后如何快速渲染。

uni提供的就是标准的view和text组件,没有定向优化。原生的对比测试例,也不能为了这个4050的测试例而定向优化。

有的原生工程师说,我用draw的方式,只创建1个view,在上面自绘出4050个方格子出来,不测量不排版,所有格子等宽。
那这就是原生为这个测试例定向优化了。这就没法比较原生的view、text,和uni的view、text到底谁快了、排版系统到底谁快了。

如果你基于draw,自己实现文字测量、布局排版,能布任何界面,那其实就是自己做了一套渲染系统。其实这就是 Compose UI 在Android上做的事情。
首先真的做出来这套系统是非常难的,其次做出来性能也很难匹敌uni蒸汽。

Compose UI 的view和text同屏4050测试例,我们是在benchmark里公开过的,比Android原生view还慢,更比不过uni蒸汽。[^1]

如果某个原生工程师有兴趣钻研此道,自己一个人搞是不行的,这是大工程,还是加入DCloud一起干吧,哈哈。

8. 我有个疑惑,为什么操作系统那么多专业的人,不去把原生UI优化的更快呢?给三方这么大的超越空间

我不是操作系统团队的人,我也不知道他们怎么想的。我可以推测下,随便聊了。

可能有的系统厂商,觉得自己已经够快了,没有竞争压力给他们。因为要动这些底层的东西,代价不低,立项时总会有人问花这么多钱能带来什么财务回报?

很多领先的厂商,有一个产业义务,就是推进用户买新设备。这对用户来说听着有点恶心。但产业链的兄弟们持续有钱赚也很重要,大家有钱可以持续创新,推进产业进步。

然后就是组织大了,跨部门协调也难。
比如Android,原生的kotlin是一拨人,各种c库是另一拨人,现有的框子是一层一层叠起来的,想压穿,需要协调非常多人。

还有的技术栈喜欢自举,比如java/kotlin,很多重要的库是java写的、而不是c或汇编写的。当你的产品哲学是自举优先而不是性能优先,就很难把性能做到极致。
你看v8的源码,那不止是优秀的c代码,很多地方要上汇编的,比如json解析,这就是性能优先。

当然了,如果操作系统的人真的愿意好好优化,肯定是可以反超uni蒸汽现在的benchmark数据的。

不过呢,其实uni蒸汽,也还可以再提升性能,只不过我们也一样,觉得快了2~3倍已经足以。现在立项投很多人力财力再让uni蒸汽快4倍5倍,没有性价比。
如果原生优化的更快了,我们也会立项再追。

还是那句话,现代计算机工业这么多年,这个庞然大物减肥的空间挺多的。

9. AI时代,跨平台框架的价值是否要重新看待了。最近shopify放弃react native,转原生。这是不是未来的趋势?你们搞了4年多,这个跨平台框架是不是在AI时代生不逢时了?

AI刚来时,我也有点焦虑,担心AI会颠覆掉跨平台框架。
但后来实践、观察、深入思考,心就定了。

首先看数据,AI Coding后,uni的应用数量进一步增长,并不是下降。
然后看我们内部的实践,我们除了框架,也做应用,比如uni-ai、uni-im,一个是开源的AI聊天客户端,一个是开源的IM系统,我们和我们的开发者沟通,都是在uni-im上。

如果没有 uni-app x 和uniCloud,这些项目在我们内部根本无法立项。是因为实际这2个项目,只有一个前端工程师。

如果要用原生来做,2个应用系统,5个客户端平台,Android、iOS、鸿蒙、web、微信小程序,客户端+服务器+测试,即使每个人都在用ai,最少也是十几个人。

除了开发,还要考虑。不同平台,不但代码不同,测试例、测试工具链也不同。但在uni下,不止代码统一了、测试例也是一套。

uni这边,真的从前到后,从开发到测试,所有平台,只有一个人。他用着AI并行开发,快速完成了2个系统。这生产效率和成本,其他方案真的比不了。

主持人:有的人说,我不要6个人,我一个人就让ai干活,写5个平台的原生代码加服务器和测试例,不行吗?

AI只是工具,它需要人用,需要人背锅。工具不背锅。进度延期、线上出bug,你无法甩锅给AI。只有懂AI写的代码,才能hold住,才能担起责任。

尤其是面向广大用户的、涉及重要权益的系统,AI写的代码有问题,造成线上损失,是需要你担责的。

做一些压力不大的系统,比如自用的、有限人使用的内部管理只读报表,可以不看代码。但真要上线,要面向用户承担责任,那人得把关。

回到上一个问题,我们做的uni-ai,它的性能比原生开发的AI客户端都好,和ChatGPT、豆包、deepseek相比帧率都更稳。

如果使用原生开发,每个平台都达到这个性能水平,那我刚才说的十几个人都还不够,那投资得非常大了。

所以uni系产品带来的价值,生产力,非常大。

AI虽然是个工具,但你让它干活时,它也想用好工具,它不会用好工具它就不是个好AI,对吧。
在AI时代,写代码并不是不需要框架,只是需要好框架。
比如ts、vue,这些都是框架,你也可以让AI去写原始的js,但人们为什么不这么做呢?
因为明显是用ts、vue这些好框架,写代码更轻松、更容易保障性能和质量,也更省时间和token。

在AI时代,并不是跨平台框架没有价值了。而是需要更好的跨平台框架。

一个跨平台框架,只跨2个平台,性能比原生差了不少,那么shopify认为AI降低了我的成本,原生带来了更高性能,这ROI是合适的,就会转原生。
假使,我说的是假使,react native的渲染性能比原生更快,那shopify怎么会切原生去?闲的慌啊。

所以AI时代,那些问题比较多的跨平台框架,不管是性能、质量、跨平台数量,如果做的不好,就会没落。
我个人觉得,AI Coding时代,在App平台,可能未来只有原生和uni-app x。其他跨平台框架估计要走下坡路。
这会和小程序领域类似,拼到最后只剩原生和uni-app了。

我这话说的,听起来有点满,其实我是深入思考过的。
新的编程框架规范,对AI非常不友好,很难冒出来。
老的这几家,比如flutter、react native、CMP,如果能解决做到即跨平台、又不比原生性能差,那自然也不可能被AI淘汰。

但flutter的自渲染,包括CMP在iOS上也是自渲染,这种在混合原生渲染时有硬伤。这个在另一篇文章专门讨论过自渲染和原生渲染的问题。[^2]

而react native,从meta挪到基金会后,这个机制有好有坏,不好的就是很难找到一个强力的人做大改,但如果不大改,它解决不了比原生性能差的问题。
shopify本来是基金会的重要赞助者,出力做了不少关键库,更是给基金会出钱。它的撤出,对react native的打击很大。

当然难归难,也不是绝对的,不知道谁家会天降猛人改变局面。

说回来,我认为,AI会淘汰一些简易、低质量的轮子。但复杂、高质量的轮子,永远都受欢迎。AI想要好轮子。

10. uni-app x 刚出来的时候推的是uts语言,现在蒸汽模式又改成了普通的js/ts。为什么会这样调整?怎么考量的?

uni-app x 立项的时候,是参考我们在小程序上的做法,小程序就是把uni-app的写法,编译成了小程序的原生写法,重编译轻运行时。

那么怎么把uni-app x也编译成各个平台的原生应用?比如在Android上,把uni的写法就编译成一个纯的kotlin应用。

这就需要先做一个基建,跨平台的编译型语言,这就是uts,一个语言可以编译成kotlin、Swift、ArkTS。
一个语言,做出来难,做好更难。

但是语言搞定后,我们发现渲染系统做的不好,拖累了整体表现,又开始研究渲染系统。
语言是一个大关,渲染系统又是一个大关,我们连续磕了2个大难关。

当初立项新版渲染系统时,搭配的语言还是uts。因为当时没有预期蒸汽渲染系统这么快。
新版渲染系统出来后,结果是大幅超过我们预期的。我也没想到这块的优化余地这么大,在这么多测试场景中,都能比原生能快2~3倍。[^1]

然后这个时候AI来了,uts语言作为一个新语言,国外模型不熟悉,也没有模型为uts单独做强化学习,给我们带来了巨大的压力。
其实AI在某种程度上,是不利于创新的。不止uts,其他新的编程语言也一样难。

这逼着我们思考,既然渲染系统能快这么多,那么把语言改回到普通ts/js,合体版本是不是也可以做到比原生快?是不是这样的产品才是AI时代更适合开发者的。
这个时候我们团队内部是转不过来弯的,辛辛苦苦搞了几年uts,投入大量心血,真的要尝试换回js吗?自己的孩子,怎么能不心疼?
我自己也给自己做了很多心理建设。

最后是分期立项,优化跨语言通信。先在鸿蒙和iOS上,确认js+蒸汽渲染仍然可以做到比原生渲染快,然后才全体坚定信心,大力推进Android的js更换。
外部看不到,内部其实经历了一个过渡期才把弯转过来。

优化跨语言通信,这个立项最初目标是js+蒸汽渲染系统,能和原生一样快就行。
最终这个项目的结果,也超过了当初立项的预期。
也得益于这几年对语言底层的持续研究,搞明白很多原理,优化后,跨语言通信只损失了百分之几,但蒸汽渲染系统比原生渲染快200%~300%,
最后整合的效果,基本没变,那百分之几在各种评测实验数据上几乎看不出来,还是比原生快2~3倍。[^1]

跨语言通信有个极限例子,就是canvas,在js中绘制碰撞的小球,绘制逻辑都是在js里。最终这个测试例大家也看到benchmark里的数据了,uni蒸汽下,可以绘制几十万个小球同屏碰撞而不掉帧。

最后js+蒸汽渲染引擎这个方案就定下来了,AI熟悉、开发者门槛低、还能热更新,还比原生渲染快,这不就是最佳方案了吗?夫复何求,对吧。

有最佳方案就行,你过去投入的心血,不要遮蔽住你为用户寻找最佳方案的眼睛。

11. 那uts以后怎么办呢?不管是uts语言,还是你们的HBuilder,都是国产自主创新的代表,最近你们基于vscode搞了HBuilderV,感觉这些国产创新的东西好不容易做出来,要被丢掉吗?

国产化,除非是政府资助,对于一个市场化的产品来讲,还是需要在市场中让用户买单。

我们也会觉得可惜,但以国产创新的理由做技术自嗨,得不到用户的认可,没用。
反正我们自己从来不说:flutter、react native不是国产的,请大家支持国产的uni-app。

uts语言不会消失,我们内部也有大量代码是基于uts的,而且我们内部的uts代码还在增加,它仍然是一个不错的跨平台语言。
会的人可以继续用,不会的人,不学也行,原生插件开发也可以使用原生混编。

其实我们还研究过uts直接编译成机器码,发现可行。只是,做出来容易,但想做好,又得几年、好几亿的投入。
除非是政府主导,一定要搞国产化语言,对于市场主体来讲,这事没有性价比。

编译机器码不难,但编译出的机器码非常高效,这特别难。
我们不能假设只要编译成机器码,性能就一定高。
很多语言都尝试过这事,kotlin也有k/n方案,但同样是机器码,它还得处理gc,它的机器码优化程度,和c语言差太多了。

我们也可以很快拿出一个uts编译机器码的方案,但性能想达到第一梯队,就是达到c、zig的水平,需要很长时间和很多资金投入。

至于HBuilderV,这个产品得感谢AI时代,没有AI迁移,这个产品很难立项。
我们的逻辑是这样,给习惯vscode的开发者提供一种选择,有助于大家更接受uni蒸汽,且在AI的帮助下这个产品的成本可接受,那就做了。

目前HBuilderX和HBuilderV会一起演进。

至于未来,HBuilderX是基于QT的,QT以后不会再用了。
其实我们掌握了优秀的渲染优化技术,在PC上也是一样的,后续我们会出uni蒸汽的pc版,届时也会是最快的跨PC平台渲染引擎,体积也会很小。
在uni蒸汽的pc版出来后,我们会再重构HBuilder。
当然也可能换名字。未来AI持续发展,IDE的传统功能都不一定适用了。可能会是一个全新形态。

11.1 那很期待。至于HBuilderV,你们为什么不做vscode插件,而是和cursor一样整体套壳修改?

大家可能对vscode的插件api的丰富度有过高预期。
当初cursor之所以基于vscode套壳二开,就是因为vscode的插件api不够用,AI所需要的功能,插件无法实现,只能整体二开。

对于HBuilderV也一样,HBuilderX里很简单的一个常用需求,就是同时运行多个平台,在底部开一堆控制台,这功能插件就做不了。
还有各处弹出的界面,比如运行、打包,以及我们也有一个AI产品,叫uni-agent,大量的功能需求,用vscode插件无法实现。只能整体二开。

12. 你刚才提到了c、zig,没有提到rust,现在rust风生水起,bun也从zig迁移到rust了。你们的组件不用rust,还使用c语言吗?

rust在pc上发展的更顺利些,手机上用的要少一些。手机系统的c api、ndk编译链,都是围绕c、c++构建的。

不管是c、zig、rust,核心差别是内存谁来管。
其实放大看,所有语言都有这个终极问题,内存到底怎么管。用这点,可以把所有编程语言放在一个坐标体系里看待。

最开始的c语言,典型的手动管理内存。
但人们觉得,我就是开发业务,你让我研究内存怎么管,还经常泄漏,这门槛太高了,不利于编程普及。
然后就出现了gc,就是垃圾回收,java、js,这些语言凭借一点,你别再操心内存了,专注你的业务就完事,帮助这个世界产生了更多的程序员。
然而gc从来都是有代价的,会影响性能。于是有人又想出点子,能不能让编译器来管内存,运行时不要gc了,然后就设计了一套复杂的借用机制,在编译阶段确保内存安全,这就是rust。
但这套机制,学习门槛很高,为了高性能有时还得用unsafe。
于是又有人想,那能不能还是人手动管理内存,但管理的方便点。zig就出现了。
当然zig不止这一个优势,但通过这个对比,可以简单的明白这些编程语言的定位差异。

就我个人喜好,其实我喜欢zig。当然zig的成熟度、生态确实不如rust。
bun的压力来自于基于它的claude code,用户量巨大,带来了很多边界问题。
但bun的问题,并非zig下不能解。
只不过在zig下,需要投很多人来解。但对于一个AI模型公司来讲,能让AI解更符合他们的飞轮。

当然站在第三方来看,折腾这事干嘛,想求稳,应该直接把bun换成nodejs,而不是把bun从zig改成rust。

13. 你也提到一个成熟的产品,需要处理掉各种边界问题,才能获得开发者的认可。那么uni蒸汽刚出来,是否短期内也面临成熟度问题?

这是肯定的。所有新产品都有成熟过程。对于uni蒸汽来讲,就要想各种办法加速它的成熟。
我可以分享下我们怎么做的。

uni-app x 从2023年起第一个版本就开发了自动化测试系统。
并且 uni-app x 坚持一个原则,修复的每个开发者报的issues,都要配套自动化测试例。每个版本发布,都要过测所有测试例才能发版。
我们专门做了一套issues系统,开发人员在提测issues的时候,表单里就有自动化测试例地址。

这几年来,已经积累几万个自动化测试例,包括功能、性能、UI截图对比、内存泄漏、崩溃等多种维度。
每天晚上,在我们内部的持续集成系统里,都要跑几万个测试例,在各种手机os、各种版本上跑,几十万次,做断言比对、截图对比。。。

uni-app x 蒸汽模式 虽然是2026年推出的,但它每次发版,都得通过 uni-app x 积累的所有自动化测试例。

uni蒸汽的质量管理压力,其实对我们来讲,轻了很多。
以前在vdom模式时,我们还要操心uts语言的质量,现在语言的稳定性交给js引擎处理了。
我们就可以集中精力在渲染系统的质量上。
uni蒸汽还有一个特点:不同平台的组件内部实现代码基本都是一套,都是跨平台的c或uts。所以它的平台差异很小。这也让我们的质量管理压力变小。

精力不分散后,我们也有更多人力投在响应开发者反馈的bug上,现在uni蒸汽的修bug速度还是很快的。

在AI时代,AI其实也在帮助我们把控质量,分析隐患、生成更多自动化测试例。这些都让一个新产品的成熟度比以往更快。

我不能说uni蒸汽已经成熟了,但我认为它的质量是可商用的。

好的,感谢王总的坦诚分享。与王总交流,总能感受到一些新的观点和角度。相信读者们,除了更深度的了解uni相关产品外,还可以从这个访谈中学到一些其他东西,比如一个复杂项目如何立项、如何管理、如何提升性能和质量。更多问题,大家可以留言,我们尽量协调王总回答哈。

没问题。

关联阅读

​1. uni-app x 蒸汽模式 性能测试基准报告 Benchmark【鸿蒙版】
​2. uni-app x 蒸汽模式性能测试基准报告 Benchmark【 iOS版】
​3. uni-app x 蒸汽模式 性能测试基准报告 Benchmark【Android版】

  1. 一文讲透原生渲染和自渲染
收起阅读 »

uniappx 基础脚手架,拒绝重复造轮子

uniappx 基础脚手架,包含功能:

  1. 基础简单的登录示例代码
  2. request请求封装
  3. 跳转 + 登录白名单/拦截
  4. 版本更新(多渠道,应用市场, 蒲公英分发或第三方链接等平台)
  5. 文件的上传下载
  6. 图像裁剪上传封装
  7. 主题的切换,包括明暗主题与自定义主题等
继续阅读 »

uniappx 基础脚手架,包含功能:

  1. 基础简单的登录示例代码
  2. request请求封装
  3. 跳转 + 登录白名单/拦截
  4. 版本更新(多渠道,应用市场, 蒲公英分发或第三方链接等平台)
  5. 文件的上传下载
  6. 图像裁剪上传封装
  7. 主题的切换,包括明暗主题与自定义主题等
收起阅读 »

uniappx本地打包微信登录分享提示无提供商

build.gradle.kts新增
buildConfigField("String", "UTSRegisterProviders", "\"[{\\"name\\":\\"weixin\\",\\"service\\":\\"oauth\\",\\"class\\":\\"uts.sdk.modules.DCloudUniOauthWeixin.UniOAuthWeixinProviderImpl\\"},{\\"name\\":\\"weixin\\",\\"service\\":\\"share\\",\\"class\\":\\"uts.sdk.modules.DCloudUniShareWeixin.UniShareWeixinProviderImpl\\"}]\"")

继续阅读 »

build.gradle.kts新增
buildConfigField("String", "UTSRegisterProviders", "\"[{\\"name\\":\\"weixin\\",\\"service\\":\\"oauth\\",\\"class\\":\\"uts.sdk.modules.DCloudUniOauthWeixin.UniOAuthWeixinProviderImpl\\"},{\\"name\\":\\"weixin\\",\\"service\\":\\"share\\",\\"class\\":\\"uts.sdk.modules.DCloudUniShareWeixin.UniShareWeixinProviderImpl\\"}]\"")

收起阅读 »

如何实现Matrix2D图形在画布中心点放大并旋转角度

问题现象
如何在画布中心点实现Matrix2D图形指定倍数的放大和指定角度的旋转?

背景知识
Matrix2D组件的方法rotate,能够以旋转点为中心、对当前矩阵进行右乘旋转运算,调用公式为:rotate(degree: number, rx?: number, ry?: number),其中degree为旋转角度,rx和ry为旋转中心点相对[0,0]点的水平和垂直方向坐标,如果[0,0]点通过translate发生移动,则以新的点计算相对的rx和ry值。
translate方法能够对当前矩阵进行左乘平移运算,调用公式为:translate(tx?: number, ty?: number)。
scale方法能够对当前矩阵进行右乘缩放运算,调用公式为:scale(sx?: number, sy?: number),其中sx和sy为水平和垂直缩放比例系数。以[0,0]为基准进行缩放,如果[0,0]点通过translate发生移动,则以移动后的点为基准点进行缩放。
解决方案
使用Flex布局将Canvas组件居中显示,创建矩形。
使用rotate方法对矩阵进行旋转操作,旋转角度由angle决定,旋转中心为矩形的中心;使用scale方法对矩阵进行缩放操作,缩放倍数由scaleTimesX和scaleTimesY决定,以中心点为基准进行缩放。rotate和scale存在调用顺序,遵循就近原则,因此需要将scale方法的使用放在rotate上方。
使用translate方法对矩阵进行平移操作,将画布的原点移动到画布的中心,即可将图形移动至画布中心。将经过变换的矩阵应用到CanvasRenderingContext2D对象上。
用fillRect()方法在画布上绘制矩形。
完整示例参考如下:
@Entry
@Component
struct MatrixChange {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private matrix: Matrix2D = new Matrix2D();
x: number = 50; // 图形大小
y: number = 50; // 图形大小
canvasWidth: number = 200; // 画布大小
canvasHeight: number = 200; // 画布大小
scaleTimesX: number = 2; // 缩放设置
scaleTimesY: number = 2; // 缩放设置
angle: number = -30; // 旋转角度

build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Canvas(this.context)
.width(this.canvasWidth)
.height(this.canvasHeight)
.backgroundColor('#0D5AF5')
.onReady(() => {
this.matrix.scaleX = 1;
this.matrix.scaleY = 1;
this.matrix.scale(this.scaleTimesX, this.scaleTimesY);
this.matrix.rotate(this.angle Math.PI / 180, this.x / 2, this.y / 2);
this.matrix.translate(this.canvasWidth / 2, this.canvasHeight / 2);
this.matrix.translate(-this.x
this.scaleTimesX / 2, -this.y * this.scaleTimesY / 2);
this.context.setTransform(this.matrix);
this.context.fillRect(0, 0, this.x, this.y);
});
}
.width('100%')
.height('100%');
}
}
总结
Matrix2D图形能够在画布中心点放大指定倍数并旋转指定角度,该功能适用于需要展示图形并对其进行各种变换的场景,例如数据可视化、图形编辑器等。
https://pastebin.com/jYcy8DG4
https://pastebin.com/Hp1sbSaS
https://pastebin.com/hTxV2yyS
https://pastebin.com/TQCjtTcP
https://pastebin.com/JpF1LSct
https://pastebin.com/Fr3XnQdE
https://pastebin.com/yTvhcbNw
https://pastebin.com/hRe1DgNt
https://pastebin.com/M3y1HL1H
https://pastebin.com/VraVGXVf
https://pastebin.com/griS9fnH
https://pastebin.com/pwrdFgiY
https://pastebin.com/Pbn36K7e
https://pastebin.com/7DMANuaz
https://pastebin.com/rHKJdsZT
https://pastebin.com/NLD2BSd3
https://pastebin.com/95Shz0dA
https://pastebin.com/zhphtiyk
https://pastebin.com/3HY6p8t3
https://pastebin.com/XQEtXnKJ
https://pastebin.com/56Zk2nAE
https://pastebin.com/FHzZvcGp
https://pastebin.com/jvCBLRx8
https://pastebin.com/UQ8bqVpS
https://pastebin.com/H5XxuvFD
https://pastebin.com/ZTRtEnKD
https://pastebin.com/DXXEFpJ7
https://pastebin.com/Xz7is4PA
https://pastebin.com/ujgDH7CQ
https://pastebin.com/QXvsH58y
https://pastebin.com/Syp4LL5s
https://pastebin.com/CLgzB5B2
https://pastebin.com/B1P1eZiT
https://pastebin.com/XsyVXiCg
https://pastebin.com/1XCeaqAU
https://pastebin.com/Jw13Rebn
https://pastebin.com/DyCcgNsv
https://pastebin.com/7LEMiQZ2
https://pastebin.com/dS3rfyQ6
https://pastebin.com/wLwm6rp7
https://pastebin.com/4WGjDaH1
https://pastebin.com/zb1zD5aE
https://pastebin.com/c1zNryeR
https://pastebin.com/4s5BeWXh
https://pastebin.com/Ce3nxNXr
https://pastebin.com/cmj5c080
https://pastebin.com/1H5LCdEY
https://pastebin.com/zZkVFh7Q
https://pastebin.com/BBrYWefF
https://pastebin.com/U4vvzbtw
https://pastebin.com/BWKQJ9Bf
https://pastebin.com/B5CXLSym
https://pastebin.com/PebrcV9M
https://pastebin.com/Ee30dGS4
https://pastebin.com/beXkZr16
https://pastebin.com/qfWN2Ryj
https://pastebin.com/c7mAxDum
https://pastebin.com/MpP9YNFU
https://pastebin.com/SQ8jWZqx
https://pastebin.com/9VALda7W
https://pastebin.com/xZ8P3Ks8
https://pastebin.com/niR9ymit
https://pastebin.com/Rv0T6rUx
https://pastebin.com/jAh0PWYa
https://pastebin.com/3wv5Agv8
https://pastebin.com/EtNNfJmj
https://pastebin.com/a9j6TNjC
https://pastebin.com/x6xxPkPX
https://pastebin.com/az8dYRbQ
https://pastebin.com/husRdHb4
https://pastebin.com/ZQw2Pbhx
https://pastebin.com/C59eWK5e
https://pastebin.com/kzcn3KvK
https://pastebin.com/mT8ehVrP
https://pastebin.com/uv3DmK2r
https://pastebin.com/9A90NJRS
https://pastebin.com/nxKRJ6DM
https://pastebin.com/em4D4Psa
https://pastebin.com/sq1LkcWj
https://pastebin.com/X9Qr3LsF
https://pastebin.com/0yhb1G5u
https://pastebin.com/RNBk3wcp
https://pastebin.com/n21GrF8N
https://pastebin.com/BVGGULWt
https://pastebin.com/2LVYSav1
https://pastebin.com/37m2vAg8
https://pastebin.com/YALGJ9Dj
https://pastebin.com/MPrNKc2X
https://pastebin.com/WcQR7d4P
https://pastebin.com/frpzGFbV
https://pastebin.com/MTZN74bH
https://pastebin.com/4XFYvRhd
https://pastebin.com/XEnbVYfF
https://pastebin.com/iGNu6WyC
https://pastebin.com/iC4qCnDP
https://pastebin.com/SubVHQiU
https://pastebin.com/GtmFQfJ6
https://pastebin.com/PqcdaaqX
https://pastebin.com/wbpVhAa4
https://pastebin.com/Gs8c9UJM
https://pastebin.com/rvBJLy48
https://pastebin.com/qJhsAkBV
https://pastebin.com/GsUxwvRp
https://pastebin.com/xhtemkVi
https://pastebin.com/VpcarPwJ
https://pastebin.com/M2MZ4XjZ
https://pastebin.com/1hsmtBAh
https://pastebin.com/FBKiMKLM
https://pastebin.com/TFcszpgY
https://pastebin.com/wZyc488a
https://pastebin.com/eyzX1hF0
https://pastebin.com/pn2C5r5y
https://pastebin.com/dyHRSz7V
https://pastebin.com/qspZ7UVQ
https://pastebin.com/hXZ59vW5
https://pastebin.com/EBymVseu
https://pastebin.com/Pi2qAyiG
https://pastebin.com/r53FkZrX
https://pastebin.com/pBxp5x6P
https://pastebin.com/kRmwv8xX
https://pastebin.com/YPBtC4bD
https://pastebin.com/zaazK0qN
https://pastebin.com/MH9s5BWr
https://pastebin.com/kUWEmtQ3
https://pastebin.com/EiVfHUPQ
https://pastebin.com/F1zDdap8
https://pastebin.com/x6ninbWm
https://pastebin.com/kp5xBGss
https://pastebin.com/X8rLUdJN
https://pastebin.com/V1Zmj7FJ
https://pastebin.com/UheVuGRp
https://pastebin.com/yKHmpBX3
https://pastebin.com/GQaa3tDW
https://pastebin.com/D00GfaUn
https://pastebin.com/2bTz1JVU
https://pastebin.com/hhdmx2bc
https://pastebin.com/KaXVaXRZ
https://pastebin.com/R0R3JCNA
https://pastebin.com/zQGT96rX
https://pastebin.com/cbqQbPyf
https://pastebin.com/6PJN3M7N
https://pastebin.com/k1yeEBr9
https://pastebin.com/GAVHCfGW
https://pastebin.com/V9BDRArR
https://pastebin.com/Ms7giaW9
https://pastebin.com/ddfs4xn7
https://pastebin.com/bhtqrAfp
https://pastebin.com/hCwrGzXp
https://pastebin.com/Cf6NEq1t
https://pastebin.com/i2YZAQTR
https://pastebin.com/PBVaCkjV
https://pastebin.com/CZh9JdFZ
https://pastebin.com/Cf4LLKEd
https://pastebin.com/EiNK5had
https://pastebin.com/j2XSp6pu
https://pastebin.com/1EM6BSxm
https://pastebin.com/P2Zf7gHn
https://pastebin.com/tjjJFy7s
https://pastebin.com/jZ2QMP9d
https://pastebin.com/vtrnXw8f
https://pastebin.com/rKNy2k7F
https://pastebin.com/t792XJE7
https://pastebin.com/BBb77Pfy
https://pastebin.com/euTtybUX
https://pastebin.com/DDiRxvXw
https://pastebin.com/SpLWzDPF
https://pastebin.com/jVk5exQP
https://pastebin.com/FXCHxBMy
https://pastebin.com/WWUqnR5j
https://pastebin.com/fPQs5mLa
https://pastebin.com/RH5zBVEh
https://pastebin.com/C5w1KpPC
https://pastebin.com/1u6PSz3M
https://pastebin.com/hEWn5WFA
https://pastebin.com/8yH2QWxL
https://pastebin.com/TVdRsjMp
https://pastebin.com/rDbhUfwu
https://pastebin.com/drQEKtGE
https://pastebin.com/99Rmnksr
https://pastebin.com/BXZmvJNc
https://pastebin.com/WG4vFfa5
https://pastebin.com/mMuTtyhm
https://pastebin.com/24dfdEw2
https://pastebin.com/3TkPm4xm
https://pastebin.com/X32JiquQ
https://pastebin.com/j5e9PiRm
https://pastebin.com/cuDaup6h
https://pastebin.com/cwxTRY00
https://pastebin.com/J71r912Y
https://pastebin.com/CzD4M9uD
https://pastebin.com/7KBxgcqK
https://pastebin.com/7NfWAa0J
https://pastebin.com/zwkkUavP
https://pastebin.com/shhZgj5L
https://pastebin.com/H68TdJqL
https://pastebin.com/A5DUFX8G
https://pastebin.com/pbPakzr2
https://pastebin.com/XyVssEmP
https://pastebin.com/wtuvMrT4
https://pastebin.com/w24BpmWr
https://pastebin.com/aiG2D9nf
https://pastebin.com/maNzV2CK
https://pastebin.com/cfE0zNBy

继续阅读 »

问题现象
如何在画布中心点实现Matrix2D图形指定倍数的放大和指定角度的旋转?

背景知识
Matrix2D组件的方法rotate,能够以旋转点为中心、对当前矩阵进行右乘旋转运算,调用公式为:rotate(degree: number, rx?: number, ry?: number),其中degree为旋转角度,rx和ry为旋转中心点相对[0,0]点的水平和垂直方向坐标,如果[0,0]点通过translate发生移动,则以新的点计算相对的rx和ry值。
translate方法能够对当前矩阵进行左乘平移运算,调用公式为:translate(tx?: number, ty?: number)。
scale方法能够对当前矩阵进行右乘缩放运算,调用公式为:scale(sx?: number, sy?: number),其中sx和sy为水平和垂直缩放比例系数。以[0,0]为基准进行缩放,如果[0,0]点通过translate发生移动,则以移动后的点为基准点进行缩放。
解决方案
使用Flex布局将Canvas组件居中显示,创建矩形。
使用rotate方法对矩阵进行旋转操作,旋转角度由angle决定,旋转中心为矩形的中心;使用scale方法对矩阵进行缩放操作,缩放倍数由scaleTimesX和scaleTimesY决定,以中心点为基准进行缩放。rotate和scale存在调用顺序,遵循就近原则,因此需要将scale方法的使用放在rotate上方。
使用translate方法对矩阵进行平移操作,将画布的原点移动到画布的中心,即可将图形移动至画布中心。将经过变换的矩阵应用到CanvasRenderingContext2D对象上。
用fillRect()方法在画布上绘制矩形。
完整示例参考如下:
@Entry
@Component
struct MatrixChange {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private matrix: Matrix2D = new Matrix2D();
x: number = 50; // 图形大小
y: number = 50; // 图形大小
canvasWidth: number = 200; // 画布大小
canvasHeight: number = 200; // 画布大小
scaleTimesX: number = 2; // 缩放设置
scaleTimesY: number = 2; // 缩放设置
angle: number = -30; // 旋转角度

build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Canvas(this.context)
.width(this.canvasWidth)
.height(this.canvasHeight)
.backgroundColor('#0D5AF5')
.onReady(() => {
this.matrix.scaleX = 1;
this.matrix.scaleY = 1;
this.matrix.scale(this.scaleTimesX, this.scaleTimesY);
this.matrix.rotate(this.angle Math.PI / 180, this.x / 2, this.y / 2);
this.matrix.translate(this.canvasWidth / 2, this.canvasHeight / 2);
this.matrix.translate(-this.x
this.scaleTimesX / 2, -this.y * this.scaleTimesY / 2);
this.context.setTransform(this.matrix);
this.context.fillRect(0, 0, this.x, this.y);
});
}
.width('100%')
.height('100%');
}
}
总结
Matrix2D图形能够在画布中心点放大指定倍数并旋转指定角度,该功能适用于需要展示图形并对其进行各种变换的场景,例如数据可视化、图形编辑器等。
https://pastebin.com/jYcy8DG4
https://pastebin.com/Hp1sbSaS
https://pastebin.com/hTxV2yyS
https://pastebin.com/TQCjtTcP
https://pastebin.com/JpF1LSct
https://pastebin.com/Fr3XnQdE
https://pastebin.com/yTvhcbNw
https://pastebin.com/hRe1DgNt
https://pastebin.com/M3y1HL1H
https://pastebin.com/VraVGXVf
https://pastebin.com/griS9fnH
https://pastebin.com/pwrdFgiY
https://pastebin.com/Pbn36K7e
https://pastebin.com/7DMANuaz
https://pastebin.com/rHKJdsZT
https://pastebin.com/NLD2BSd3
https://pastebin.com/95Shz0dA
https://pastebin.com/zhphtiyk
https://pastebin.com/3HY6p8t3
https://pastebin.com/XQEtXnKJ
https://pastebin.com/56Zk2nAE
https://pastebin.com/FHzZvcGp
https://pastebin.com/jvCBLRx8
https://pastebin.com/UQ8bqVpS
https://pastebin.com/H5XxuvFD
https://pastebin.com/ZTRtEnKD
https://pastebin.com/DXXEFpJ7
https://pastebin.com/Xz7is4PA
https://pastebin.com/ujgDH7CQ
https://pastebin.com/QXvsH58y
https://pastebin.com/Syp4LL5s
https://pastebin.com/CLgzB5B2
https://pastebin.com/B1P1eZiT
https://pastebin.com/XsyVXiCg
https://pastebin.com/1XCeaqAU
https://pastebin.com/Jw13Rebn
https://pastebin.com/DyCcgNsv
https://pastebin.com/7LEMiQZ2
https://pastebin.com/dS3rfyQ6
https://pastebin.com/wLwm6rp7
https://pastebin.com/4WGjDaH1
https://pastebin.com/zb1zD5aE
https://pastebin.com/c1zNryeR
https://pastebin.com/4s5BeWXh
https://pastebin.com/Ce3nxNXr
https://pastebin.com/cmj5c080
https://pastebin.com/1H5LCdEY
https://pastebin.com/zZkVFh7Q
https://pastebin.com/BBrYWefF
https://pastebin.com/U4vvzbtw
https://pastebin.com/BWKQJ9Bf
https://pastebin.com/B5CXLSym
https://pastebin.com/PebrcV9M
https://pastebin.com/Ee30dGS4
https://pastebin.com/beXkZr16
https://pastebin.com/qfWN2Ryj
https://pastebin.com/c7mAxDum
https://pastebin.com/MpP9YNFU
https://pastebin.com/SQ8jWZqx
https://pastebin.com/9VALda7W
https://pastebin.com/xZ8P3Ks8
https://pastebin.com/niR9ymit
https://pastebin.com/Rv0T6rUx
https://pastebin.com/jAh0PWYa
https://pastebin.com/3wv5Agv8
https://pastebin.com/EtNNfJmj
https://pastebin.com/a9j6TNjC
https://pastebin.com/x6xxPkPX
https://pastebin.com/az8dYRbQ
https://pastebin.com/husRdHb4
https://pastebin.com/ZQw2Pbhx
https://pastebin.com/C59eWK5e
https://pastebin.com/kzcn3KvK
https://pastebin.com/mT8ehVrP
https://pastebin.com/uv3DmK2r
https://pastebin.com/9A90NJRS
https://pastebin.com/nxKRJ6DM
https://pastebin.com/em4D4Psa
https://pastebin.com/sq1LkcWj
https://pastebin.com/X9Qr3LsF
https://pastebin.com/0yhb1G5u
https://pastebin.com/RNBk3wcp
https://pastebin.com/n21GrF8N
https://pastebin.com/BVGGULWt
https://pastebin.com/2LVYSav1
https://pastebin.com/37m2vAg8
https://pastebin.com/YALGJ9Dj
https://pastebin.com/MPrNKc2X
https://pastebin.com/WcQR7d4P
https://pastebin.com/frpzGFbV
https://pastebin.com/MTZN74bH
https://pastebin.com/4XFYvRhd
https://pastebin.com/XEnbVYfF
https://pastebin.com/iGNu6WyC
https://pastebin.com/iC4qCnDP
https://pastebin.com/SubVHQiU
https://pastebin.com/GtmFQfJ6
https://pastebin.com/PqcdaaqX
https://pastebin.com/wbpVhAa4
https://pastebin.com/Gs8c9UJM
https://pastebin.com/rvBJLy48
https://pastebin.com/qJhsAkBV
https://pastebin.com/GsUxwvRp
https://pastebin.com/xhtemkVi
https://pastebin.com/VpcarPwJ
https://pastebin.com/M2MZ4XjZ
https://pastebin.com/1hsmtBAh
https://pastebin.com/FBKiMKLM
https://pastebin.com/TFcszpgY
https://pastebin.com/wZyc488a
https://pastebin.com/eyzX1hF0
https://pastebin.com/pn2C5r5y
https://pastebin.com/dyHRSz7V
https://pastebin.com/qspZ7UVQ
https://pastebin.com/hXZ59vW5
https://pastebin.com/EBymVseu
https://pastebin.com/Pi2qAyiG
https://pastebin.com/r53FkZrX
https://pastebin.com/pBxp5x6P
https://pastebin.com/kRmwv8xX
https://pastebin.com/YPBtC4bD
https://pastebin.com/zaazK0qN
https://pastebin.com/MH9s5BWr
https://pastebin.com/kUWEmtQ3
https://pastebin.com/EiVfHUPQ
https://pastebin.com/F1zDdap8
https://pastebin.com/x6ninbWm
https://pastebin.com/kp5xBGss
https://pastebin.com/X8rLUdJN
https://pastebin.com/V1Zmj7FJ
https://pastebin.com/UheVuGRp
https://pastebin.com/yKHmpBX3
https://pastebin.com/GQaa3tDW
https://pastebin.com/D00GfaUn
https://pastebin.com/2bTz1JVU
https://pastebin.com/hhdmx2bc
https://pastebin.com/KaXVaXRZ
https://pastebin.com/R0R3JCNA
https://pastebin.com/zQGT96rX
https://pastebin.com/cbqQbPyf
https://pastebin.com/6PJN3M7N
https://pastebin.com/k1yeEBr9
https://pastebin.com/GAVHCfGW
https://pastebin.com/V9BDRArR
https://pastebin.com/Ms7giaW9
https://pastebin.com/ddfs4xn7
https://pastebin.com/bhtqrAfp
https://pastebin.com/hCwrGzXp
https://pastebin.com/Cf6NEq1t
https://pastebin.com/i2YZAQTR
https://pastebin.com/PBVaCkjV
https://pastebin.com/CZh9JdFZ
https://pastebin.com/Cf4LLKEd
https://pastebin.com/EiNK5had
https://pastebin.com/j2XSp6pu
https://pastebin.com/1EM6BSxm
https://pastebin.com/P2Zf7gHn
https://pastebin.com/tjjJFy7s
https://pastebin.com/jZ2QMP9d
https://pastebin.com/vtrnXw8f
https://pastebin.com/rKNy2k7F
https://pastebin.com/t792XJE7
https://pastebin.com/BBb77Pfy
https://pastebin.com/euTtybUX
https://pastebin.com/DDiRxvXw
https://pastebin.com/SpLWzDPF
https://pastebin.com/jVk5exQP
https://pastebin.com/FXCHxBMy
https://pastebin.com/WWUqnR5j
https://pastebin.com/fPQs5mLa
https://pastebin.com/RH5zBVEh
https://pastebin.com/C5w1KpPC
https://pastebin.com/1u6PSz3M
https://pastebin.com/hEWn5WFA
https://pastebin.com/8yH2QWxL
https://pastebin.com/TVdRsjMp
https://pastebin.com/rDbhUfwu
https://pastebin.com/drQEKtGE
https://pastebin.com/99Rmnksr
https://pastebin.com/BXZmvJNc
https://pastebin.com/WG4vFfa5
https://pastebin.com/mMuTtyhm
https://pastebin.com/24dfdEw2
https://pastebin.com/3TkPm4xm
https://pastebin.com/X32JiquQ
https://pastebin.com/j5e9PiRm
https://pastebin.com/cuDaup6h
https://pastebin.com/cwxTRY00
https://pastebin.com/J71r912Y
https://pastebin.com/CzD4M9uD
https://pastebin.com/7KBxgcqK
https://pastebin.com/7NfWAa0J
https://pastebin.com/zwkkUavP
https://pastebin.com/shhZgj5L
https://pastebin.com/H68TdJqL
https://pastebin.com/A5DUFX8G
https://pastebin.com/pbPakzr2
https://pastebin.com/XyVssEmP
https://pastebin.com/wtuvMrT4
https://pastebin.com/w24BpmWr
https://pastebin.com/aiG2D9nf
https://pastebin.com/maNzV2CK
https://pastebin.com/cfE0zNBy

收起阅读 »

如何在不同场景下实现页面置灰效果

问题现象
在不同的场景下,如何实现将单个组件、页面或者整个app置灰的效果,具体场景如下:

场景

场景说明

场景一:单个组件置灰

将单个组件颜色置灰,其他组件不受影响,例如将一张彩色图片变成灰白色图片

场景二:单个页面置灰

在多个页面场景中,只将其中某个页面颜色置灰,其他页面不受影响

场景三:所有页面置灰

将app中的所有页面颜色置灰,例如当发生一些重大事件(哀悼日)时,要求将整个app的页面颜色统一设置为灰色

背景知识
Navigation:路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
grayscale:为组件添加灰度效果。上层渲染灰度会覆盖下层子组件渲染。值定义为灰度转换的比例,入参1则完全转为灰度图像,入参0则图像无变化,入参在0和1之间时,效果呈线性变化。
saturate:为组件添加饱和度效果,饱和度为颜色中的含色成分和消色成分(灰)的比例,入参为1时,显示原图像,大于1时含色成分越大,饱和度越大,小于1时消色成分越大,饱和度越小。
解决方案
场景一:单个组件置灰。
通过给Image组件添加grayscale属性并将属性值设置为1来实现图片置灰效果,或者使用saturate属性并将属性值设置为0来实现图片置灰效果。示例代码如下:
@Entry
@Component
struct GrayDemo {
@State grayscaleValue: number = 0; // 灰度默认为0,正常显示
@State saturateValue: number = 1; // 饱和度默认为1,正常显示

build() {
Row() {
Column() {
Text('grayscale:')
Image($r('app.media.startIcon'))
.autoResize(true)
.width(100)
.height(100)
.margin(16)
.grayscale(this.grayscaleValue) // 设置图片灰度效果
Button('一键置灰').onClick(() => {
this.grayscaleValue = 1; // 设置灰度为100%
})

    Text('saturate:').margin({ top: 16 })  
    Image($r('app.media.startIcon'))  
      .autoResize(true)  
      .width(100)  
      .height(100)  
      .margin(16)  
      .saturate(this.saturateValue) // 设置图片的饱和度  
    Button('一键置灰').onClick(() => {  
      this.saturateValue = 0; // 将饱和度设置为0,显示灰白  
    })  
  }  
}  
.justifyContent(FlexAlign.Center)  
.alignItems(VerticalAlign.Center)  
.width('100%')  
.height('100%')  

}
}
效果图如下:

点击放大

场景二:单个页面置灰。
在多个页面场景,如果只有某个页面需要置灰,其他页面不变,只需将相应页面根元素设置grayscale属性为1。

首先定义一个变量grayscaleValueSingle,本页面根组件Column设置grayscale属性,分别给用于实现本页面置灰和还原效果的Button绑定onClick事件,该事件用于修改grayscaleValueSingle的值,当该值变化时,即可实现本页面的置灰效果,子页面没有置灰。

效果图如下:

点击放大

场景三:所有页面置灰。Navigation设置的一些属性会影响所有的页面,将主页的Navigation组件设置grayscale属性为1,可实现所有页面置灰的效果。完整示例代码如下:主页Index.ets示例代码如下:
@Entry
@Component
struct Index {
@State grayscaleValueAll: number = 0; // 灰度默认为0,正常显示
@State grayscaleValueSingle: number = 0;
pageStack: NavPathStack = new NavPathStack();

build() {
Navigation(this.pageStack) {
Column() {
Button('前往子页', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageStack.pushPathByName('PageOne', null, false);
})
Image($r('app.media.startIcon'))
.width('50%')
.height('30%')
Button('本页面置灰', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.grayscaleValueSingle = 1; // 设置灰度为100%
})
Button('全局置灰', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.onClick(() => {
this.grayscaleValueAll = 1; // 设置灰度为100%
})
Button('还原', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.grayscaleValueAll = 0;
this.grayscaleValueSingle = 0;
})
}.grayscale(this.grayscaleValueSingle)
}
.title('主页')
.grayscale(this.grayscaleValueAll)
}
}
子页PageOne.ets示例代码如下:

@Builder
export function PageOneBuilder() {
PageOne()
}

@Entry
@Component
struct PageOne {
build() {
NavDestination() {
Column() {
Button('这是子页', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
}
.width('100%')
.height('100%')
}
}
}
route_map需放置在项目名/src/main/resources/base/profile/route_map.json路径下,具体代码如下:

{
"routerMap": [
{
"name": "PageOne",
"pageSourceFile": "src/main/ets/pages/PageOne.ets",
"buildFunction": "PageOneBuilder",
"data": {
"description": "This is PageOne"
}
}
]
}
在module.json5配置文件的module标签中定义routerMap字段,指向定义的路由表配置文件route_map.json,如:"routerMap": "$profile:route_map"。

效果图如下:

点击放大

常见FAQ
Q:在Navigation组件中添加的属性,在NavDestination组件中也会生效吗?

A:NavDestination作为子页面的根容器,用于显示Navigation的内容区。NavDestination组件在渲染时会使用到Navigation组件中添加的属性。
https://pastebin.com/tB45X2Jp
https://pastebin.com/hjD1EQe6
https://pastebin.com/ZPZSXrit
https://pastebin.com/BKvXc1XH
https://pastebin.com/EWXDC53c
https://pastebin.com/cZtc3Gm0
https://pastebin.com/D8xdXuhM
https://pastebin.com/EgrbEUJD
https://pastebin.com/8VvqjShG
https://pastebin.com/C8U16GGp
https://pastebin.com/601zAGr0
https://pastebin.com/VUYU8tTc
https://pastebin.com/4eSAuAFs
https://pastebin.com/hYWauvkZ
https://pastebin.com/dKtpzYwL
https://pastebin.com/faS8dizU
https://pastebin.com/epWAdZjT
https://pastebin.com/N28eTbrZ
https://pastebin.com/4Yhy1g4R
https://pastebin.com/pVHAppet
https://pastebin.com/6n0m31zk
https://pastebin.com/BwNRYEsp
https://pastebin.com/UHgxVZrY
https://pastebin.com/sdBk4pwS
https://pastebin.com/qw3Qeiv3
https://pastebin.com/ZE3yTP0u
https://pastebin.com/fbLvkuFA
https://pastebin.com/jrQByFhQ
https://pastebin.com/BLDqNVZd
https://pastebin.com/rsWambwi
https://pastebin.com/jSNv47wj
https://pastebin.com/SgFwMwCn
https://pastebin.com/ii9Mx19w
https://pastebin.com/U12DCAjc
https://pastebin.com/J4UTSFCv
https://pastebin.com/vLKGZmEC
https://pastebin.com/45sT2V0T
https://pastebin.com/HNSU6w07
https://pastebin.com/Q1ez1SyN
https://pastebin.com/NU2YtB2j
https://pastebin.com/4vYkErDF
https://pastebin.com/HbEs4xd0
https://pastebin.com/EQ5rygX0
https://pastebin.com/56zyx4mu
https://pastebin.com/BxL1hQtN
https://pastebin.com/u95LA4eA
https://pastebin.com/SERQ5L1R
https://pastebin.com/1HLR4hfE
https://pastebin.com/uhYYhvY7
https://pastebin.com/Gzwqu9J8
https://pastebin.com/TRzJAa7C
https://pastebin.com/jkvnkxUf
https://pastebin.com/tQniYitM
https://pastebin.com/teftqPhB
https://pastebin.com/ghmm3Z45
https://pastebin.com/u8zhaGJe
https://pastebin.com/pBUjLT6a
https://pastebin.com/F8xircWV
https://pastebin.com/bHfQyddL
https://pastebin.com/US11UXan
https://pastebin.com/uUq8A9PG
https://pastebin.com/gf0TnLaX
https://pastebin.com/KbjDei75
https://pastebin.com/Eg8SBXip
https://pastebin.com/TRnQT00H
https://pastebin.com/hrJTVPDq
https://pastebin.com/FQybjNNY
https://pastebin.com/vkjZ4wN3
https://pastebin.com/Em0EVbKz
https://pastebin.com/8SQtnCqe
https://pastebin.com/JzVba9rR
https://pastebin.com/VYum9agp
https://pastebin.com/kkQzAQQb
https://pastebin.com/rupgeDSt
https://pastebin.com/zaLpEv2s
https://pastebin.com/GcbHRNFi
https://pastebin.com/bpp4uE8Z
https://pastebin.com/7HKMVmd2
https://pastebin.com/Fg7cmq4m
https://pastebin.com/4yzireaB
https://pastebin.com/8JxAZZaT
https://pastebin.com/Ja0YVzXd
https://pastebin.com/ZD0SeiaT
https://pastebin.com/03wkfuNw
https://pastebin.com/2LsPES0D
https://pastebin.com/kpcMTN8H
https://pastebin.com/wDTn15yX
https://pastebin.com/DpmFJRhA
https://pastebin.com/WSgL8jW4
https://pastebin.com/XpDeHfFU
https://pastebin.com/HwhXqV7d
https://pastebin.com/KQuJBJGJ
https://pastebin.com/PvAf8UmX
https://pastebin.com/NvrpD52h
https://pastebin.com/mVcns0hQ
https://pastebin.com/iHkN8Zqt
https://pastebin.com/CtB7EXHR
https://pastebin.com/LGWxiLVh
https://pastebin.com/e1yYzpGV
https://pastebin.com/r690DqD6
https://pastebin.com/dxHZMZpR
https://pastebin.com/9Bt6QmGx
https://pastebin.com/3GR4wnyg
https://pastebin.com/U48VGWDZ
https://pastebin.com/DsvcvGfg
https://pastebin.com/5mJKbEHB
https://pastebin.com/XJHSbEB5
https://pastebin.com/K2TCyawf
https://pastebin.com/Ewn4DHM5
https://pastebin.com/Ta7ufBT4
https://pastebin.com/atf8fKFA
https://pastebin.com/SCwnf7Kq
https://pastebin.com/WwPcSmwn
https://pastebin.com/u1BUyfCp
https://pastebin.com/MdTnRfHn
https://pastebin.com/EbeshHhc
https://pastebin.com/qGcygDuP
https://pastebin.com/SqRmqdgU
https://pastebin.com/7npW5utw
https://pastebin.com/GjSNL297
https://pastebin.com/unDuaQRL
https://pastebin.com/GW5NYdDa
https://pastebin.com/v7NDFUPc
https://pastebin.com/8VRW7Q9K
https://pastebin.com/2KXX6VGt
https://pastebin.com/MCmswb3v
https://pastebin.com/iiuA6adw
https://pastebin.com/D58m3xJB
https://pastebin.com/0S5C0W5s
https://pastebin.com/kd37nDY6
https://pastebin.com/THKnHxWN
https://pastebin.com/b0cNSgs9
https://pastebin.com/4EgxXbvr
https://pastebin.com/e0Y46BLB
https://pastebin.com/JYnQsScA
https://pastebin.com/0JHz4u7D
https://pastebin.com/eLygzWDz
https://pastebin.com/BpNaLvtM
https://pastebin.com/YEpynedk
https://pastebin.com/nr92i4cm
https://pastebin.com/GyJdL3jT
https://pastebin.com/5J59xjwP
https://pastebin.com/YzKHrHf8
https://pastebin.com/9e9Fw0qZ
https://pastebin.com/xeyWtjCZ
https://pastebin.com/ZuNfjEXd
https://pastebin.com/1eAXJXch
https://pastebin.com/wScAwcxA
https://pastebin.com/veLVP8JY
https://pastebin.com/gp9ivT1n
https://pastebin.com/09pRUVcp
https://pastebin.com/KDpZcsSs
https://pastebin.com/qPuKxaGF
https://pastebin.com/Z1HRFRjF
https://pastebin.com/jR9E2zAk
https://pastebin.com/E42XsidQ
https://pastebin.com/vnpF0CrJ
https://pastebin.com/tPdeYkh9
https://pastebin.com/SvdMHSSw
https://pastebin.com/8vBeN0S8
https://pastebin.com/B6WRVRQ4
https://pastebin.com/zydaXiMd
https://pastebin.com/nsb4NEC3
https://pastebin.com/QxyaK9dt
https://pastebin.com/aztZVSES
https://pastebin.com/gq0Ugnzu
https://pastebin.com/WQKjeyU6
https://pastebin.com/JjqU9JHd
https://pastebin.com/m8x0g5jW
https://pastebin.com/5nbWUgFh
https://pastebin.com/Rzgpje7d
https://pastebin.com/K0wf5rAY
https://pastebin.com/ZDPfudPN
https://pastebin.com/RSxAxA0k
https://pastebin.com/WwvnWgmu
https://pastebin.com/pGpEm8MC
https://pastebin.com/yeGNby1Y
https://pastebin.com/G0i3MQkj
https://pastebin.com/te38JUac
https://pastebin.com/dDmpLYkT
https://pastebin.com/a6eywsqw
https://pastebin.com/u2aXcVQm
https://pastebin.com/ECmeMX9f
https://pastebin.com/VcR7W9zx
https://pastebin.com/XRU0XdJV
https://pastebin.com/vrr5hgAv
https://pastebin.com/fRf4Uz2n
https://pastebin.com/wNUGRHMQ
https://pastebin.com/yVnHymtb
https://pastebin.com/gQWJU5a2
https://pastebin.com/VfquXqXb
https://pastebin.com/k1bQrXjs
https://pastebin.com/2BRqX78a
https://pastebin.com/qhfE1Qni
https://pastebin.com/uxwXhuAJ
https://pastebin.com/Z68kDKtq
https://pastebin.com/DUsNuWn8
https://pastebin.com/ALf4SeRQ
https://pastebin.com/6922Wz0j
https://pastebin.com/GwpbvjZw
https://pastebin.com/psciSpxN
https://pastebin.com/3gZnEA2Q
https://pastebin.com/n0cwrgXU
https://pastebin.com/hq907uQR
https://pastebin.com/9VehTSWL
https://pastebin.com/8UE4KeVN

继续阅读 »

问题现象
在不同的场景下,如何实现将单个组件、页面或者整个app置灰的效果,具体场景如下:

场景

场景说明

场景一:单个组件置灰

将单个组件颜色置灰,其他组件不受影响,例如将一张彩色图片变成灰白色图片

场景二:单个页面置灰

在多个页面场景中,只将其中某个页面颜色置灰,其他页面不受影响

场景三:所有页面置灰

将app中的所有页面颜色置灰,例如当发生一些重大事件(哀悼日)时,要求将整个app的页面颜色统一设置为灰色

背景知识
Navigation:路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(Navigation的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
grayscale:为组件添加灰度效果。上层渲染灰度会覆盖下层子组件渲染。值定义为灰度转换的比例,入参1则完全转为灰度图像,入参0则图像无变化,入参在0和1之间时,效果呈线性变化。
saturate:为组件添加饱和度效果,饱和度为颜色中的含色成分和消色成分(灰)的比例,入参为1时,显示原图像,大于1时含色成分越大,饱和度越大,小于1时消色成分越大,饱和度越小。
解决方案
场景一:单个组件置灰。
通过给Image组件添加grayscale属性并将属性值设置为1来实现图片置灰效果,或者使用saturate属性并将属性值设置为0来实现图片置灰效果。示例代码如下:
@Entry
@Component
struct GrayDemo {
@State grayscaleValue: number = 0; // 灰度默认为0,正常显示
@State saturateValue: number = 1; // 饱和度默认为1,正常显示

build() {
Row() {
Column() {
Text('grayscale:')
Image($r('app.media.startIcon'))
.autoResize(true)
.width(100)
.height(100)
.margin(16)
.grayscale(this.grayscaleValue) // 设置图片灰度效果
Button('一键置灰').onClick(() => {
this.grayscaleValue = 1; // 设置灰度为100%
})

    Text('saturate:').margin({ top: 16 })  
    Image($r('app.media.startIcon'))  
      .autoResize(true)  
      .width(100)  
      .height(100)  
      .margin(16)  
      .saturate(this.saturateValue) // 设置图片的饱和度  
    Button('一键置灰').onClick(() => {  
      this.saturateValue = 0; // 将饱和度设置为0,显示灰白  
    })  
  }  
}  
.justifyContent(FlexAlign.Center)  
.alignItems(VerticalAlign.Center)  
.width('100%')  
.height('100%')  

}
}
效果图如下:

点击放大

场景二:单个页面置灰。
在多个页面场景,如果只有某个页面需要置灰,其他页面不变,只需将相应页面根元素设置grayscale属性为1。

首先定义一个变量grayscaleValueSingle,本页面根组件Column设置grayscale属性,分别给用于实现本页面置灰和还原效果的Button绑定onClick事件,该事件用于修改grayscaleValueSingle的值,当该值变化时,即可实现本页面的置灰效果,子页面没有置灰。

效果图如下:

点击放大

场景三:所有页面置灰。Navigation设置的一些属性会影响所有的页面,将主页的Navigation组件设置grayscale属性为1,可实现所有页面置灰的效果。完整示例代码如下:主页Index.ets示例代码如下:
@Entry
@Component
struct Index {
@State grayscaleValueAll: number = 0; // 灰度默认为0,正常显示
@State grayscaleValueSingle: number = 0;
pageStack: NavPathStack = new NavPathStack();

build() {
Navigation(this.pageStack) {
Column() {
Button('前往子页', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pageStack.pushPathByName('PageOne', null, false);
})
Image($r('app.media.startIcon'))
.width('50%')
.height('30%')
Button('本页面置灰', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.grayscaleValueSingle = 1; // 设置灰度为100%
})
Button('全局置灰', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.onClick(() => {
this.grayscaleValueAll = 1; // 设置灰度为100%
})
Button('还原', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.grayscaleValueAll = 0;
this.grayscaleValueSingle = 0;
})
}.grayscale(this.grayscaleValueSingle)
}
.title('主页')
.grayscale(this.grayscaleValueAll)
}
}
子页PageOne.ets示例代码如下:

@Builder
export function PageOneBuilder() {
PageOne()
}

@Entry
@Component
struct PageOne {
build() {
NavDestination() {
Column() {
Button('这是子页', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
}
.width('100%')
.height('100%')
}
}
}
route_map需放置在项目名/src/main/resources/base/profile/route_map.json路径下,具体代码如下:

{
"routerMap": [
{
"name": "PageOne",
"pageSourceFile": "src/main/ets/pages/PageOne.ets",
"buildFunction": "PageOneBuilder",
"data": {
"description": "This is PageOne"
}
}
]
}
在module.json5配置文件的module标签中定义routerMap字段,指向定义的路由表配置文件route_map.json,如:"routerMap": "$profile:route_map"。

效果图如下:

点击放大

常见FAQ
Q:在Navigation组件中添加的属性,在NavDestination组件中也会生效吗?

A:NavDestination作为子页面的根容器,用于显示Navigation的内容区。NavDestination组件在渲染时会使用到Navigation组件中添加的属性。
https://pastebin.com/tB45X2Jp
https://pastebin.com/hjD1EQe6
https://pastebin.com/ZPZSXrit
https://pastebin.com/BKvXc1XH
https://pastebin.com/EWXDC53c
https://pastebin.com/cZtc3Gm0
https://pastebin.com/D8xdXuhM
https://pastebin.com/EgrbEUJD
https://pastebin.com/8VvqjShG
https://pastebin.com/C8U16GGp
https://pastebin.com/601zAGr0
https://pastebin.com/VUYU8tTc
https://pastebin.com/4eSAuAFs
https://pastebin.com/hYWauvkZ
https://pastebin.com/dKtpzYwL
https://pastebin.com/faS8dizU
https://pastebin.com/epWAdZjT
https://pastebin.com/N28eTbrZ
https://pastebin.com/4Yhy1g4R
https://pastebin.com/pVHAppet
https://pastebin.com/6n0m31zk
https://pastebin.com/BwNRYEsp
https://pastebin.com/UHgxVZrY
https://pastebin.com/sdBk4pwS
https://pastebin.com/qw3Qeiv3
https://pastebin.com/ZE3yTP0u
https://pastebin.com/fbLvkuFA
https://pastebin.com/jrQByFhQ
https://pastebin.com/BLDqNVZd
https://pastebin.com/rsWambwi
https://pastebin.com/jSNv47wj
https://pastebin.com/SgFwMwCn
https://pastebin.com/ii9Mx19w
https://pastebin.com/U12DCAjc
https://pastebin.com/J4UTSFCv
https://pastebin.com/vLKGZmEC
https://pastebin.com/45sT2V0T
https://pastebin.com/HNSU6w07
https://pastebin.com/Q1ez1SyN
https://pastebin.com/NU2YtB2j
https://pastebin.com/4vYkErDF
https://pastebin.com/HbEs4xd0
https://pastebin.com/EQ5rygX0
https://pastebin.com/56zyx4mu
https://pastebin.com/BxL1hQtN
https://pastebin.com/u95LA4eA
https://pastebin.com/SERQ5L1R
https://pastebin.com/1HLR4hfE
https://pastebin.com/uhYYhvY7
https://pastebin.com/Gzwqu9J8
https://pastebin.com/TRzJAa7C
https://pastebin.com/jkvnkxUf
https://pastebin.com/tQniYitM
https://pastebin.com/teftqPhB
https://pastebin.com/ghmm3Z45
https://pastebin.com/u8zhaGJe
https://pastebin.com/pBUjLT6a
https://pastebin.com/F8xircWV
https://pastebin.com/bHfQyddL
https://pastebin.com/US11UXan
https://pastebin.com/uUq8A9PG
https://pastebin.com/gf0TnLaX
https://pastebin.com/KbjDei75
https://pastebin.com/Eg8SBXip
https://pastebin.com/TRnQT00H
https://pastebin.com/hrJTVPDq
https://pastebin.com/FQybjNNY
https://pastebin.com/vkjZ4wN3
https://pastebin.com/Em0EVbKz
https://pastebin.com/8SQtnCqe
https://pastebin.com/JzVba9rR
https://pastebin.com/VYum9agp
https://pastebin.com/kkQzAQQb
https://pastebin.com/rupgeDSt
https://pastebin.com/zaLpEv2s
https://pastebin.com/GcbHRNFi
https://pastebin.com/bpp4uE8Z
https://pastebin.com/7HKMVmd2
https://pastebin.com/Fg7cmq4m
https://pastebin.com/4yzireaB
https://pastebin.com/8JxAZZaT
https://pastebin.com/Ja0YVzXd
https://pastebin.com/ZD0SeiaT
https://pastebin.com/03wkfuNw
https://pastebin.com/2LsPES0D
https://pastebin.com/kpcMTN8H
https://pastebin.com/wDTn15yX
https://pastebin.com/DpmFJRhA
https://pastebin.com/WSgL8jW4
https://pastebin.com/XpDeHfFU
https://pastebin.com/HwhXqV7d
https://pastebin.com/KQuJBJGJ
https://pastebin.com/PvAf8UmX
https://pastebin.com/NvrpD52h
https://pastebin.com/mVcns0hQ
https://pastebin.com/iHkN8Zqt
https://pastebin.com/CtB7EXHR
https://pastebin.com/LGWxiLVh
https://pastebin.com/e1yYzpGV
https://pastebin.com/r690DqD6
https://pastebin.com/dxHZMZpR
https://pastebin.com/9Bt6QmGx
https://pastebin.com/3GR4wnyg
https://pastebin.com/U48VGWDZ
https://pastebin.com/DsvcvGfg
https://pastebin.com/5mJKbEHB
https://pastebin.com/XJHSbEB5
https://pastebin.com/K2TCyawf
https://pastebin.com/Ewn4DHM5
https://pastebin.com/Ta7ufBT4
https://pastebin.com/atf8fKFA
https://pastebin.com/SCwnf7Kq
https://pastebin.com/WwPcSmwn
https://pastebin.com/u1BUyfCp
https://pastebin.com/MdTnRfHn
https://pastebin.com/EbeshHhc
https://pastebin.com/qGcygDuP
https://pastebin.com/SqRmqdgU
https://pastebin.com/7npW5utw
https://pastebin.com/GjSNL297
https://pastebin.com/unDuaQRL
https://pastebin.com/GW5NYdDa
https://pastebin.com/v7NDFUPc
https://pastebin.com/8VRW7Q9K
https://pastebin.com/2KXX6VGt
https://pastebin.com/MCmswb3v
https://pastebin.com/iiuA6adw
https://pastebin.com/D58m3xJB
https://pastebin.com/0S5C0W5s
https://pastebin.com/kd37nDY6
https://pastebin.com/THKnHxWN
https://pastebin.com/b0cNSgs9
https://pastebin.com/4EgxXbvr
https://pastebin.com/e0Y46BLB
https://pastebin.com/JYnQsScA
https://pastebin.com/0JHz4u7D
https://pastebin.com/eLygzWDz
https://pastebin.com/BpNaLvtM
https://pastebin.com/YEpynedk
https://pastebin.com/nr92i4cm
https://pastebin.com/GyJdL3jT
https://pastebin.com/5J59xjwP
https://pastebin.com/YzKHrHf8
https://pastebin.com/9e9Fw0qZ
https://pastebin.com/xeyWtjCZ
https://pastebin.com/ZuNfjEXd
https://pastebin.com/1eAXJXch
https://pastebin.com/wScAwcxA
https://pastebin.com/veLVP8JY
https://pastebin.com/gp9ivT1n
https://pastebin.com/09pRUVcp
https://pastebin.com/KDpZcsSs
https://pastebin.com/qPuKxaGF
https://pastebin.com/Z1HRFRjF
https://pastebin.com/jR9E2zAk
https://pastebin.com/E42XsidQ
https://pastebin.com/vnpF0CrJ
https://pastebin.com/tPdeYkh9
https://pastebin.com/SvdMHSSw
https://pastebin.com/8vBeN0S8
https://pastebin.com/B6WRVRQ4
https://pastebin.com/zydaXiMd
https://pastebin.com/nsb4NEC3
https://pastebin.com/QxyaK9dt
https://pastebin.com/aztZVSES
https://pastebin.com/gq0Ugnzu
https://pastebin.com/WQKjeyU6
https://pastebin.com/JjqU9JHd
https://pastebin.com/m8x0g5jW
https://pastebin.com/5nbWUgFh
https://pastebin.com/Rzgpje7d
https://pastebin.com/K0wf5rAY
https://pastebin.com/ZDPfudPN
https://pastebin.com/RSxAxA0k
https://pastebin.com/WwvnWgmu
https://pastebin.com/pGpEm8MC
https://pastebin.com/yeGNby1Y
https://pastebin.com/G0i3MQkj
https://pastebin.com/te38JUac
https://pastebin.com/dDmpLYkT
https://pastebin.com/a6eywsqw
https://pastebin.com/u2aXcVQm
https://pastebin.com/ECmeMX9f
https://pastebin.com/VcR7W9zx
https://pastebin.com/XRU0XdJV
https://pastebin.com/vrr5hgAv
https://pastebin.com/fRf4Uz2n
https://pastebin.com/wNUGRHMQ
https://pastebin.com/yVnHymtb
https://pastebin.com/gQWJU5a2
https://pastebin.com/VfquXqXb
https://pastebin.com/k1bQrXjs
https://pastebin.com/2BRqX78a
https://pastebin.com/qhfE1Qni
https://pastebin.com/uxwXhuAJ
https://pastebin.com/Z68kDKtq
https://pastebin.com/DUsNuWn8
https://pastebin.com/ALf4SeRQ
https://pastebin.com/6922Wz0j
https://pastebin.com/GwpbvjZw
https://pastebin.com/psciSpxN
https://pastebin.com/3gZnEA2Q
https://pastebin.com/n0cwrgXU
https://pastebin.com/hq907uQR
https://pastebin.com/9VehTSWL
https://pastebin.com/8UE4KeVN

收起阅读 »

#插件需求# uniapp x实现APP热更新

插件需求

uniapp x蒸汽模式希望官方出一个 wgt热更新,或是出一个热更新的解决方案,便于APP后期持续开发

uniapp x蒸汽模式希望官方出一个 wgt热更新,或是出一个热更新的解决方案,便于APP后期持续开发

如何实现键盘上方自定义工具栏

问题现象
跟随键盘的工具栏如何实现?

背景知识
自定义弹窗(CustomDialog):通过CustomDialogController类显示自定义弹窗。使用弹窗组件时,优先考虑自定义弹窗,便于弹窗样式与内容的自定义。
defaultFocus:设置当前组件是否为当前页面上的默认焦点。
on('keyboardHeightChange'):开启固定态软键盘高度变化的监听,当软键盘由本窗口唤出并存在重叠区域时通知键盘高度变化。
Stack:堆叠容器,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件。
alignContent:设置子组件在容器内的对齐方式。该属性与通用属性align同时设置时,后设置的属性生效。
解决方案
针对跟随键盘的工具栏,可采用以下两种实现方案:

方案一:使用CustomDialog组件自定义工具栏。以下示例通过点击文本组件使弹窗自动获焦,设置弹窗从底部弹出,并为弹窗添加弹出与关闭时的过渡动画,以实现跟随键盘的动画效果。
// 自定义对话框组件,用于显示输入框
@CustomDialog
@Component
export struct Dialog {
dialogController: CustomDialogController;

build() {
Column() {
TextInput({ text: '', placeholder: 'input your word...' })
.type(InputType.Normal)
.defaultFocus(true) // 设置为默认聚焦状态
.onBlur(() => {
// 输入框失去焦点时关闭对话框
this.dialogController.close();
})
.height('50%')
.margin({ top: 25, left: 16, right: 16 });
}
.height(100)
.backgroundColor(Color.White);
}
}

@Entry
@Component
struct Index {
message: string = 'Hello World';
// 自定义对话框控制器,配置对话框的显示参数
customDialogController: CustomDialogController = new CustomDialogController({
builder: Dialog(), // 设置对话框内容组件
alignment: DialogAlignment.Bottom, // 对话框从底部弹出
cornerRadius: 0,
customStyle: true,
openAnimation: { duration: 0.001, tempo: 0 }, // 打开弹窗时动画设置
closeAnimation: { duration: 0.001, tempo: 0 }, // 关闭弹窗时动画设置
});

build() {
Column() {
// 点击按钮打开对话框
Text(this.message)
.onClick(() => {
this.customDialogController.open(); // 触发对话框显示
})
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold);
}
.height('100%')
.width('100%');
}
}
效果预览:

点击放大

方案二:自定义工具栏组件,借助堆叠容器Stack进行页面布局,将组件设置为底部横向居中。定义状态变量isVisible以控制自定义工具栏的显示与隐藏:在TextInput组件聚焦时显示,失焦时隐藏。同时,通过on('keyboardHeightChange')方法监听软键盘高度变化,依据键盘高度动态调整自定义工具栏的位置,使其紧贴于键盘上方。
import { window } from '@kit.ArkUI';

@Entry
@Component
struct Index {
message: string = '图文信息区域';
// 控制自定义工具栏组件可见性
@State isVisible: Visibility = Visibility.Hidden;
// 存储键盘高度信息
@State keyboardHeight: string = '0px';
controller: RichEditorController = new RichEditorController();

// 页面加载时监听键盘高度变化
aboutToAppear(): void {
window.getLastWindow(this.getUIContext().getHostContext()).then((win) => {
win.on('keyboardHeightChange', (height) => {
console.info('height' + height.toString());
if (height) {
this.keyboardHeight = height.toString() + 'px'; // 保存键盘高度
this.isVisible = Visibility.Visible; // 显示自定义组件
} else {
this.isVisible = Visibility.Hidden; // 隐藏自定义组件
}
});
});
}

build() {
// 使用Stack布局,底部对齐
Stack({ alignContent: Alignment.Bottom }) {
Column() {
Text(this.message)
.fontSize(52)
.height('90%')
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black);
// 操作按钮行
Row() {
Text('点击拉起键盘')
.fontColor(Color.White)
.width('90%')
.height('40')
.backgroundColor('#ff1a73f8')
.borderRadius(20)
.textAlign(TextAlign.Center)
.margin({ left: 16, right: 16 })
.onClick(() => {
this.isVisible = Visibility.Visible; // 显示自定义组件
setTimeout(() => {
focusControl.requestFocus('textArea'); // 焦点切换到输入框
}, 10);
});
}.height('10%').width('100%');
}
.expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.BOTTOM]); // 适配键盘区域

  // 遮罩层(点击隐藏自定义)  
  Column()  
    .width('100%')  
    .height('120%')  
    .opacity(0.5)  
    .backgroundColor('rgba(0,0,0,0.5)')  
    .onClick(() => {  
      this.isVisible = Visibility.Hidden; // 隐藏自定义组件  
    })  
    .visibility(this.isVisible)  
    .expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.BOTTOM, SafeAreaEdge.TOP]);  
  // 动态加载自定义组件  
  CommentComponent({ isVisible: this.isVisible, keyboardHeight: this.keyboardHeight });  
};  

}

// 页面卸载时隐藏自定义组件
aboutToDisappear(): void {
this.isVisible = Visibility.Hidden;
}
}

// 自定义工具栏组件
@Component
struct CommentComponent {
controller: RichEditorController = new RichEditorController();
@Link isVisible: Visibility;
@Link keyboardHeight: string;

build() {
Flex({ direction: FlexDirection.ColumnReverse, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Stretch }) {
Row() {
RichEditor({ controller: this.controller })
.id('textArea') // 设置输入框ID用于焦点控制
.placeholder('喜欢就评论一下吧~', { fontColor: Color.Gray, font: { size: 16, weight: FontWeight.Normal } })
.width('65%');
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.End, alignItems: ItemAlign.Stretch }) {
Image($r('app.media.startIcon')).height(20).constraintSize({ minWidth: 20, maxWidth: 20 });
}
.width(110);
}
.margin({
top: 10,
bottom: this.keyboardHeight, // 根据键盘高度设置动态调整底部边距
left: 10,
right: 10
})
.border({ width: 1 })
.borderRadius(50);
}
.width('100%')
.height(380)
.expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.BOTTOM, SafeAreaEdge.TOP]) // 适配键盘区域
.backgroundColor(Color.White)
.visibility(this.isVisible);
}
}
https://pastebin.com/6KBPeVqL
https://pastebin.com/bN1ddiZp
https://pastebin.com/gtYW43NW
https://pastebin.com/eJcix7ad
https://pastebin.com/HcrRQFtA
https://pastebin.com/XgyPuxHD
https://pastebin.com/263D2tG3
https://pastebin.com/VFXnShya
https://pastebin.com/JcCfedC7
https://pastebin.com/XgiWLKtb
https://pastebin.com/SnA9AdtL
https://pastebin.com/gXXg0rwX
https://pastebin.com/6JGUtchA
https://pastebin.com/ZbTwd0Ua
https://pastebin.com/0s559jyS
https://pastebin.com/FF4bCbp7
https://pastebin.com/rbJciCS7
https://pastebin.com/fMBrbeYx
https://pastebin.com/3ZzHtZqE
https://pastebin.com/xgZc6Wpx
https://pastebin.com/q9MHz0sP
https://pastebin.com/7BGqgqgb
https://pastebin.com/762Emz0H
https://pastebin.com/KS7UgY63
https://pastebin.com/E0CUmHyN
https://pastebin.com/RaxmNbrv
https://pastebin.com/sWGhM2xB
https://pastebin.com/q4USiCDw
https://pastebin.com/XBjfa0Jv
https://pastebin.com/5R6CQqw3
https://pastebin.com/Q7C1fgyH
https://pastebin.com/fBtA7KM6
https://pastebin.com/dZHaQ1uQ
https://pastebin.com/YEKBNnfc
https://pastebin.com/T3fNdkNE
https://pastebin.com/r2YxuAHc
https://pastebin.com/Y1Fzshsu
https://pastebin.com/KJvxU2vN
https://pastebin.com/57egjdLv
https://pastebin.com/VxNKdCta
https://pastebin.com/3YsNKCSd
https://pastebin.com/Wk06JeP9
https://pastebin.com/T2pGjSNZ
https://pastebin.com/M0kVVxRE
https://pastebin.com/7kAYYh9u
https://pastebin.com/uiKRsuLh
https://pastebin.com/XLg7wEiV
https://pastebin.com/Uk7dPXfC
https://pastebin.com/HXaxi2bh
https://pastebin.com/2stztMET
https://pastebin.com/RxKisB4V
https://pastebin.com/ig8KqDGg
https://pastebin.com/cRQ0yb3J
https://pastebin.com/4hR7SdmD
https://pastebin.com/EwmMgWdr
https://pastebin.com/4b5AFeRk
https://pastebin.com/3CFxLac5
https://pastebin.com/mKn2fjXp
https://pastebin.com/BSa8L13Z
https://pastebin.com/NAtqrReY
https://pastebin.com/34KDinEG
https://pastebin.com/YaXZqyZQ
https://pastebin.com/j2MhC6kw
https://pastebin.com/c1n5dn5T
https://pastebin.com/weATT6j7
https://pastebin.com/zV5bp85z
https://pastebin.com/isfmyWk7
https://pastebin.com/S86UAeMg
https://pastebin.com/XkUgvZQW
https://pastebin.com/v9j6jg3T
https://pastebin.com/3ip0M9T8
https://pastebin.com/W9c1w8Xb
https://pastebin.com/jtcypgTt
https://pastebin.com/tgPhxe2L
https://pastebin.com/bs9jzpuL
https://pastebin.com/W8sLLPtk
https://pastebin.com/gJTGQYCL
https://pastebin.com/Qh2u2nkn
https://pastebin.com/EPEHmxwS
https://pastebin.com/y41haJUr
https://pastebin.com/J6cevDf0
https://pastebin.com/24RSFBR0
https://pastebin.com/KZsSaA18
https://pastebin.com/21egpH32
https://pastebin.com/wDziwJnn
https://pastebin.com/QeQvkA9r
https://pastebin.com/icJRn8VJ
https://pastebin.com/WsvP4LwZ
https://pastebin.com/H8nZikEJ
https://pastebin.com/rn2gyWiP
https://pastebin.com/tfehkve3
https://pastebin.com/kn7dX8Fh
https://pastebin.com/Dn1ZfNEV
https://pastebin.com/yy811Lis
https://pastebin.com/QcqL3CXH
https://pastebin.com/QbMe6xXP
https://pastebin.com/NpR2Ra0H
https://pastebin.com/DeevkS6j
https://pastebin.com/yhumdvyC
https://pastebin.com/mHjSzVib
https://pastebin.com/F2wyRKNJ
https://pastebin.com/cc96TLXV
https://pastebin.com/2U1qwTrS
https://pastebin.com/n6hYfTdQ
https://pastebin.com/rhAFXjq6
https://pastebin.com/Hmj59VVm
https://pastebin.com/YmrAxXMU
https://pastebin.com/JBF9n5PL
https://pastebin.com/f21MNNJt
https://pastebin.com/D1hn89Qa
https://pastebin.com/xqdaxD0A
https://pastebin.com/tm2zPQbn
https://pastebin.com/8j3PBMNW
https://pastebin.com/3LcqrmTu
https://pastebin.com/GY4abWzw
https://pastebin.com/9LnRRWuk
https://pastebin.com/N5HJByTK
https://pastebin.com/aaSvnC3j
https://pastebin.com/UYFUHSWk
https://pastebin.com/fcvPwAqU
https://pastebin.com/fS9cgSGG
https://pastebin.com/NP5mQWhJ
https://pastebin.com/em54RVwp
https://pastebin.com/FhSzXrRD
https://pastebin.com/EaU2AmjN
https://pastebin.com/86fScibj
https://pastebin.com/rgPcewwp
https://pastebin.com/FAhu2G96
https://pastebin.com/fU6un6pq
https://pastebin.com/Fmpvt6K9
https://pastebin.com/DMJHzHuW
https://pastebin.com/F39T4W12
https://pastebin.com/9wGgKwMC
https://pastebin.com/xJXUSDnS
https://pastebin.com/FqUGuWTV
https://pastebin.com/Z3ppjriB
https://pastebin.com/A2AQwvEd
https://pastebin.com/tqBWPMe7
https://pastebin.com/LQ3LFEHs
https://pastebin.com/qgkbKhDs
https://pastebin.com/vNvhLWpB
https://pastebin.com/BYu5Twtq
https://pastebin.com/9MapWu1K
https://pastebin.com/DUS2V33c
https://pastebin.com/WSuFmrmW
https://pastebin.com/vKwBpE8f
https://pastebin.com/ARLCtkJy
https://pastebin.com/R0eRbYXX
https://pastebin.com/vzusjTqP
https://pastebin.com/6xQWzuPp
https://pastebin.com/XZsLs6N3
https://pastebin.com/ccCGb1c1
https://pastebin.com/GbxBPVJ6
https://pastebin.com/c6rWM7yT
https://pastebin.com/0fhv7SWU
https://pastebin.com/LCXSxwqu
https://pastebin.com/UKiEaw1b
https://pastebin.com/rR7WFgRd
https://pastebin.com/GMr8Uit3
https://pastebin.com/EeUAEUzt
https://pastebin.com/iMdgtEvx
https://pastebin.com/M54w1AB8
https://pastebin.com/NBenUCdx
https://pastebin.com/gcYXCccc
https://pastebin.com/VKbbyQWT
https://pastebin.com/AjjbCsHT
https://pastebin.com/95MGXGGW
https://pastebin.com/MGSQGvHt
https://pastebin.com/8vVDgwEX
https://pastebin.com/xg8NhUUu
https://pastebin.com/ejAPdK9h
https://pastebin.com/HC44DqUE
https://pastebin.com/AAERa624
https://pastebin.com/TrqEhKJ0
https://pastebin.com/PhRqJLQH
https://pastebin.com/s10g8uCx
https://pastebin.com/a3RKhwkq
https://pastebin.com/PEsA2ngB

继续阅读 »

问题现象
跟随键盘的工具栏如何实现?

背景知识
自定义弹窗(CustomDialog):通过CustomDialogController类显示自定义弹窗。使用弹窗组件时,优先考虑自定义弹窗,便于弹窗样式与内容的自定义。
defaultFocus:设置当前组件是否为当前页面上的默认焦点。
on('keyboardHeightChange'):开启固定态软键盘高度变化的监听,当软键盘由本窗口唤出并存在重叠区域时通知键盘高度变化。
Stack:堆叠容器,子组件按照顺序依次入栈,后一个子组件覆盖前一个子组件。
alignContent:设置子组件在容器内的对齐方式。该属性与通用属性align同时设置时,后设置的属性生效。
解决方案
针对跟随键盘的工具栏,可采用以下两种实现方案:

方案一:使用CustomDialog组件自定义工具栏。以下示例通过点击文本组件使弹窗自动获焦,设置弹窗从底部弹出,并为弹窗添加弹出与关闭时的过渡动画,以实现跟随键盘的动画效果。
// 自定义对话框组件,用于显示输入框
@CustomDialog
@Component
export struct Dialog {
dialogController: CustomDialogController;

build() {
Column() {
TextInput({ text: '', placeholder: 'input your word...' })
.type(InputType.Normal)
.defaultFocus(true) // 设置为默认聚焦状态
.onBlur(() => {
// 输入框失去焦点时关闭对话框
this.dialogController.close();
})
.height('50%')
.margin({ top: 25, left: 16, right: 16 });
}
.height(100)
.backgroundColor(Color.White);
}
}

@Entry
@Component
struct Index {
message: string = 'Hello World';
// 自定义对话框控制器,配置对话框的显示参数
customDialogController: CustomDialogController = new CustomDialogController({
builder: Dialog(), // 设置对话框内容组件
alignment: DialogAlignment.Bottom, // 对话框从底部弹出
cornerRadius: 0,
customStyle: true,
openAnimation: { duration: 0.001, tempo: 0 }, // 打开弹窗时动画设置
closeAnimation: { duration: 0.001, tempo: 0 }, // 关闭弹窗时动画设置
});

build() {
Column() {
// 点击按钮打开对话框
Text(this.message)
.onClick(() => {
this.customDialogController.open(); // 触发对话框显示
})
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold);
}
.height('100%')
.width('100%');
}
}
效果预览:

点击放大

方案二:自定义工具栏组件,借助堆叠容器Stack进行页面布局,将组件设置为底部横向居中。定义状态变量isVisible以控制自定义工具栏的显示与隐藏:在TextInput组件聚焦时显示,失焦时隐藏。同时,通过on('keyboardHeightChange')方法监听软键盘高度变化,依据键盘高度动态调整自定义工具栏的位置,使其紧贴于键盘上方。
import { window } from '@kit.ArkUI';

@Entry
@Component
struct Index {
message: string = '图文信息区域';
// 控制自定义工具栏组件可见性
@State isVisible: Visibility = Visibility.Hidden;
// 存储键盘高度信息
@State keyboardHeight: string = '0px';
controller: RichEditorController = new RichEditorController();

// 页面加载时监听键盘高度变化
aboutToAppear(): void {
window.getLastWindow(this.getUIContext().getHostContext()).then((win) => {
win.on('keyboardHeightChange', (height) => {
console.info('height' + height.toString());
if (height) {
this.keyboardHeight = height.toString() + 'px'; // 保存键盘高度
this.isVisible = Visibility.Visible; // 显示自定义组件
} else {
this.isVisible = Visibility.Hidden; // 隐藏自定义组件
}
});
});
}

build() {
// 使用Stack布局,底部对齐
Stack({ alignContent: Alignment.Bottom }) {
Column() {
Text(this.message)
.fontSize(52)
.height('90%')
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black);
// 操作按钮行
Row() {
Text('点击拉起键盘')
.fontColor(Color.White)
.width('90%')
.height('40')
.backgroundColor('#ff1a73f8')
.borderRadius(20)
.textAlign(TextAlign.Center)
.margin({ left: 16, right: 16 })
.onClick(() => {
this.isVisible = Visibility.Visible; // 显示自定义组件
setTimeout(() => {
focusControl.requestFocus('textArea'); // 焦点切换到输入框
}, 10);
});
}.height('10%').width('100%');
}
.expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.BOTTOM]); // 适配键盘区域

  // 遮罩层(点击隐藏自定义)  
  Column()  
    .width('100%')  
    .height('120%')  
    .opacity(0.5)  
    .backgroundColor('rgba(0,0,0,0.5)')  
    .onClick(() => {  
      this.isVisible = Visibility.Hidden; // 隐藏自定义组件  
    })  
    .visibility(this.isVisible)  
    .expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.BOTTOM, SafeAreaEdge.TOP]);  
  // 动态加载自定义组件  
  CommentComponent({ isVisible: this.isVisible, keyboardHeight: this.keyboardHeight });  
};  

}

// 页面卸载时隐藏自定义组件
aboutToDisappear(): void {
this.isVisible = Visibility.Hidden;
}
}

// 自定义工具栏组件
@Component
struct CommentComponent {
controller: RichEditorController = new RichEditorController();
@Link isVisible: Visibility;
@Link keyboardHeight: string;

build() {
Flex({ direction: FlexDirection.ColumnReverse, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Stretch }) {
Row() {
RichEditor({ controller: this.controller })
.id('textArea') // 设置输入框ID用于焦点控制
.placeholder('喜欢就评论一下吧~', { fontColor: Color.Gray, font: { size: 16, weight: FontWeight.Normal } })
.width('65%');
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.End, alignItems: ItemAlign.Stretch }) {
Image($r('app.media.startIcon')).height(20).constraintSize({ minWidth: 20, maxWidth: 20 });
}
.width(110);
}
.margin({
top: 10,
bottom: this.keyboardHeight, // 根据键盘高度设置动态调整底部边距
left: 10,
right: 10
})
.border({ width: 1 })
.borderRadius(50);
}
.width('100%')
.height(380)
.expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.BOTTOM, SafeAreaEdge.TOP]) // 适配键盘区域
.backgroundColor(Color.White)
.visibility(this.isVisible);
}
}
https://pastebin.com/6KBPeVqL
https://pastebin.com/bN1ddiZp
https://pastebin.com/gtYW43NW
https://pastebin.com/eJcix7ad
https://pastebin.com/HcrRQFtA
https://pastebin.com/XgyPuxHD
https://pastebin.com/263D2tG3
https://pastebin.com/VFXnShya
https://pastebin.com/JcCfedC7
https://pastebin.com/XgiWLKtb
https://pastebin.com/SnA9AdtL
https://pastebin.com/gXXg0rwX
https://pastebin.com/6JGUtchA
https://pastebin.com/ZbTwd0Ua
https://pastebin.com/0s559jyS
https://pastebin.com/FF4bCbp7
https://pastebin.com/rbJciCS7
https://pastebin.com/fMBrbeYx
https://pastebin.com/3ZzHtZqE
https://pastebin.com/xgZc6Wpx
https://pastebin.com/q9MHz0sP
https://pastebin.com/7BGqgqgb
https://pastebin.com/762Emz0H
https://pastebin.com/KS7UgY63
https://pastebin.com/E0CUmHyN
https://pastebin.com/RaxmNbrv
https://pastebin.com/sWGhM2xB
https://pastebin.com/q4USiCDw
https://pastebin.com/XBjfa0Jv
https://pastebin.com/5R6CQqw3
https://pastebin.com/Q7C1fgyH
https://pastebin.com/fBtA7KM6
https://pastebin.com/dZHaQ1uQ
https://pastebin.com/YEKBNnfc
https://pastebin.com/T3fNdkNE
https://pastebin.com/r2YxuAHc
https://pastebin.com/Y1Fzshsu
https://pastebin.com/KJvxU2vN
https://pastebin.com/57egjdLv
https://pastebin.com/VxNKdCta
https://pastebin.com/3YsNKCSd
https://pastebin.com/Wk06JeP9
https://pastebin.com/T2pGjSNZ
https://pastebin.com/M0kVVxRE
https://pastebin.com/7kAYYh9u
https://pastebin.com/uiKRsuLh
https://pastebin.com/XLg7wEiV
https://pastebin.com/Uk7dPXfC
https://pastebin.com/HXaxi2bh
https://pastebin.com/2stztMET
https://pastebin.com/RxKisB4V
https://pastebin.com/ig8KqDGg
https://pastebin.com/cRQ0yb3J
https://pastebin.com/4hR7SdmD
https://pastebin.com/EwmMgWdr
https://pastebin.com/4b5AFeRk
https://pastebin.com/3CFxLac5
https://pastebin.com/mKn2fjXp
https://pastebin.com/BSa8L13Z
https://pastebin.com/NAtqrReY
https://pastebin.com/34KDinEG
https://pastebin.com/YaXZqyZQ
https://pastebin.com/j2MhC6kw
https://pastebin.com/c1n5dn5T
https://pastebin.com/weATT6j7
https://pastebin.com/zV5bp85z
https://pastebin.com/isfmyWk7
https://pastebin.com/S86UAeMg
https://pastebin.com/XkUgvZQW
https://pastebin.com/v9j6jg3T
https://pastebin.com/3ip0M9T8
https://pastebin.com/W9c1w8Xb
https://pastebin.com/jtcypgTt
https://pastebin.com/tgPhxe2L
https://pastebin.com/bs9jzpuL
https://pastebin.com/W8sLLPtk
https://pastebin.com/gJTGQYCL
https://pastebin.com/Qh2u2nkn
https://pastebin.com/EPEHmxwS
https://pastebin.com/y41haJUr
https://pastebin.com/J6cevDf0
https://pastebin.com/24RSFBR0
https://pastebin.com/KZsSaA18
https://pastebin.com/21egpH32
https://pastebin.com/wDziwJnn
https://pastebin.com/QeQvkA9r
https://pastebin.com/icJRn8VJ
https://pastebin.com/WsvP4LwZ
https://pastebin.com/H8nZikEJ
https://pastebin.com/rn2gyWiP
https://pastebin.com/tfehkve3
https://pastebin.com/kn7dX8Fh
https://pastebin.com/Dn1ZfNEV
https://pastebin.com/yy811Lis
https://pastebin.com/QcqL3CXH
https://pastebin.com/QbMe6xXP
https://pastebin.com/NpR2Ra0H
https://pastebin.com/DeevkS6j
https://pastebin.com/yhumdvyC
https://pastebin.com/mHjSzVib
https://pastebin.com/F2wyRKNJ
https://pastebin.com/cc96TLXV
https://pastebin.com/2U1qwTrS
https://pastebin.com/n6hYfTdQ
https://pastebin.com/rhAFXjq6
https://pastebin.com/Hmj59VVm
https://pastebin.com/YmrAxXMU
https://pastebin.com/JBF9n5PL
https://pastebin.com/f21MNNJt
https://pastebin.com/D1hn89Qa
https://pastebin.com/xqdaxD0A
https://pastebin.com/tm2zPQbn
https://pastebin.com/8j3PBMNW
https://pastebin.com/3LcqrmTu
https://pastebin.com/GY4abWzw
https://pastebin.com/9LnRRWuk
https://pastebin.com/N5HJByTK
https://pastebin.com/aaSvnC3j
https://pastebin.com/UYFUHSWk
https://pastebin.com/fcvPwAqU
https://pastebin.com/fS9cgSGG
https://pastebin.com/NP5mQWhJ
https://pastebin.com/em54RVwp
https://pastebin.com/FhSzXrRD
https://pastebin.com/EaU2AmjN
https://pastebin.com/86fScibj
https://pastebin.com/rgPcewwp
https://pastebin.com/FAhu2G96
https://pastebin.com/fU6un6pq
https://pastebin.com/Fmpvt6K9
https://pastebin.com/DMJHzHuW
https://pastebin.com/F39T4W12
https://pastebin.com/9wGgKwMC
https://pastebin.com/xJXUSDnS
https://pastebin.com/FqUGuWTV
https://pastebin.com/Z3ppjriB
https://pastebin.com/A2AQwvEd
https://pastebin.com/tqBWPMe7
https://pastebin.com/LQ3LFEHs
https://pastebin.com/qgkbKhDs
https://pastebin.com/vNvhLWpB
https://pastebin.com/BYu5Twtq
https://pastebin.com/9MapWu1K
https://pastebin.com/DUS2V33c
https://pastebin.com/WSuFmrmW
https://pastebin.com/vKwBpE8f
https://pastebin.com/ARLCtkJy
https://pastebin.com/R0eRbYXX
https://pastebin.com/vzusjTqP
https://pastebin.com/6xQWzuPp
https://pastebin.com/XZsLs6N3
https://pastebin.com/ccCGb1c1
https://pastebin.com/GbxBPVJ6
https://pastebin.com/c6rWM7yT
https://pastebin.com/0fhv7SWU
https://pastebin.com/LCXSxwqu
https://pastebin.com/UKiEaw1b
https://pastebin.com/rR7WFgRd
https://pastebin.com/GMr8Uit3
https://pastebin.com/EeUAEUzt
https://pastebin.com/iMdgtEvx
https://pastebin.com/M54w1AB8
https://pastebin.com/NBenUCdx
https://pastebin.com/gcYXCccc
https://pastebin.com/VKbbyQWT
https://pastebin.com/AjjbCsHT
https://pastebin.com/95MGXGGW
https://pastebin.com/MGSQGvHt
https://pastebin.com/8vVDgwEX
https://pastebin.com/xg8NhUUu
https://pastebin.com/ejAPdK9h
https://pastebin.com/HC44DqUE
https://pastebin.com/AAERa624
https://pastebin.com/TrqEhKJ0
https://pastebin.com/PhRqJLQH
https://pastebin.com/s10g8uCx
https://pastebin.com/a3RKhwkq
https://pastebin.com/PEsA2ngB

收起阅读 »

跳转其他页面返回时,如何设置Tabs的默认显示页面

问题现象
使用Tabs组件,如何实现当前显示页签为1的页面内容,点击某个页签使用router跳转到其他页面再返回时,显示的依然是页签为1的页面内容?

效果预览
点击放大

背景知识
onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件作为页面时生效。
Tabs:通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
getRouter().pushUrl():跳转到应用内的指定页面,通过Promise获取跳转异常的返回结果。
解决方案
@Entry修饰的页面在显示时会触发onPageShow生命周期回调,可以在此回调中设置页面显示的初始状态,Tabs组件显示具体哪个页面由Tabs内的currentIndex参数决定的,当页面返回时在onPageShow()方法内重设currentIndex的值使其显示对应页面。运行下述示例需要自行创建一个简单的PageA页面。

class TabBar {
title: string;
index: number;

constructor(title: string, index: number) {
this.title = title;
this.index = index;
}
}

@Entry
@Component
struct TabsTestPage {
uiContext = this.getUIContext();
// 当前选中Tabs的索引
@State currentIndex: number = 1;
// 判断Tabs是否选中(用于自定义Tabs列表的选中状态)

@State selectedIndex: number = 0;
private tabsController: TabsController = new TabsController();
private tabBars: TabBar[] = [
new TabBar('翻译机', 0),
new TabBar('首页', 1),
new TabBar('推荐', 2),
];

// 页面显示时初始化状态
onPageShow(): void {
this.currentIndex = 1;
}

// 自定义Tabs组件构建函数
@Builder
TabBuilder() {
List() {
ForEach(this.tabBars, (item: TabBar, index: number) => {
ListItem() {
Column() {
Text(item.title) // 根据选中状态改变文字颜色
.fontColor(this.currentIndex === item.index ? '#0A59F7' : Color.Black)
.fontSize(20)
.align(Alignment.Center);
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
.onClick(() => {
// 更新Tabs组件的选中状态
this.currentIndex = index;
});
}.height(100);
});
}.height(100)
.listDirection(Axis.Horizontal)
.scrollBar(BarState.Off);
}

build() {
Column() {
Flex({ alignItems: ItemAlign.Center }) {
this.TabBuilder();
}.width('100%').height(100);

  Tabs({ barPosition: BarPosition.Start, index: this.currentIndex, controller: this.tabsController }) {  
    TabContent() {  
      Text('翻译机的内容')  
        .fontSize(30)  
        .onClick(() => {  
          let promptShow = this.uiContext.getPromptAction();  
          promptShow.showToast({  
            message: '翻译机跳转'  
          });  
          // 需要自行创建一个PageA的@Entry页面  
          this.uiContext.getRouter().pushUrl({ url: 'pages/PageA' });  
        });  
    };  

    TabContent() {  
      Text('首页的内容')  
        .fontSize(30);  
    };  

    TabContent() {  
      Text('推荐的内容')  
        .fontSize(30);  
    };  
  }.barHeight(0)  
  .onAnimationStart((targetIndex: number) => {  
    this.currentIndex = targetIndex;  
  })  
  .onChange((index: number) => {  
    // currentIndex控制TabContent显示页签  
    this.currentIndex = index;  
    this.selectedIndex = index;  
  });  

}.height('100%').width('100%');  

}
}
总结
单例模式跳转时,由于也是复用路由栈内已有的页面实例,也可在本方案所述的页面生命周期内实现。
https://pastebin.com/dty8sxD5
https://pastebin.com/dHEXCxFt
https://pastebin.com/pE6ePUAY
https://pastebin.com/2U1LgwWz
https://pastebin.com/WQpk7jSa
https://pastebin.com/G8LPQVtP
https://pastebin.com/VRpjsRTK
https://pastebin.com/aCy0qhBi
https://pastebin.com/NFQMuiXH
https://pastebin.com/S064raxa
https://pastebin.com/4s7w0BBW
https://pastebin.com/m7UDFUJ6
https://pastebin.com/UBeiNw6e
https://pastebin.com/kms3qfYM
https://pastebin.com/dYjjH5yP
https://pastebin.com/6cvgDvgL
https://pastebin.com/KPmn6RCf
https://pastebin.com/DiPdkuXM
https://pastebin.com/53nPeHWZ
https://pastebin.com/WNdjQBtb
https://pastebin.com/gD3BbuEt
https://pastebin.com/K1djmuru
https://pastebin.com/uqMvn9Uq
https://pastebin.com/paibxsw8
https://pastebin.com/Una68Ba5
https://pastebin.com/P7CrcBfR
https://pastebin.com/UiQ5wXCw
https://pastebin.com/LYHMErrw
https://pastebin.com/T3Fjh1Er
https://pastebin.com/m7Zx61yT
https://pastebin.com/dRmeRwsB
https://pastebin.com/vN5zyV7U
https://pastebin.com/hqms8PnB
https://pastebin.com/VJJ9tacg
https://pastebin.com/gnHE24Vx
https://pastebin.com/2DN8jUYE
https://pastebin.com/NYYLtSrA
https://pastebin.com/FJcvCjYv
https://pastebin.com/C50vS8E5
https://pastebin.com/x8G6uZzB
https://pastebin.com/xwqDW20Q
https://pastebin.com/bbrPiHh4
https://pastebin.com/KwcxyrV3
https://pastebin.com/8Yq1Mwpv
https://pastebin.com/rdXkVZnR
https://pastebin.com/X1QpPxj6
https://pastebin.com/rk4jhKwM
https://pastebin.com/AYH757Hd
https://pastebin.com/arrJtdPC
https://pastebin.com/x1a0QkrE
https://pastebin.com/ZPzMV6KV
https://pastebin.com/qZPAArab
https://pastebin.com/dDiiWq7r
https://pastebin.com/wH1CZA3w
https://pastebin.com/9CHyNi7a
https://pastebin.com/Wkzwp2vS
https://pastebin.com/MVpzkpRy
https://pastebin.com/4AuW9a4S
https://pastebin.com/42tkYqcy
https://pastebin.com/XeksS1ku
https://pastebin.com/L110ndKg
https://pastebin.com/4Axgsuj0
https://pastebin.com/eAA2mxp4
https://pastebin.com/aU4WMaqg
https://pastebin.com/f1hun2kM
https://pastebin.com/wiqMJgk3
https://pastebin.com/uEN8vUKX
https://pastebin.com/D5qiKNFB
https://pastebin.com/F65rvR1J
https://pastebin.com/rasLrbYn
https://pastebin.com/21n3MQq5
https://pastebin.com/bXDj2phg
https://pastebin.com/0PVahXVh
https://pastebin.com/7JtdfNGH
https://pastebin.com/0986iXre
https://pastebin.com/NUb3eRVe
https://pastebin.com/gvz7gKTZ
https://pastebin.com/UusP3Pyp
https://pastebin.com/QSMmWQi9
https://pastebin.com/jLtF7m6h
https://pastebin.com/csKNzJ6w
https://pastebin.com/wBeZafkw
https://pastebin.com/KESDZmpi
https://pastebin.com/mppjXxUF
https://pastebin.com/MepfrkCE
https://pastebin.com/ibwAybQc
https://pastebin.com/WuELbpHn
https://pastebin.com/hTM9NQ5N
https://pastebin.com/jj0Cp5Ux
https://pastebin.com/LzZFQwaZ
https://pastebin.com/tY8wgRRh
https://pastebin.com/XhgJKNVx
https://pastebin.com/zmytyHRD
https://pastebin.com/ps3FBe7w
https://pastebin.com/hKEiN00Z
https://pastebin.com/0krAGqa8
https://pastebin.com/bmN1vmY5
https://pastebin.com/tXEzETbZ
https://pastebin.com/sxvMRT7d
https://pastebin.com/PBYx1zjX
https://pastebin.com/YL3YtvW4
https://pastebin.com/wYkL6tyV
https://pastebin.com/G9yg2jbz
https://pastebin.com/YS9qiTPE
https://pastebin.com/zRzdakmk
https://pastebin.com/cPbmqAwR
https://pastebin.com/k6VnHXDj
https://pastebin.com/xaU4Lde8
https://pastebin.com/T3qhtZbi
https://pastebin.com/ti5Gv5Uy
https://pastebin.com/p9B6cLzp
https://pastebin.com/0McP0kzG
https://pastebin.com/QRsCLz7H
https://pastebin.com/rTnmVGim
https://pastebin.com/dirbSPyj
https://pastebin.com/QJ1gdh7k
https://pastebin.com/qgCPAp80
https://pastebin.com/H6YUkDVV
https://pastebin.com/YzEYse2d
https://pastebin.com/kUbmfLr0
https://pastebin.com/DmfhXUWK
https://pastebin.com/rxfuCib1
https://pastebin.com/wCR9BHRT
https://pastebin.com/rfJqDQph
https://pastebin.com/UVXE7g9P
https://pastebin.com/vB8FW842
https://pastebin.com/93JiaU8B
https://pastebin.com/40fKbQF1
https://pastebin.com/UpbtX9Vs
https://pastebin.com/GwhrB2rt
https://pastebin.com/CMt33cpf
https://pastebin.com/qPsRkEzz
https://pastebin.com/RR7a27Wk
https://pastebin.com/zU5CC8qs
https://pastebin.com/D3aJGYQk
https://pastebin.com/hx9ya1cr
https://pastebin.com/2GgcbCuP
https://pastebin.com/gYczTsfK
https://pastebin.com/VXsw9xTt
https://pastebin.com/R7DhK5Tq
https://pastebin.com/hHKGpdvA
https://pastebin.com/xREcgGy2

继续阅读 »

问题现象
使用Tabs组件,如何实现当前显示页签为1的页面内容,点击某个页签使用router跳转到其他页面再返回时,显示的依然是页签为1的页面内容?

效果预览
点击放大

背景知识
onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件作为页面时生效。
Tabs:通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
getRouter().pushUrl():跳转到应用内的指定页面,通过Promise获取跳转异常的返回结果。
解决方案
@Entry修饰的页面在显示时会触发onPageShow生命周期回调,可以在此回调中设置页面显示的初始状态,Tabs组件显示具体哪个页面由Tabs内的currentIndex参数决定的,当页面返回时在onPageShow()方法内重设currentIndex的值使其显示对应页面。运行下述示例需要自行创建一个简单的PageA页面。

class TabBar {
title: string;
index: number;

constructor(title: string, index: number) {
this.title = title;
this.index = index;
}
}

@Entry
@Component
struct TabsTestPage {
uiContext = this.getUIContext();
// 当前选中Tabs的索引
@State currentIndex: number = 1;
// 判断Tabs是否选中(用于自定义Tabs列表的选中状态)

@State selectedIndex: number = 0;
private tabsController: TabsController = new TabsController();
private tabBars: TabBar[] = [
new TabBar('翻译机', 0),
new TabBar('首页', 1),
new TabBar('推荐', 2),
];

// 页面显示时初始化状态
onPageShow(): void {
this.currentIndex = 1;
}

// 自定义Tabs组件构建函数
@Builder
TabBuilder() {
List() {
ForEach(this.tabBars, (item: TabBar, index: number) => {
ListItem() {
Column() {
Text(item.title) // 根据选中状态改变文字颜色
.fontColor(this.currentIndex === item.index ? '#0A59F7' : Color.Black)
.fontSize(20)
.align(Alignment.Center);
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
.onClick(() => {
// 更新Tabs组件的选中状态
this.currentIndex = index;
});
}.height(100);
});
}.height(100)
.listDirection(Axis.Horizontal)
.scrollBar(BarState.Off);
}

build() {
Column() {
Flex({ alignItems: ItemAlign.Center }) {
this.TabBuilder();
}.width('100%').height(100);

  Tabs({ barPosition: BarPosition.Start, index: this.currentIndex, controller: this.tabsController }) {  
    TabContent() {  
      Text('翻译机的内容')  
        .fontSize(30)  
        .onClick(() => {  
          let promptShow = this.uiContext.getPromptAction();  
          promptShow.showToast({  
            message: '翻译机跳转'  
          });  
          // 需要自行创建一个PageA的@Entry页面  
          this.uiContext.getRouter().pushUrl({ url: 'pages/PageA' });  
        });  
    };  

    TabContent() {  
      Text('首页的内容')  
        .fontSize(30);  
    };  

    TabContent() {  
      Text('推荐的内容')  
        .fontSize(30);  
    };  
  }.barHeight(0)  
  .onAnimationStart((targetIndex: number) => {  
    this.currentIndex = targetIndex;  
  })  
  .onChange((index: number) => {  
    // currentIndex控制TabContent显示页签  
    this.currentIndex = index;  
    this.selectedIndex = index;  
  });  

}.height('100%').width('100%');  

}
}
总结
单例模式跳转时,由于也是复用路由栈内已有的页面实例,也可在本方案所述的页面生命周期内实现。
https://pastebin.com/dty8sxD5
https://pastebin.com/dHEXCxFt
https://pastebin.com/pE6ePUAY
https://pastebin.com/2U1LgwWz
https://pastebin.com/WQpk7jSa
https://pastebin.com/G8LPQVtP
https://pastebin.com/VRpjsRTK
https://pastebin.com/aCy0qhBi
https://pastebin.com/NFQMuiXH
https://pastebin.com/S064raxa
https://pastebin.com/4s7w0BBW
https://pastebin.com/m7UDFUJ6
https://pastebin.com/UBeiNw6e
https://pastebin.com/kms3qfYM
https://pastebin.com/dYjjH5yP
https://pastebin.com/6cvgDvgL
https://pastebin.com/KPmn6RCf
https://pastebin.com/DiPdkuXM
https://pastebin.com/53nPeHWZ
https://pastebin.com/WNdjQBtb
https://pastebin.com/gD3BbuEt
https://pastebin.com/K1djmuru
https://pastebin.com/uqMvn9Uq
https://pastebin.com/paibxsw8
https://pastebin.com/Una68Ba5
https://pastebin.com/P7CrcBfR
https://pastebin.com/UiQ5wXCw
https://pastebin.com/LYHMErrw
https://pastebin.com/T3Fjh1Er
https://pastebin.com/m7Zx61yT
https://pastebin.com/dRmeRwsB
https://pastebin.com/vN5zyV7U
https://pastebin.com/hqms8PnB
https://pastebin.com/VJJ9tacg
https://pastebin.com/gnHE24Vx
https://pastebin.com/2DN8jUYE
https://pastebin.com/NYYLtSrA
https://pastebin.com/FJcvCjYv
https://pastebin.com/C50vS8E5
https://pastebin.com/x8G6uZzB
https://pastebin.com/xwqDW20Q
https://pastebin.com/bbrPiHh4
https://pastebin.com/KwcxyrV3
https://pastebin.com/8Yq1Mwpv
https://pastebin.com/rdXkVZnR
https://pastebin.com/X1QpPxj6
https://pastebin.com/rk4jhKwM
https://pastebin.com/AYH757Hd
https://pastebin.com/arrJtdPC
https://pastebin.com/x1a0QkrE
https://pastebin.com/ZPzMV6KV
https://pastebin.com/qZPAArab
https://pastebin.com/dDiiWq7r
https://pastebin.com/wH1CZA3w
https://pastebin.com/9CHyNi7a
https://pastebin.com/Wkzwp2vS
https://pastebin.com/MVpzkpRy
https://pastebin.com/4AuW9a4S
https://pastebin.com/42tkYqcy
https://pastebin.com/XeksS1ku
https://pastebin.com/L110ndKg
https://pastebin.com/4Axgsuj0
https://pastebin.com/eAA2mxp4
https://pastebin.com/aU4WMaqg
https://pastebin.com/f1hun2kM
https://pastebin.com/wiqMJgk3
https://pastebin.com/uEN8vUKX
https://pastebin.com/D5qiKNFB
https://pastebin.com/F65rvR1J
https://pastebin.com/rasLrbYn
https://pastebin.com/21n3MQq5
https://pastebin.com/bXDj2phg
https://pastebin.com/0PVahXVh
https://pastebin.com/7JtdfNGH
https://pastebin.com/0986iXre
https://pastebin.com/NUb3eRVe
https://pastebin.com/gvz7gKTZ
https://pastebin.com/UusP3Pyp
https://pastebin.com/QSMmWQi9
https://pastebin.com/jLtF7m6h
https://pastebin.com/csKNzJ6w
https://pastebin.com/wBeZafkw
https://pastebin.com/KESDZmpi
https://pastebin.com/mppjXxUF
https://pastebin.com/MepfrkCE
https://pastebin.com/ibwAybQc
https://pastebin.com/WuELbpHn
https://pastebin.com/hTM9NQ5N
https://pastebin.com/jj0Cp5Ux
https://pastebin.com/LzZFQwaZ
https://pastebin.com/tY8wgRRh
https://pastebin.com/XhgJKNVx
https://pastebin.com/zmytyHRD
https://pastebin.com/ps3FBe7w
https://pastebin.com/hKEiN00Z
https://pastebin.com/0krAGqa8
https://pastebin.com/bmN1vmY5
https://pastebin.com/tXEzETbZ
https://pastebin.com/sxvMRT7d
https://pastebin.com/PBYx1zjX
https://pastebin.com/YL3YtvW4
https://pastebin.com/wYkL6tyV
https://pastebin.com/G9yg2jbz
https://pastebin.com/YS9qiTPE
https://pastebin.com/zRzdakmk
https://pastebin.com/cPbmqAwR
https://pastebin.com/k6VnHXDj
https://pastebin.com/xaU4Lde8
https://pastebin.com/T3qhtZbi
https://pastebin.com/ti5Gv5Uy
https://pastebin.com/p9B6cLzp
https://pastebin.com/0McP0kzG
https://pastebin.com/QRsCLz7H
https://pastebin.com/rTnmVGim
https://pastebin.com/dirbSPyj
https://pastebin.com/QJ1gdh7k
https://pastebin.com/qgCPAp80
https://pastebin.com/H6YUkDVV
https://pastebin.com/YzEYse2d
https://pastebin.com/kUbmfLr0
https://pastebin.com/DmfhXUWK
https://pastebin.com/rxfuCib1
https://pastebin.com/wCR9BHRT
https://pastebin.com/rfJqDQph
https://pastebin.com/UVXE7g9P
https://pastebin.com/vB8FW842
https://pastebin.com/93JiaU8B
https://pastebin.com/40fKbQF1
https://pastebin.com/UpbtX9Vs
https://pastebin.com/GwhrB2rt
https://pastebin.com/CMt33cpf
https://pastebin.com/qPsRkEzz
https://pastebin.com/RR7a27Wk
https://pastebin.com/zU5CC8qs
https://pastebin.com/D3aJGYQk
https://pastebin.com/hx9ya1cr
https://pastebin.com/2GgcbCuP
https://pastebin.com/gYczTsfK
https://pastebin.com/VXsw9xTt
https://pastebin.com/R7DhK5Tq
https://pastebin.com/hHKGpdvA
https://pastebin.com/xREcgGy2

收起阅读 »

并发接口拦截器中如何实现全局弹窗

问题现象
在应用中,每个接口都可能返回响应码Code,需要在接口拦截器中实现一个全局弹窗。由于接口是并发的,弹窗只能弹出一次。那么如何实现这个全局弹窗呢?此外,由于弹窗可能会在多个页面弹出(如启动页、登录页、主页等),这些页面可能会被销毁,这会导致弹窗无法正常显示。

效果预览
点击放大 点击放大 点击放大

背景知识
使用弹窗组件时,可优先考虑自定义弹窗,便于自定义弹窗的样式与内容。通过CustomDialogController类显示自定义弹窗,不支持直接在类中定义和使用。通常需要将弹框逻辑封装成Builder或其他组件,以便在需要时调用。
可以使用@StorageLink与AppStorage中的key对应的属性建立双向数据同步,该属性可以和UI组件同步,且可以在应用业务逻辑中被访问。
解决方案
在并发接口拦截器中,由于弹窗弹出位置不确定且仅弹出一次,因此需要维护一个全局变量来保证弹窗的弹出状态。可以在AppStorage中定义弹窗弹出状态,并通过@StorageLink来获取弹窗是否曾弹出,具体实现可参考以下示例:

EntryAbility.ets的onWindowStageCreate方法里通过AppStorage定义关于弹框显示的全局属性,默认false不显示:
windowStage.loadContent('pages/Index', (err) => {
AppStorage.setOrCreate('showGlobalCustomDialog', false);
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
封装1个弹框实例类CustomDialogLayout.ets:
@CustomDialog
export struct CustomDialogLayout {
controller?: CustomDialogController;

build() {
Column() {
Text('Global Custom Dialog Test');
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.height(400);
}
}
Index.ets,在该页面import并创建实例import { CustomDialogLayout } from './CustomDialogLayout',并且监听showGlobalCustomDialog属性值的改变并进行拉起弹窗动作:
import { CustomDialogLayout } from './CustomDialogLayout';

@Entry
@Component
struct Index {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@StorageLink('showGlobalCustomDialog') @Watch('globalCustomDialogStateChange') showGlobalCustomDialog: boolean = false;

globalCustomDialogStateChange() {
if (this.showGlobalCustomDialog) {
if (this.dialogController != null) {
this.dialogController.open();
AppStorage.setOrCreate('showGlobalCustomDialog', false);
}
}
}

dialogController: CustomDialogController | null = new CustomDialogController({
builder: CustomDialogLayout({}),
autoCancel: true,
alignment: DialogAlignment.Center,
});

build() {
Navigation(this.pathStack) {
RelativeContainer() {
Button('跳转其他页面')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
})

      .onClick(() => {  
        this.pathStack.pushPathByName('DetailPage', null);  
      });  
  }  
  .height('100%')  
  .width('100%');  
}  
.mode(NavigationMode.Stack);  

}
}
DetailPage.ets,在该页面设置showGlobalCustomDialog全局属性为true即可调起弹框:
@Builder
export function DetailPageBuilder() {
DetailPage();
}

@Component
export struct DetailPage {
@Consume('pathStack') pathStack: NavPathStack;

build() {
NavDestination() {
RelativeContainer() {
Button('promptAction弹窗')
.onClick(() => {
AppStorage.setOrCreate('showGlobalCustomDialog', true);
})
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
});
};
}.title('DetailPage');
}
}
总结
在并发场景下,为了实现条件判断和控制弹窗弹出的行为,可以通过在AppStorage中维护一个全局变量,并使用@StorageLink进行同步监听。这种方法可以保证弹窗只弹出一次,从而避免重复弹出问题。
https://pastebin.com/JihHB9qj
https://pastebin.com/pp4XgNHN
https://pastebin.com/gFi7Ty8i
https://pastebin.com/6nsKZsrR
https://pastebin.com/ZC5tJmyd
https://pastebin.com/vAsXffGW
https://pastebin.com/ha4pYMBs
https://pastebin.com/Lef3jLKc
https://pastebin.com/TJ2Pj2vx
https://pastebin.com/RhihyVw7
https://pastebin.com/erh6eVzC
https://pastebin.com/bKGKK1dm
https://pastebin.com/2UFK6D1y
https://pastebin.com/7R29tEfq
https://pastebin.com/xrH49CPL
https://pastebin.com/gdw9yUrU
https://pastebin.com/EUVjawaY
https://pastebin.com/4TuGak9p
https://pastebin.com/EqWheA0Z
https://pastebin.com/nAWfX2Ce
https://pastebin.com/CAFaEHu9
https://pastebin.com/xCXmycSc
https://pastebin.com/WmenYTsS
https://pastebin.com/z9spTDji
https://pastebin.com/uVp7Nsp1
https://pastebin.com/TEaDnLGQ
https://pastebin.com/mURyCtEV
https://pastebin.com/9zMixq54
https://pastebin.com/YPKWWGwt
https://pastebin.com/BbpwnY0b
https://pastebin.com/kzXJ2Sj5
https://pastebin.com/98nCcftH
https://pastebin.com/g4bA9DML
https://pastebin.com/vjf4hZig
https://pastebin.com/pQPff8MH
https://pastebin.com/2iXVx5dF
https://pastebin.com/prBWGhpk
https://pastebin.com/EJKznYt6
https://pastebin.com/Waa8ezGu
https://pastebin.com/Hje8V6t1
https://pastebin.com/awWHMgZY
https://pastebin.com/EiUjbuN0
https://pastebin.com/XaW0WUTn
https://pastebin.com/JRnZe7Tm
https://pastebin.com/507BPDNz
https://pastebin.com/LKaevb2Y
https://pastebin.com/Sptc9XfX
https://pastebin.com/0e52nKM3
https://pastebin.com/pRFAKR9p
https://pastebin.com/WR98YgEn
https://pastebin.com/MHZyyxsk
https://pastebin.com/zygnUg3G
https://pastebin.com/Y1R09XKN
https://pastebin.com/vWzeCqM7
https://pastebin.com/DNTXXrcn
https://pastebin.com/BfqGBiUU
https://pastebin.com/LwskiZkg
https://pastebin.com/ZwvqpJFq
https://pastebin.com/FQeL9Lfv
https://pastebin.com/s79uWV97
https://pastebin.com/vzVbnZtK
https://pastebin.com/WjTV0ewV
https://pastebin.com/jyVBYEik
https://pastebin.com/Uzcz0PVy
https://pastebin.com/eQxuA195
https://pastebin.com/RpRNfuyG
https://pastebin.com/ZYkLnSEJ
https://pastebin.com/dbvxYAEL
https://pastebin.com/Jr3BFC6q
https://pastebin.com/AK3Fkc6N
https://pastebin.com/eR15KBgQ
https://pastebin.com/MiNxqBcV
https://pastebin.com/0hgraL2f
https://pastebin.com/BpkhJeBE
https://pastebin.com/CtEt6Jhh
https://pastebin.com/XbCJyFi6
https://pastebin.com/K3kM1H0t
https://pastebin.com/szcQwiUx
https://pastebin.com/fBNMQweP
https://pastebin.com/a5BFSqKk
https://pastebin.com/ugaAe5bM
https://pastebin.com/X7cTkwjx
https://pastebin.com/eYM3ucew
https://pastebin.com/pfti4VKT
https://pastebin.com/hsHQwwgQ
https://pastebin.com/z17PcPCS
https://pastebin.com/6gjv06Ri
https://pastebin.com/XE9Bajbu
https://pastebin.com/3wVx4KqR
https://pastebin.com/586hmSqD
https://pastebin.com/qEzZahup
https://pastebin.com/3WGJ7PKM
https://pastebin.com/CzRJGMNg
https://pastebin.com/Pfb1e6ww
https://pastebin.com/kSt2e5bn
https://pastebin.com/WUApxj6r
https://pastebin.com/xEagAfbb
https://pastebin.com/cvkQfAjy
https://pastebin.com/sBFEPgwx
https://pastebin.com/DeThFfPT
https://pastebin.com/j7HGLNrn
https://pastebin.com/mK2WQPEr
https://pastebin.com/Kf6eEfSp
https://pastebin.com/Cd9cBH5m
https://pastebin.com/g0X42ybn
https://pastebin.com/mZyGx93u
https://pastebin.com/VkfwmAZR
https://pastebin.com/0gTGQkqF
https://pastebin.com/pqFk4DeF
https://pastebin.com/5y7Vq3Hb
https://pastebin.com/MWBqZUM0
https://pastebin.com/KU878iEv
https://pastebin.com/zXqt3ek3
https://pastebin.com/c0Bg5wWE
https://pastebin.com/UVdGibVC
https://pastebin.com/QpGAkSny
https://pastebin.com/SUUDWFHy
https://pastebin.com/xyLjSgQV
https://pastebin.com/8PbH8qL1
https://pastebin.com/Hk1ALAEc
https://pastebin.com/eXtjEzhZ
https://pastebin.com/nfC0PSKj
https://pastebin.com/MYfr25pM
https://pastebin.com/mBfJkAaY
https://pastebin.com/W7Z00fYY
https://pastebin.com/fPRnwJds
https://pastebin.com/856Bn5dS

继续阅读 »

问题现象
在应用中,每个接口都可能返回响应码Code,需要在接口拦截器中实现一个全局弹窗。由于接口是并发的,弹窗只能弹出一次。那么如何实现这个全局弹窗呢?此外,由于弹窗可能会在多个页面弹出(如启动页、登录页、主页等),这些页面可能会被销毁,这会导致弹窗无法正常显示。

效果预览
点击放大 点击放大 点击放大

背景知识
使用弹窗组件时,可优先考虑自定义弹窗,便于自定义弹窗的样式与内容。通过CustomDialogController类显示自定义弹窗,不支持直接在类中定义和使用。通常需要将弹框逻辑封装成Builder或其他组件,以便在需要时调用。
可以使用@StorageLink与AppStorage中的key对应的属性建立双向数据同步,该属性可以和UI组件同步,且可以在应用业务逻辑中被访问。
解决方案
在并发接口拦截器中,由于弹窗弹出位置不确定且仅弹出一次,因此需要维护一个全局变量来保证弹窗的弹出状态。可以在AppStorage中定义弹窗弹出状态,并通过@StorageLink来获取弹窗是否曾弹出,具体实现可参考以下示例:

EntryAbility.ets的onWindowStageCreate方法里通过AppStorage定义关于弹框显示的全局属性,默认false不显示:
windowStage.loadContent('pages/Index', (err) => {
AppStorage.setOrCreate('showGlobalCustomDialog', false);
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
封装1个弹框实例类CustomDialogLayout.ets:
@CustomDialog
export struct CustomDialogLayout {
controller?: CustomDialogController;

build() {
Column() {
Text('Global Custom Dialog Test');
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.height(400);
}
}
Index.ets,在该页面import并创建实例import { CustomDialogLayout } from './CustomDialogLayout',并且监听showGlobalCustomDialog属性值的改变并进行拉起弹窗动作:
import { CustomDialogLayout } from './CustomDialogLayout';

@Entry
@Component
struct Index {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
@StorageLink('showGlobalCustomDialog') @Watch('globalCustomDialogStateChange') showGlobalCustomDialog: boolean = false;

globalCustomDialogStateChange() {
if (this.showGlobalCustomDialog) {
if (this.dialogController != null) {
this.dialogController.open();
AppStorage.setOrCreate('showGlobalCustomDialog', false);
}
}
}

dialogController: CustomDialogController | null = new CustomDialogController({
builder: CustomDialogLayout({}),
autoCancel: true,
alignment: DialogAlignment.Center,
});

build() {
Navigation(this.pathStack) {
RelativeContainer() {
Button('跳转其他页面')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
})

      .onClick(() => {  
        this.pathStack.pushPathByName('DetailPage', null);  
      });  
  }  
  .height('100%')  
  .width('100%');  
}  
.mode(NavigationMode.Stack);  

}
}
DetailPage.ets,在该页面设置showGlobalCustomDialog全局属性为true即可调起弹框:
@Builder
export function DetailPageBuilder() {
DetailPage();
}

@Component
export struct DetailPage {
@Consume('pathStack') pathStack: NavPathStack;

build() {
NavDestination() {
RelativeContainer() {
Button('promptAction弹窗')
.onClick(() => {
AppStorage.setOrCreate('showGlobalCustomDialog', true);
})
.alignRules({
center: { anchor: 'container', align: VerticalAlign.Center },
middle: { anchor: 'container', align: HorizontalAlign.Center }
});
};
}.title('DetailPage');
}
}
总结
在并发场景下,为了实现条件判断和控制弹窗弹出的行为,可以通过在AppStorage中维护一个全局变量,并使用@StorageLink进行同步监听。这种方法可以保证弹窗只弹出一次,从而避免重复弹出问题。
https://pastebin.com/JihHB9qj
https://pastebin.com/pp4XgNHN
https://pastebin.com/gFi7Ty8i
https://pastebin.com/6nsKZsrR
https://pastebin.com/ZC5tJmyd
https://pastebin.com/vAsXffGW
https://pastebin.com/ha4pYMBs
https://pastebin.com/Lef3jLKc
https://pastebin.com/TJ2Pj2vx
https://pastebin.com/RhihyVw7
https://pastebin.com/erh6eVzC
https://pastebin.com/bKGKK1dm
https://pastebin.com/2UFK6D1y
https://pastebin.com/7R29tEfq
https://pastebin.com/xrH49CPL
https://pastebin.com/gdw9yUrU
https://pastebin.com/EUVjawaY
https://pastebin.com/4TuGak9p
https://pastebin.com/EqWheA0Z
https://pastebin.com/nAWfX2Ce
https://pastebin.com/CAFaEHu9
https://pastebin.com/xCXmycSc
https://pastebin.com/WmenYTsS
https://pastebin.com/z9spTDji
https://pastebin.com/uVp7Nsp1
https://pastebin.com/TEaDnLGQ
https://pastebin.com/mURyCtEV
https://pastebin.com/9zMixq54
https://pastebin.com/YPKWWGwt
https://pastebin.com/BbpwnY0b
https://pastebin.com/kzXJ2Sj5
https://pastebin.com/98nCcftH
https://pastebin.com/g4bA9DML
https://pastebin.com/vjf4hZig
https://pastebin.com/pQPff8MH
https://pastebin.com/2iXVx5dF
https://pastebin.com/prBWGhpk
https://pastebin.com/EJKznYt6
https://pastebin.com/Waa8ezGu
https://pastebin.com/Hje8V6t1
https://pastebin.com/awWHMgZY
https://pastebin.com/EiUjbuN0
https://pastebin.com/XaW0WUTn
https://pastebin.com/JRnZe7Tm
https://pastebin.com/507BPDNz
https://pastebin.com/LKaevb2Y
https://pastebin.com/Sptc9XfX
https://pastebin.com/0e52nKM3
https://pastebin.com/pRFAKR9p
https://pastebin.com/WR98YgEn
https://pastebin.com/MHZyyxsk
https://pastebin.com/zygnUg3G
https://pastebin.com/Y1R09XKN
https://pastebin.com/vWzeCqM7
https://pastebin.com/DNTXXrcn
https://pastebin.com/BfqGBiUU
https://pastebin.com/LwskiZkg
https://pastebin.com/ZwvqpJFq
https://pastebin.com/FQeL9Lfv
https://pastebin.com/s79uWV97
https://pastebin.com/vzVbnZtK
https://pastebin.com/WjTV0ewV
https://pastebin.com/jyVBYEik
https://pastebin.com/Uzcz0PVy
https://pastebin.com/eQxuA195
https://pastebin.com/RpRNfuyG
https://pastebin.com/ZYkLnSEJ
https://pastebin.com/dbvxYAEL
https://pastebin.com/Jr3BFC6q
https://pastebin.com/AK3Fkc6N
https://pastebin.com/eR15KBgQ
https://pastebin.com/MiNxqBcV
https://pastebin.com/0hgraL2f
https://pastebin.com/BpkhJeBE
https://pastebin.com/CtEt6Jhh
https://pastebin.com/XbCJyFi6
https://pastebin.com/K3kM1H0t
https://pastebin.com/szcQwiUx
https://pastebin.com/fBNMQweP
https://pastebin.com/a5BFSqKk
https://pastebin.com/ugaAe5bM
https://pastebin.com/X7cTkwjx
https://pastebin.com/eYM3ucew
https://pastebin.com/pfti4VKT
https://pastebin.com/hsHQwwgQ
https://pastebin.com/z17PcPCS
https://pastebin.com/6gjv06Ri
https://pastebin.com/XE9Bajbu
https://pastebin.com/3wVx4KqR
https://pastebin.com/586hmSqD
https://pastebin.com/qEzZahup
https://pastebin.com/3WGJ7PKM
https://pastebin.com/CzRJGMNg
https://pastebin.com/Pfb1e6ww
https://pastebin.com/kSt2e5bn
https://pastebin.com/WUApxj6r
https://pastebin.com/xEagAfbb
https://pastebin.com/cvkQfAjy
https://pastebin.com/sBFEPgwx
https://pastebin.com/DeThFfPT
https://pastebin.com/j7HGLNrn
https://pastebin.com/mK2WQPEr
https://pastebin.com/Kf6eEfSp
https://pastebin.com/Cd9cBH5m
https://pastebin.com/g0X42ybn
https://pastebin.com/mZyGx93u
https://pastebin.com/VkfwmAZR
https://pastebin.com/0gTGQkqF
https://pastebin.com/pqFk4DeF
https://pastebin.com/5y7Vq3Hb
https://pastebin.com/MWBqZUM0
https://pastebin.com/KU878iEv
https://pastebin.com/zXqt3ek3
https://pastebin.com/c0Bg5wWE
https://pastebin.com/UVdGibVC
https://pastebin.com/QpGAkSny
https://pastebin.com/SUUDWFHy
https://pastebin.com/xyLjSgQV
https://pastebin.com/8PbH8qL1
https://pastebin.com/Hk1ALAEc
https://pastebin.com/eXtjEzhZ
https://pastebin.com/nfC0PSKj
https://pastebin.com/MYfr25pM
https://pastebin.com/mBfJkAaY
https://pastebin.com/W7Z00fYY
https://pastebin.com/fPRnwJds
https://pastebin.com/856Bn5dS

收起阅读 »

如何解决NavDestination切换页面后浏览位置无法保存问题

问题现象
在HarmonyOS中,使用NavDestination实现页面跳转至其他页面后,下次进入如何保持跳转前页面的浏览位置?

背景知识
Navigation:Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(NavDestination的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
@ohos.arkui.observer(无感监听)提供UI组件行为变化的无感监听能力。可以监听Navigation的页面切换事件进行相应操作。
Scroll是一种可滚动的容器组件,当子组件的布局尺寸超过父组件的尺寸时,内容可以滚动。
onDidScroll方法在Scroll滚动时触发,可用于在滑动过程中获取Scroll组件的偏移量yOffset。
scrollTo方法可用于让Scroll组件滑动到指定位置。
AppStorage是应用全局的UI状态存储,和应用的进程绑定,只能在UI主线程中使用,无法在子线程中使用、修改。
解决方案
方案一:使用路由模式保留页面实例。
在页面跳转时不要使用pop和clear等方法,否则NavDestination页面会被回收。可以采用单例模式MOVE_TO_TOP_SINGLETON跳转至指定的页面,该方式会使用栈内已存在的页面实例(即浏览位置不变)。

由于单例模式是从栈底到栈顶依次查找,当栈内存在多个同名页面实例时,会默认跳转最底层的同名页面实例,所以需确保栈内只存在一个同名实例,否则跳转的页面保留的滚动位置与上一次显示的页面会不一致(显示的是栈底同名实例保留的滚动位置)。
https://pastebin.com/jg7cApH9
https://pastebin.com/JptcDPdH
https://pastebin.com/fJ3FRPmU
https://pastebin.com/XW6K3fae
https://pastebin.com/LTuQfDs4
https://pastebin.com/B7u5aJgV
https://pastebin.com/kQDwTYr0
https://pastebin.com/xBwWHVWP
https://pastebin.com/XnbRLadK
https://pastebin.com/P7cJhcvz
https://pastebin.com/PPHJBRbF
https://pastebin.com/p3d9QNpd
https://pastebin.com/cvXpr2Xe
https://pastebin.com/xPFHs3J9
https://pastebin.com/jSmP0nAz
https://pastebin.com/F7BVe7rA
https://pastebin.com/rxxsw7rL
https://pastebin.com/dsapsf85
https://pastebin.com/WDTMjhBj
https://pastebin.com/YZm9PuQ0
https://pastebin.com/Ww8WiVCh
https://pastebin.com/1mdA2xYa
https://pastebin.com/HmnTgAvU
https://pastebin.com/4VFUyY1W
https://pastebin.com/iSCA3e1G
https://pastebin.com/DvvZgR5T
https://pastebin.com/XkCssDHN
https://pastebin.com/MS590bez
https://pastebin.com/APFd7Si7
https://pastebin.com/zszhKUha
https://pastebin.com/eCxbgYRD
https://pastebin.com/rvuEnH0U
https://pastebin.com/gvp8Sryc
https://pastebin.com/5G9bqXX8
https://pastebin.com/iveHA0B9
https://pastebin.com/YN31LHqg
https://pastebin.com/wwePXfTd
https://pastebin.com/xVjTy8Eh
https://pastebin.com/R2GZKWcL
https://pastebin.com/5R5n97MN
https://pastebin.com/bTJYZwXQ
https://pastebin.com/3MMSMF4w
https://pastebin.com/8FfWGCfV
https://pastebin.com/FG1V23qy
https://pastebin.com/R76515LN
https://pastebin.com/dgwf902E
https://pastebin.com/vF3LW9wz
https://pastebin.com/xuVFsSpN
https://pastebin.com/ycNDJLKt
https://pastebin.com/856Ss0AA
https://pastebin.com/SU4ZD1Zn
https://pastebin.com/VdiknXu9
https://pastebin.com/Qt1TYCpX
https://pastebin.com/7S5zwEmb
https://pastebin.com/YgNQiKJu
https://pastebin.com/8qE1cRZh
https://pastebin.com/qgtGNvG2
https://pastebin.com/ripQ1rCp
https://pastebin.com/vj9eK6Tw
https://pastebin.com/qD8waSpv
https://pastebin.com/LTPgDm4K
https://pastebin.com/AD1JQT8M
https://pastebin.com/F8BZKARC
https://pastebin.com/rtgxejCJ
https://pastebin.com/iGDCC7v6
https://pastebin.com/KDGz1ZAw
https://pastebin.com/T6ZRvjnv
https://pastebin.com/gn4VQL8C
https://pastebin.com/Pe2hxUPW
https://pastebin.com/ABUpdTBf
https://pastebin.com/hkr9q5qT
https://pastebin.com/7eMPsQSF
https://pastebin.com/2eazmQsj
https://pastebin.com/ZDyMcSUu
https://pastebin.com/40sqLNgD
https://pastebin.com/K4tB8zUq
https://pastebin.com/Vu5qGCNq
https://pastebin.com/NadrhJpP
https://pastebin.com/DMmxaYYg
https://pastebin.com/MD2QievW
https://pastebin.com/PE70Eqmr
https://pastebin.com/Ew5Fv07S
https://pastebin.com/UTUpEMDs
https://pastebin.com/msjPsKni
https://pastebin.com/Cy8PQ25K
https://pastebin.com/7mxb1JXt
https://pastebin.com/AgycmibL
https://pastebin.com/AVU1pR19
https://pastebin.com/3RmynMcG
https://pastebin.com/bAnvjLAq
https://pastebin.com/MNhKPjgL
https://pastebin.com/28gSRM4Y
https://pastebin.com/K10xjm2X
https://pastebin.com/HDW8s1cW
https://pastebin.com/Vz1hyR31
https://pastebin.com/0NVNEz8A
https://pastebin.com/5fqUpYWd
https://pastebin.com/L3Q0tjsz
https://pastebin.com/vcr11xWp
https://pastebin.com/QcuCqCyz
https://pastebin.com/VwCDdqGb
https://pastebin.com/RxyBYiQ1
https://pastebin.com/0cDzryZ8
https://pastebin.com/Ckxhy2mx
https://pastebin.com/y5gNKnjD
https://pastebin.com/k9cBDx1L
https://pastebin.com/KrdmLCzz
https://pastebin.com/XnbC31tF
https://pastebin.com/75Uq20MB
https://pastebin.com/w99f2xji
https://pastebin.com/wcbEE71j
https://pastebin.com/kGjYuVGW
https://pastebin.com/fugp0ttc
https://pastebin.com/N7s1QHRQ
https://pastebin.com/pPsWPb1a
https://pastebin.com/NWCppgZH
https://pastebin.com/sG0EpEW3
https://pastebin.com/QBXpHJhW
https://pastebin.com/9uaS9gT8
https://pastebin.com/q5RL1azA
https://pastebin.com/sXgTvvNe
https://pastebin.com/dM3Y0fNj
https://pastebin.com/2fvDixft
https://pastebin.com/TVCDt8Bk
https://pastebin.com/C5TNtJzS
https://pastebin.com/gvu7HrFG
https://pastebin.com/6gtmpefC
https://pastebin.com/e5XXTcSu
https://pastebin.com/ULRT40ic
https://pastebin.com/35dDdqDv
https://pastebin.com/V3Q4xPDB
https://pastebin.com/6FQ3hjhu
https://pastebin.com/GXVmSxWa
https://pastebin.com/ARpP5Sgx
https://pastebin.com/2C4G0GXm
https://pastebin.com/aydQrrL2
https://pastebin.com/RcDaQksi
https://pastebin.com/A09zVHhV
https://pastebin.com/EW4dKr6p
https://pastebin.com/NbuHg3jA
https://pastebin.com/mpvxPgsk
https://pastebin.com/d5me4T4d
https://pastebin.com/Pq4xqrwy
https://pastebin.com/1fwUEq5w
https://pastebin.com/RhQmmqpX
https://pastebin.com/0dmyxL0K
https://pastebin.com/whYZqr0q
https://pastebin.com/2Jzhd4hu
https://pastebin.com/kphAgh4R
https://pastebin.com/GDrrQdRp
https://pastebin.com/fY2yfAB7
https://pastebin.com/qYKKXLYL
https://pastebin.com/ybxMQCT1
https://pastebin.com/2au0x8Lf
https://pastebin.com/BvAZqhcP
https://pastebin.com/46LiCfhw
https://pastebin.com/DtZgXzmr
https://pastebin.com/XcWD26ZA
https://pastebin.com/C9emeaLS
https://pastebin.com/09GWiGur
https://pastebin.com/aH6FR1eC
https://pastebin.com/P8LJSBmN
https://pastebin.com/wiNqpWqX
https://pastebin.com/zT6vi5ys
https://pastebin.com/AVaKA8ci
https://pastebin.com/k3SrKPSD
https://pastebin.com/qtCMmq6p
https://pastebin.com/FJ464td4
https://pastebin.com/c6fkeh3w
https://pastebin.com/SbkLNL4E
https://pastebin.com/VGqk15nm
https://pastebin.com/h4SxwUdU
https://pastebin.com/Ngzb6uJq
https://pastebin.com/ff3aCBWh
https://pastebin.com/0i06Njsd
https://pastebin.com/fdYcqYTb
https://pastebin.com/bFiemuPx
https://pastebin.com/Syn6MvVa
https://pastebin.com/wGFSznjy
https://pastebin.com/StLnicTr
https://pastebin.com/KYLknUL1
https://pastebin.com/qKFitbxZ
https://pastebin.com/Q6tJT1ar
https://pastebin.com/GAyLPptG
https://pastebin.com/2zqFYn9X
https://pastebin.com/5ED0Vjjx
https://pastebin.com/dfPRkZay
https://pastebin.com/5W2xXc9L
https://pastebin.com/KRiFEcKH
https://pastebin.com/1kJv45DQ
https://pastebin.com/m2624v5Q
https://pastebin.com/w06qZAVW
https://pastebin.com/L4LmccPV
https://pastebin.com/JzNusi76
https://pastebin.com/EwSR48QA
https://pastebin.com/45A8NUsn
https://pastebin.com/TYg6awym
https://pastebin.com/ti8fjF4t
https://pastebin.com/eVVGSL2Z
https://pastebin.com/fRVbrzKS
https://pastebin.com/aVSd5N24
https://pastebin.com/tscNEGQe
https://pastebin.com/YWnRTD7n
https://pastebin.com/KWXg4hdG
https://pastebin.com/9phkHrjB
https://pastebin.com/3PtPqjvc
https://pastebin.com/8duhjkTw
https://pastebin.com/r2FdrSPY
https://pastebin.com/6fnVj3bj
https://pastebin.com/WsT72uPu

继续阅读 »

问题现象
在HarmonyOS中,使用NavDestination实现页面跳转至其他页面后,下次进入如何保持跳转前页面的浏览位置?

背景知识
Navigation:Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,其内部默认包含了标题栏、内容区和工具栏,其中内容区默认首页显示导航内容(NavDestination的子组件)或非首页显示(NavDestination的子组件),首页和非首页通过路由进行切换。
@ohos.arkui.observer(无感监听)提供UI组件行为变化的无感监听能力。可以监听Navigation的页面切换事件进行相应操作。
Scroll是一种可滚动的容器组件,当子组件的布局尺寸超过父组件的尺寸时,内容可以滚动。
onDidScroll方法在Scroll滚动时触发,可用于在滑动过程中获取Scroll组件的偏移量yOffset。
scrollTo方法可用于让Scroll组件滑动到指定位置。
AppStorage是应用全局的UI状态存储,和应用的进程绑定,只能在UI主线程中使用,无法在子线程中使用、修改。
解决方案
方案一:使用路由模式保留页面实例。
在页面跳转时不要使用pop和clear等方法,否则NavDestination页面会被回收。可以采用单例模式MOVE_TO_TOP_SINGLETON跳转至指定的页面,该方式会使用栈内已存在的页面实例(即浏览位置不变)。

由于单例模式是从栈底到栈顶依次查找,当栈内存在多个同名页面实例时,会默认跳转最底层的同名页面实例,所以需确保栈内只存在一个同名实例,否则跳转的页面保留的滚动位置与上一次显示的页面会不一致(显示的是栈底同名实例保留的滚动位置)。
https://pastebin.com/jg7cApH9
https://pastebin.com/JptcDPdH
https://pastebin.com/fJ3FRPmU
https://pastebin.com/XW6K3fae
https://pastebin.com/LTuQfDs4
https://pastebin.com/B7u5aJgV
https://pastebin.com/kQDwTYr0
https://pastebin.com/xBwWHVWP
https://pastebin.com/XnbRLadK
https://pastebin.com/P7cJhcvz
https://pastebin.com/PPHJBRbF
https://pastebin.com/p3d9QNpd
https://pastebin.com/cvXpr2Xe
https://pastebin.com/xPFHs3J9
https://pastebin.com/jSmP0nAz
https://pastebin.com/F7BVe7rA
https://pastebin.com/rxxsw7rL
https://pastebin.com/dsapsf85
https://pastebin.com/WDTMjhBj
https://pastebin.com/YZm9PuQ0
https://pastebin.com/Ww8WiVCh
https://pastebin.com/1mdA2xYa
https://pastebin.com/HmnTgAvU
https://pastebin.com/4VFUyY1W
https://pastebin.com/iSCA3e1G
https://pastebin.com/DvvZgR5T
https://pastebin.com/XkCssDHN
https://pastebin.com/MS590bez
https://pastebin.com/APFd7Si7
https://pastebin.com/zszhKUha
https://pastebin.com/eCxbgYRD
https://pastebin.com/rvuEnH0U
https://pastebin.com/gvp8Sryc
https://pastebin.com/5G9bqXX8
https://pastebin.com/iveHA0B9
https://pastebin.com/YN31LHqg
https://pastebin.com/wwePXfTd
https://pastebin.com/xVjTy8Eh
https://pastebin.com/R2GZKWcL
https://pastebin.com/5R5n97MN
https://pastebin.com/bTJYZwXQ
https://pastebin.com/3MMSMF4w
https://pastebin.com/8FfWGCfV
https://pastebin.com/FG1V23qy
https://pastebin.com/R76515LN
https://pastebin.com/dgwf902E
https://pastebin.com/vF3LW9wz
https://pastebin.com/xuVFsSpN
https://pastebin.com/ycNDJLKt
https://pastebin.com/856Ss0AA
https://pastebin.com/SU4ZD1Zn
https://pastebin.com/VdiknXu9
https://pastebin.com/Qt1TYCpX
https://pastebin.com/7S5zwEmb
https://pastebin.com/YgNQiKJu
https://pastebin.com/8qE1cRZh
https://pastebin.com/qgtGNvG2
https://pastebin.com/ripQ1rCp
https://pastebin.com/vj9eK6Tw
https://pastebin.com/qD8waSpv
https://pastebin.com/LTPgDm4K
https://pastebin.com/AD1JQT8M
https://pastebin.com/F8BZKARC
https://pastebin.com/rtgxejCJ
https://pastebin.com/iGDCC7v6
https://pastebin.com/KDGz1ZAw
https://pastebin.com/T6ZRvjnv
https://pastebin.com/gn4VQL8C
https://pastebin.com/Pe2hxUPW
https://pastebin.com/ABUpdTBf
https://pastebin.com/hkr9q5qT
https://pastebin.com/7eMPsQSF
https://pastebin.com/2eazmQsj
https://pastebin.com/ZDyMcSUu
https://pastebin.com/40sqLNgD
https://pastebin.com/K4tB8zUq
https://pastebin.com/Vu5qGCNq
https://pastebin.com/NadrhJpP
https://pastebin.com/DMmxaYYg
https://pastebin.com/MD2QievW
https://pastebin.com/PE70Eqmr
https://pastebin.com/Ew5Fv07S
https://pastebin.com/UTUpEMDs
https://pastebin.com/msjPsKni
https://pastebin.com/Cy8PQ25K
https://pastebin.com/7mxb1JXt
https://pastebin.com/AgycmibL
https://pastebin.com/AVU1pR19
https://pastebin.com/3RmynMcG
https://pastebin.com/bAnvjLAq
https://pastebin.com/MNhKPjgL
https://pastebin.com/28gSRM4Y
https://pastebin.com/K10xjm2X
https://pastebin.com/HDW8s1cW
https://pastebin.com/Vz1hyR31
https://pastebin.com/0NVNEz8A
https://pastebin.com/5fqUpYWd
https://pastebin.com/L3Q0tjsz
https://pastebin.com/vcr11xWp
https://pastebin.com/QcuCqCyz
https://pastebin.com/VwCDdqGb
https://pastebin.com/RxyBYiQ1
https://pastebin.com/0cDzryZ8
https://pastebin.com/Ckxhy2mx
https://pastebin.com/y5gNKnjD
https://pastebin.com/k9cBDx1L
https://pastebin.com/KrdmLCzz
https://pastebin.com/XnbC31tF
https://pastebin.com/75Uq20MB
https://pastebin.com/w99f2xji
https://pastebin.com/wcbEE71j
https://pastebin.com/kGjYuVGW
https://pastebin.com/fugp0ttc
https://pastebin.com/N7s1QHRQ
https://pastebin.com/pPsWPb1a
https://pastebin.com/NWCppgZH
https://pastebin.com/sG0EpEW3
https://pastebin.com/QBXpHJhW
https://pastebin.com/9uaS9gT8
https://pastebin.com/q5RL1azA
https://pastebin.com/sXgTvvNe
https://pastebin.com/dM3Y0fNj
https://pastebin.com/2fvDixft
https://pastebin.com/TVCDt8Bk
https://pastebin.com/C5TNtJzS
https://pastebin.com/gvu7HrFG
https://pastebin.com/6gtmpefC
https://pastebin.com/e5XXTcSu
https://pastebin.com/ULRT40ic
https://pastebin.com/35dDdqDv
https://pastebin.com/V3Q4xPDB
https://pastebin.com/6FQ3hjhu
https://pastebin.com/GXVmSxWa
https://pastebin.com/ARpP5Sgx
https://pastebin.com/2C4G0GXm
https://pastebin.com/aydQrrL2
https://pastebin.com/RcDaQksi
https://pastebin.com/A09zVHhV
https://pastebin.com/EW4dKr6p
https://pastebin.com/NbuHg3jA
https://pastebin.com/mpvxPgsk
https://pastebin.com/d5me4T4d
https://pastebin.com/Pq4xqrwy
https://pastebin.com/1fwUEq5w
https://pastebin.com/RhQmmqpX
https://pastebin.com/0dmyxL0K
https://pastebin.com/whYZqr0q
https://pastebin.com/2Jzhd4hu
https://pastebin.com/kphAgh4R
https://pastebin.com/GDrrQdRp
https://pastebin.com/fY2yfAB7
https://pastebin.com/qYKKXLYL
https://pastebin.com/ybxMQCT1
https://pastebin.com/2au0x8Lf
https://pastebin.com/BvAZqhcP
https://pastebin.com/46LiCfhw
https://pastebin.com/DtZgXzmr
https://pastebin.com/XcWD26ZA
https://pastebin.com/C9emeaLS
https://pastebin.com/09GWiGur
https://pastebin.com/aH6FR1eC
https://pastebin.com/P8LJSBmN
https://pastebin.com/wiNqpWqX
https://pastebin.com/zT6vi5ys
https://pastebin.com/AVaKA8ci
https://pastebin.com/k3SrKPSD
https://pastebin.com/qtCMmq6p
https://pastebin.com/FJ464td4
https://pastebin.com/c6fkeh3w
https://pastebin.com/SbkLNL4E
https://pastebin.com/VGqk15nm
https://pastebin.com/h4SxwUdU
https://pastebin.com/Ngzb6uJq
https://pastebin.com/ff3aCBWh
https://pastebin.com/0i06Njsd
https://pastebin.com/fdYcqYTb
https://pastebin.com/bFiemuPx
https://pastebin.com/Syn6MvVa
https://pastebin.com/wGFSznjy
https://pastebin.com/StLnicTr
https://pastebin.com/KYLknUL1
https://pastebin.com/qKFitbxZ
https://pastebin.com/Q6tJT1ar
https://pastebin.com/GAyLPptG
https://pastebin.com/2zqFYn9X
https://pastebin.com/5ED0Vjjx
https://pastebin.com/dfPRkZay
https://pastebin.com/5W2xXc9L
https://pastebin.com/KRiFEcKH
https://pastebin.com/1kJv45DQ
https://pastebin.com/m2624v5Q
https://pastebin.com/w06qZAVW
https://pastebin.com/L4LmccPV
https://pastebin.com/JzNusi76
https://pastebin.com/EwSR48QA
https://pastebin.com/45A8NUsn
https://pastebin.com/TYg6awym
https://pastebin.com/ti8fjF4t
https://pastebin.com/eVVGSL2Z
https://pastebin.com/fRVbrzKS
https://pastebin.com/aVSd5N24
https://pastebin.com/tscNEGQe
https://pastebin.com/YWnRTD7n
https://pastebin.com/KWXg4hdG
https://pastebin.com/9phkHrjB
https://pastebin.com/3PtPqjvc
https://pastebin.com/8duhjkTw
https://pastebin.com/r2FdrSPY
https://pastebin.com/6fnVj3bj
https://pastebin.com/WsT72uPu

收起阅读 »

希望官方开放 uniappx 的 页面路由

例如页面跳转 uni.navigateTo

原生的页面转场很多丝滑表现,
例如 安卓里面的转场很丝滑,包含他头部的 标题栏 就像是增量切换一样,而uniappx 和uniapp里面的就像是一块布硬生生的拖过去

iOS 的 页面转场则更多丝滑效果

等等 路由api,就那么几个动画,方式,很基础,很多原生的特性度无法使用,希望官方能把 页面路由的切换开放,提供给我们开发者自定义处理页面的路由跳转

继续阅读 »

例如页面跳转 uni.navigateTo

原生的页面转场很多丝滑表现,
例如 安卓里面的转场很丝滑,包含他头部的 标题栏 就像是增量切换一样,而uniappx 和uniapp里面的就像是一块布硬生生的拖过去

iOS 的 页面转场则更多丝滑效果

等等 路由api,就那么几个动画,方式,很基础,很多原生的特性度无法使用,希望官方能把 页面路由的切换开放,提供给我们开发者自定义处理页面的路由跳转

收起阅读 »