HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

用 Agent 开发 uni-app,怎样让任务少停在半路?

技术分享

作者:XylemNode 团队

用 Agent 开发 uni-app,方便的地方在于:它可以读取现有项目,找到需要修改的文件,完成改动,再根据检查结果继续调整。

你交给它的是一项开发任务,中间的读文件、改代码、看报错,可以由具备相应工具和权限的 Agent 连着处理。

但开始这样用以后,模型接入这一步就值得多花点心思。一个任务可能要经过多轮模型调用,期间还会执行本地操作。如果中途停下来,开发者需要知道:是模型请求没完成,还是构建报错?文件已经改到哪一步?接下来能不能继续?

下面就从 Agent 接入模型服务说起,聊聊用它开发 uni-app 时,哪些地方提前处理好,能少一些来回折腾。

模型服务接在 Agent 这一侧

先把关系说明白:开发者在编程 Agent 里配置模型服务,Agent 调用模型分析任务,再通过自己的工具读取、修改 uni-app 项目。

可以把它理解成:

开发者提出任务 → Agent 调用模型 → Agent 执行项目操作 → 把执行结果交给模型继续处理

以接入 XylemNode 为例,模型接口配置在支持自定义服务的 Agent 中,通常涉及接口地址、API Key 和模型名称,具体填写方式取决于该工具支持的协议。

这些是开发工具的配置,不需要为了使用 Agent,把模型服务的密钥写进 uni-app 的页面代码。

接好以后,建议先跑一个范围很小、但步骤完整的任务:读取相关文件,完成一处修改,再执行项目已有的检查。重点看 Agent 能否正确发起工具调用、收到执行结果,并据此继续处理。

因为在这种工作方式里,模型和工具之间会有往返:模型提出操作,客户端执行,再把结果传回。Anthropic 的工具调用文档介绍了这一过程。一次普通对话成功,还没有覆盖这些环节。

动手之前,先让 Agent 看懂你的 uni-app 项目

接入没问题,也不代表 Agent 已经了解项目约定。

目标是 H5、App 还是微信小程序?使用哪个版本的 Vue?项目通过 HBuilderX 管理,还是已经有命令行构建流程?现有组件和目录应该怎么沿用?

这些信息会影响它怎么改代码、怎么验证。让它先读项目配置和已有实现,可以减少凭空补一套结构、引入不必要依赖的情况。

开始任务时,可以附上几句要求:

> 先检查项目结构、依赖和目标平台,再开始修改。沿用现有组件和代码风格,新增依赖前说明原因。修改后执行项目已有的检查,并列出实际执行的命令与结果。需要在 HBuilderX、模拟器或真机上验证的步骤,单独列出来。

这里不适合给所有项目套一条固定的构建命令。Agent 能运行哪些检查,要看项目本身的配置和它具备的工具。没有执行的步骤,就保留为待验证项。

任务停下来,先分清是哪一段在等

一个修改任务可能经过这样的过程:读页面和组件、分析依赖关系、修改文件、执行检查,再根据报错继续修正。

中间既有模型请求,也有本地工具执行。看到 Agent 暂时没动静,先看它当前停在哪一步。

如果面板显示模型调用超时,就保留错误详情和请求编号,检查调用状态。如果已经拿到构建报错,就先围绕报错处理。如果工具正在等待权限或人工操作,则需要完成对应动作。

分清这些情况,才能避免没有必要的操作。构建失败时反复换接口,解决不了代码或环境问题;模型请求中断时,也不能据此认定刚才的文件修改没有执行。

尤其是任务已经改过文件的情况,继续之前先看文件差异和执行记录。如果修改已经完成,只剩检查没有跑完,就先确认能否从检查环节接着处理。

通道选择这类工作,可以少让人手动处理

对于模型调用本身,还有一部分恢复工作可以交给网关。

当某条调用路径暂时异常,而系统中存在其他符合请求要求的可用路径时,可以由网关按规则选择路径、尝试恢复,减少开发者手动切换配置的次数。

以我们 XylemNode 的 Auto 分组为例,它结合通道健康状态按策略选路,遇到通道异常时支持跨组重试,调用方继续使用原来的模型名。具体说明可以看官网的 Auto 分组介绍。

放到 Agent 开发流程里,这项能力承担的是模型请求的通道选择。开发者仍然决定任务使用哪个模型,也仍需由 Agent 管理工具执行和项目进度。

因此,通道恢复不能直接等同于整个开发任务恢复。已经写入的代码、运行过的命令,都需要结合实际状态继续处理;流式响应已经返回一部分时,也不能把重新请求理解成自动无缝续写。

接入时还应了解 Agent 和网关各自的重试规则,给尝试次数和总等待时间留出明确边界。这样,自动处理才不会变成一直等待,却不知道任务有没有进展。

最后看交付结果,别停在一句“已经完成”

用 Agent 开发 uni-app,最后还是要回到项目本身:改了哪些文件,修改是否符合要求,执行过哪些检查,目标平台上还有哪些内容需要验证。

可以要求 Agent 在结束时给出一份简短交代:

  • 本次修改涉及哪些文件,分别解决了什么问题;
  • 实际运行了哪些检查,结果是什么;
  • 还有哪些页面表现或平台兼容性需要人工验证。

命令行检查通过,就记录这项结果。页面有没有显示异常、交互是否符合预期,以及目标设备上的表现,还要通过相应环境确认。

项目上下文交代清楚,Agent 才更容易按现有约定做事;模型接入和恢复规则理顺,才能减少一部分开发过程中的打断;检查结果留下来,下次继续任务也更有依据。

对开发者来说,省心的体验很具体:少一些来回改配置,任务中断后能看清进度,结束时拿得到可以检查的结果。


本文由 XylemNode 团队整理,AI 辅助起草,技术依据见文中链接。

继续阅读 »

作者:XylemNode 团队

用 Agent 开发 uni-app,方便的地方在于:它可以读取现有项目,找到需要修改的文件,完成改动,再根据检查结果继续调整。

你交给它的是一项开发任务,中间的读文件、改代码、看报错,可以由具备相应工具和权限的 Agent 连着处理。

但开始这样用以后,模型接入这一步就值得多花点心思。一个任务可能要经过多轮模型调用,期间还会执行本地操作。如果中途停下来,开发者需要知道:是模型请求没完成,还是构建报错?文件已经改到哪一步?接下来能不能继续?

下面就从 Agent 接入模型服务说起,聊聊用它开发 uni-app 时,哪些地方提前处理好,能少一些来回折腾。

模型服务接在 Agent 这一侧

先把关系说明白:开发者在编程 Agent 里配置模型服务,Agent 调用模型分析任务,再通过自己的工具读取、修改 uni-app 项目。

可以把它理解成:

开发者提出任务 → Agent 调用模型 → Agent 执行项目操作 → 把执行结果交给模型继续处理

以接入 XylemNode 为例,模型接口配置在支持自定义服务的 Agent 中,通常涉及接口地址、API Key 和模型名称,具体填写方式取决于该工具支持的协议。

这些是开发工具的配置,不需要为了使用 Agent,把模型服务的密钥写进 uni-app 的页面代码。

接好以后,建议先跑一个范围很小、但步骤完整的任务:读取相关文件,完成一处修改,再执行项目已有的检查。重点看 Agent 能否正确发起工具调用、收到执行结果,并据此继续处理。

因为在这种工作方式里,模型和工具之间会有往返:模型提出操作,客户端执行,再把结果传回。Anthropic 的工具调用文档介绍了这一过程。一次普通对话成功,还没有覆盖这些环节。

动手之前,先让 Agent 看懂你的 uni-app 项目

接入没问题,也不代表 Agent 已经了解项目约定。

目标是 H5、App 还是微信小程序?使用哪个版本的 Vue?项目通过 HBuilderX 管理,还是已经有命令行构建流程?现有组件和目录应该怎么沿用?

这些信息会影响它怎么改代码、怎么验证。让它先读项目配置和已有实现,可以减少凭空补一套结构、引入不必要依赖的情况。

开始任务时,可以附上几句要求:

> 先检查项目结构、依赖和目标平台,再开始修改。沿用现有组件和代码风格,新增依赖前说明原因。修改后执行项目已有的检查,并列出实际执行的命令与结果。需要在 HBuilderX、模拟器或真机上验证的步骤,单独列出来。

这里不适合给所有项目套一条固定的构建命令。Agent 能运行哪些检查,要看项目本身的配置和它具备的工具。没有执行的步骤,就保留为待验证项。

任务停下来,先分清是哪一段在等

一个修改任务可能经过这样的过程:读页面和组件、分析依赖关系、修改文件、执行检查,再根据报错继续修正。

中间既有模型请求,也有本地工具执行。看到 Agent 暂时没动静,先看它当前停在哪一步。

如果面板显示模型调用超时,就保留错误详情和请求编号,检查调用状态。如果已经拿到构建报错,就先围绕报错处理。如果工具正在等待权限或人工操作,则需要完成对应动作。

分清这些情况,才能避免没有必要的操作。构建失败时反复换接口,解决不了代码或环境问题;模型请求中断时,也不能据此认定刚才的文件修改没有执行。

尤其是任务已经改过文件的情况,继续之前先看文件差异和执行记录。如果修改已经完成,只剩检查没有跑完,就先确认能否从检查环节接着处理。

通道选择这类工作,可以少让人手动处理

对于模型调用本身,还有一部分恢复工作可以交给网关。

当某条调用路径暂时异常,而系统中存在其他符合请求要求的可用路径时,可以由网关按规则选择路径、尝试恢复,减少开发者手动切换配置的次数。

以我们 XylemNode 的 Auto 分组为例,它结合通道健康状态按策略选路,遇到通道异常时支持跨组重试,调用方继续使用原来的模型名。具体说明可以看官网的 Auto 分组介绍。

放到 Agent 开发流程里,这项能力承担的是模型请求的通道选择。开发者仍然决定任务使用哪个模型,也仍需由 Agent 管理工具执行和项目进度。

因此,通道恢复不能直接等同于整个开发任务恢复。已经写入的代码、运行过的命令,都需要结合实际状态继续处理;流式响应已经返回一部分时,也不能把重新请求理解成自动无缝续写。

接入时还应了解 Agent 和网关各自的重试规则,给尝试次数和总等待时间留出明确边界。这样,自动处理才不会变成一直等待,却不知道任务有没有进展。

最后看交付结果,别停在一句“已经完成”

用 Agent 开发 uni-app,最后还是要回到项目本身:改了哪些文件,修改是否符合要求,执行过哪些检查,目标平台上还有哪些内容需要验证。

可以要求 Agent 在结束时给出一份简短交代:

  • 本次修改涉及哪些文件,分别解决了什么问题;
  • 实际运行了哪些检查,结果是什么;
  • 还有哪些页面表现或平台兼容性需要人工验证。

命令行检查通过,就记录这项结果。页面有没有显示异常、交互是否符合预期,以及目标设备上的表现,还要通过相应环境确认。

项目上下文交代清楚,Agent 才更容易按现有约定做事;模型接入和恢复规则理顺,才能减少一部分开发过程中的打断;检查结果留下来,下次继续任务也更有依据。

对开发者来说,省心的体验很具体:少一些来回改配置,任务中断后能看清进度,结束时拿得到可以检查的结果。


本文由 XylemNode 团队整理,AI 辅助起草,技术依据见文中链接。

收起阅读 »

HAR 文件怎么打开?别人发来的抓包文件怎么接着分析

iOS

'''同事发来一个文件:接口报错.har,问"你帮我看看这个请求为啥 500"。文件收到了,双击却不知道用什么打开——浏览器拖进去没反应,记事本打开是一大坨 JSON,翻半天才找到那条出错的请求。这类场景在联调里出现得不少:抓的人有抓的工具,收的人拿到 HAR 文件或 pcap 文件,第一步就卡在"用什么打开"。

要解决的不只是"打开"。文件打开之后,怎么看懂内容、怎么和手头的成功请求做对比、怎么再交给下一个环节,是一条完整的链。下面按"拿到文件之后"的顺序,把这条链走一遍。

先认清手上的是什么文件

HAR 是已经解析好的请求清单。 全称 HTTP Archive,本质是一个 JSON 文件——浏览器 F12 的 Network 面板右键导出,Fiddler 也能导,每个请求的方法、URL、请求头、响应头、耗时都按结构化字段存在里面。它记录的是应用层的事:这个请求发了什么、回了什么。

HAR 的骨架长这样:

{  
  "log": {  
    "version": "1.2",  
    "entries": [  
      {  
        "request":  { "method": "POST", "url": "https://api.example.com/order", "headers": [] },  
        "response": { "status": 500, "content": { "text": "...", "encoding": "base64" } }  
      }  
    ]  
  }  
}

注意 response.content 里的字段——encoding: base64 表示这段内容是二进制或压缩数据,原样看是乱的,得先解码。这个坑后面还会碰到。

pcap / pcapng 是网卡上的原始数据包。 Wireshark、tcpdump 抓完存的就是它,二进制格式,记录的是从网卡视角看到的原始包——一个 HTTP 请求在它眼里是若干 TCP 段,想看懂得先按连接把包重组起来。所以 pcap 的"打开"和 HAR 完全不是一回事:HAR 打开是看请求,pcap 打开先要解决"哪些包属于同一条连接、这条连接是不是 HTTPS 密文"。

还有一类是各家工具的私有会话格式:Charles 的 .chls、Fiddler 的 .saz,只能用原工具打开。给别人传文件时,导出成 HAR 比传私有格式通用。

把文件导进来

拿到文件,打开方式无非两条:找对应格式的工具,或者找一个都能吃下的。

分头解决的路线是:HAR 拖进 Chrome DevTools 的 Network 面板能看,pcap 交给 Wireshark,私有格式回原工具。这套分工能用,但文件一多就散——同事可能发来 HAR,也可能发来 Wireshark 存的 pcapng,得先判断"这归谁管"。

另一条路是用 TraceEagle 的"导入会话",HAR 1.2、.pcap / .pcapng、常见抓包工具导出的会话文件都从这一个入口进来。导入后请求就躺在列表里,跟本机抓的没有区别:切视图、看解码、查主机档案、拿去对比,都在同一套界面里。上次看同事给的 HAR 文件就是这么处理的,不用先想"该装什么软件",导进来直接看。

打开之后:三步读明白

导入只是开始,真正要回答的是"这个请求为什么出错"。

看内容。 先把乱码清零。HAR 里的 body 常是 base64 或压缩过的(开头那个 encoding: base64 就是),pcap 里非 HTTP 的流量本身是一堆原始字节。自动解压、自动识别格式、protobuf 免 .proto 解析、私有帧自动拆这一套在这里直接复用——导入的会话和本机抓的走同一套解码,不用因为"文件是别人抓的"就换流程。

查来源。 分析别人的包,经常要判断"这个域名、这个 IP 是什么来头"。HAR 里每个请求的主机名、pcap 里每个对端 IP,点开都有主机档案:归属地、ASN、证书信息、技术栈。查一个陌生 IP 的归属,不用对着地址段猜。

做对比。 这是导入场景里最实用的一步:同事抓的失败请求和你抓的成功请求,可以跨会话放在一起逐行比——参数、请求头、响应体哪里不一样。比"你把请求截图发我看看"来回几个回合高效得多,接口偶发失败这类问题,两份记录摆一起经常一眼出答案。

和常见工具怎么分工

这几个工具各管一段,位置挺清楚。

Chrome DevTools 看自己刚导出的 HAR 顺手,即开即用;但它开不了 pcap,也没法把两份 HAR 放一起比对。Wireshark 是 pcap 的专业户,包级分析(重传、时延、连接状态)没有替代品,代价是看 HTTP 内容要自己 follow stream,遇到 HTTPS 还得先有解密密钥。Charles、Fiddler 开自家会话最舒服,能改能重放,跨格式就弱一些。

TraceEagle 的位置在中间:HAR 和 pcap 都能进,进来之后解码、档案、对比、再导出一路通。实际用法多是组合——Wireshark 看完包级异常,把 pcap 导进来解出 HTTP 明文;或者同事用 Charles 抓的包导出 HAR,导进来和本地这份做对比。

问得最多的几件事

Chrome 导出的 HAR 怎么没有响应内容? 导出时选项没带对——浏览器的导出项里有"带内容"和"不带内容"两种,只有带内容的那份包含响应体。给别人导 HAR 文件排查问题时,记得选带内容的那种。

Wireshark 抓的 pcapng 导进来是明文还是密文? 看文件里有没有密钥。pcapng 可以把 TLS 解密密钥(SSLKEYLOGFILE 那套)一起写进文件,带了就能解出明文,没带就是原始密文,只能看包的特征。请对方导文件时,有条件的话顺手把密钥带上。

HAR 和 pcap 能互相转换吗? 不能直接转。HAR 是应用层的请求清单,pcap 是网卡层的原始包,信息不在一个层面——从 pcap 出 HAR 要先解密再完整重组,HAR 倒回 pcap 缺的是全部底层细节。两种格式都收的工具,内部是分别解析,不是转换。

同事发来的私有格式打不开怎么办? 请对方在原工具里导一份 HAR。HAR 1.2 是通用交换格式,浏览器和主流代理工具都认——你要把包发出去时,也可以照这个思路来。

收个尾

拿到抓包文件的分析路径可以固定下来:先看扩展名——.har 是应用层清单,找个能看 JSON 会话的工具或直接导进来;.pcap / .pcapng 是原始包,交给能重组连接的工具;私有格式请对方转 HAR。然后看、比、导:解码看清内容,对比找出差异,再导出交给下一个环节。

下次再收到同事发来的抓包文件,从"是 HAR 还是 pcap"判断起,后面的路就是一条直线。'''

继续阅读 »

'''同事发来一个文件:接口报错.har,问"你帮我看看这个请求为啥 500"。文件收到了,双击却不知道用什么打开——浏览器拖进去没反应,记事本打开是一大坨 JSON,翻半天才找到那条出错的请求。这类场景在联调里出现得不少:抓的人有抓的工具,收的人拿到 HAR 文件或 pcap 文件,第一步就卡在"用什么打开"。

要解决的不只是"打开"。文件打开之后,怎么看懂内容、怎么和手头的成功请求做对比、怎么再交给下一个环节,是一条完整的链。下面按"拿到文件之后"的顺序,把这条链走一遍。

先认清手上的是什么文件

HAR 是已经解析好的请求清单。 全称 HTTP Archive,本质是一个 JSON 文件——浏览器 F12 的 Network 面板右键导出,Fiddler 也能导,每个请求的方法、URL、请求头、响应头、耗时都按结构化字段存在里面。它记录的是应用层的事:这个请求发了什么、回了什么。

HAR 的骨架长这样:

{  
  "log": {  
    "version": "1.2",  
    "entries": [  
      {  
        "request":  { "method": "POST", "url": "https://api.example.com/order", "headers": [] },  
        "response": { "status": 500, "content": { "text": "...", "encoding": "base64" } }  
      }  
    ]  
  }  
}

注意 response.content 里的字段——encoding: base64 表示这段内容是二进制或压缩数据,原样看是乱的,得先解码。这个坑后面还会碰到。

pcap / pcapng 是网卡上的原始数据包。 Wireshark、tcpdump 抓完存的就是它,二进制格式,记录的是从网卡视角看到的原始包——一个 HTTP 请求在它眼里是若干 TCP 段,想看懂得先按连接把包重组起来。所以 pcap 的"打开"和 HAR 完全不是一回事:HAR 打开是看请求,pcap 打开先要解决"哪些包属于同一条连接、这条连接是不是 HTTPS 密文"。

还有一类是各家工具的私有会话格式:Charles 的 .chls、Fiddler 的 .saz,只能用原工具打开。给别人传文件时,导出成 HAR 比传私有格式通用。

把文件导进来

拿到文件,打开方式无非两条:找对应格式的工具,或者找一个都能吃下的。

分头解决的路线是:HAR 拖进 Chrome DevTools 的 Network 面板能看,pcap 交给 Wireshark,私有格式回原工具。这套分工能用,但文件一多就散——同事可能发来 HAR,也可能发来 Wireshark 存的 pcapng,得先判断"这归谁管"。

另一条路是用 TraceEagle 的"导入会话",HAR 1.2、.pcap / .pcapng、常见抓包工具导出的会话文件都从这一个入口进来。导入后请求就躺在列表里,跟本机抓的没有区别:切视图、看解码、查主机档案、拿去对比,都在同一套界面里。上次看同事给的 HAR 文件就是这么处理的,不用先想"该装什么软件",导进来直接看。

打开之后:三步读明白

导入只是开始,真正要回答的是"这个请求为什么出错"。

看内容。 先把乱码清零。HAR 里的 body 常是 base64 或压缩过的(开头那个 encoding: base64 就是),pcap 里非 HTTP 的流量本身是一堆原始字节。自动解压、自动识别格式、protobuf 免 .proto 解析、私有帧自动拆这一套在这里直接复用——导入的会话和本机抓的走同一套解码,不用因为"文件是别人抓的"就换流程。

查来源。 分析别人的包,经常要判断"这个域名、这个 IP 是什么来头"。HAR 里每个请求的主机名、pcap 里每个对端 IP,点开都有主机档案:归属地、ASN、证书信息、技术栈。查一个陌生 IP 的归属,不用对着地址段猜。

做对比。 这是导入场景里最实用的一步:同事抓的失败请求和你抓的成功请求,可以跨会话放在一起逐行比——参数、请求头、响应体哪里不一样。比"你把请求截图发我看看"来回几个回合高效得多,接口偶发失败这类问题,两份记录摆一起经常一眼出答案。

和常见工具怎么分工

这几个工具各管一段,位置挺清楚。

Chrome DevTools 看自己刚导出的 HAR 顺手,即开即用;但它开不了 pcap,也没法把两份 HAR 放一起比对。Wireshark 是 pcap 的专业户,包级分析(重传、时延、连接状态)没有替代品,代价是看 HTTP 内容要自己 follow stream,遇到 HTTPS 还得先有解密密钥。Charles、Fiddler 开自家会话最舒服,能改能重放,跨格式就弱一些。

TraceEagle 的位置在中间:HAR 和 pcap 都能进,进来之后解码、档案、对比、再导出一路通。实际用法多是组合——Wireshark 看完包级异常,把 pcap 导进来解出 HTTP 明文;或者同事用 Charles 抓的包导出 HAR,导进来和本地这份做对比。

问得最多的几件事

Chrome 导出的 HAR 怎么没有响应内容? 导出时选项没带对——浏览器的导出项里有"带内容"和"不带内容"两种,只有带内容的那份包含响应体。给别人导 HAR 文件排查问题时,记得选带内容的那种。

Wireshark 抓的 pcapng 导进来是明文还是密文? 看文件里有没有密钥。pcapng 可以把 TLS 解密密钥(SSLKEYLOGFILE 那套)一起写进文件,带了就能解出明文,没带就是原始密文,只能看包的特征。请对方导文件时,有条件的话顺手把密钥带上。

HAR 和 pcap 能互相转换吗? 不能直接转。HAR 是应用层的请求清单,pcap 是网卡层的原始包,信息不在一个层面——从 pcap 出 HAR 要先解密再完整重组,HAR 倒回 pcap 缺的是全部底层细节。两种格式都收的工具,内部是分别解析,不是转换。

同事发来的私有格式打不开怎么办? 请对方在原工具里导一份 HAR。HAR 1.2 是通用交换格式,浏览器和主流代理工具都认——你要把包发出去时,也可以照这个思路来。

收个尾

拿到抓包文件的分析路径可以固定下来:先看扩展名——.har 是应用层清单,找个能看 JSON 会话的工具或直接导进来;.pcap / .pcapng 是原始包,交给能重组连接的工具;私有格式请对方转 HAR。然后看、比、导:解码看清内容,对比找出差异,再导出交给下一个环节。

下次再收到同事发来的抓包文件,从"是 HAR 还是 pcap"判断起,后面的路就是一条直线。'''

收起阅读 »

如何在Canvas上实现涂抹效果

canvas

问题现象
在移动应用开发中,屏幕涂抹交互(如签名、涂鸦、擦除)是提升用户体验的关键功能。HarmonyOS的Canvas组件结合触摸事件监听与手势识别,为开发者提供了低门槛、高性能的2D图形绘制能力。本文将通过以下常见应用场景,详解如何在Canvas上实现涂抹效果:

场景一:如何结合手势或者事件实现滑动路径绘制?
场景二:如何撤销已绘制的路径?
场景三:如何擦除部分绘制内容?
背景知识
Canvas:提供画布组件,用于自定义绘制图形,开发者使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制,绘制对象可以是基础形状、文本、图片等。
lineTo:从当前点到指定点进行路径连接。
globalCompositeOperation:设置合成操作的方式,默认值为source-over。
onTouch:手指触摸动作触发该回调。可以获取滑动过的路径坐标点。
PanGesture:滑动手势事件,当滑动的最小距离达到设定的最小值时触发滑动手势事件。
解决方案
场景一:结合手势或者事件实现路径绘制。
Canvas组件可以绑定触摸事件和滑动手势来获取手指按压时的坐标,在事件触发过程中可以根据event对象获取到在屏幕上触摸的点,再结合Canvas的lineTo方法就可以把手指移动过程中的路径给记录下来,达到手指滑动屏幕就绘制的效果。

onTouch实现如下:
@Entry
@Component
struct CanvasTouch {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.onTouch((event: TouchEvent) => {
// 获取到触摸的坐标点
let x = event.touches[0].x;
let y = event.touches[0].y;
if (event.type == TouchType.Down) {
// 手指按下时画布移动到当前坐标点
this.context.beginPath();
this.context.moveTo(x, y);
}
if (event.type === TouchType.Move) {
// 手指移动时画布用线条连接到当前坐标点
this.context.lineTo(x, y);
this.context.stroke();
}
if (event.type === TouchType.Up) {
// 手指抬起时生成闭合路径
this.context.closePath();
}
})
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);
};
}
}
PanGesture实现如下:
@Entry
@Component
struct CanvasPanGesture {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.gesture(
PanGesture({ fingers: 1 })
.onActionStart((event: GestureEvent) => {
let x = event.fingerList[0].localX;
let y = event.fingerList[0].localY;
// 手指按下时画布移动到当前坐标点
this.context.beginPath();
this.context.moveTo(x, y);
})
.onActionUpdate((event: GestureEvent) => {
let x = event.fingerList[0].localX;
let y = event.fingerList[0].localY;
// 手指移动时画布用线条连接到当前坐标点
this.context.lineTo(x, y);
this.context.stroke();
})
.onActionEnd(() => {
// 手指抬起时生成闭合路径
this.context.closePath();
})
)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);
};
}
}
实现效果如下:

点击放大

场景二:撤销绘制的路径。
在一些签名场景,如果用户绘制错误需要重新绘制,直接使用clearRect方法清空画布体验不够友好,需要仅撤销最新的绘制路径,这时可以使用数组来存储绘制过程中的路径,然后点击撤销时移除最新路径,最后重绘剩余路径,这样即可实现撤销绘制功能。示例代码参考如下:

interface Point {
x: number;
y: number;
}

export class DrawingPath {
points: Point[] = [];
}

@Entry
@Component
struct CanvasCancelDraw {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private paths: DrawingPath[] = [];

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('570vp')
.borderRadius(16)
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.onTouch((event: TouchEvent) => {
let x = event.touches[0].x;
let y = event.touches[0].y;
if (event.type == TouchType.Down) {
// 按下时新建一条路径
const path = new DrawingPath();
path.points.push({ x: x, y: y });
this.paths.push(path);
this.context.beginPath();
this.context.moveTo(x, y);
}
if (event.type === TouchType.Move) {
// 移动时把当前的坐标点放入最新的路径中
this.paths[this.paths.length - 1].points.push({ x: x, y: y });
this.context.lineTo(x, y);
this.context.stroke();
}
if (event.type === TouchType.Up) {
this.context.closePath();
}
});

  Row({ space: 20 }) {  
    Button('清空')  
      .layoutWeight(1)  
      .onClick(() => {  
        this.paths = [];  
        this.context.clearRect(0, 0, this.context.width, this.context.height);  
      });  
    Button('撤销')  
      .layoutWeight(1)  
      .onClick(() => {  
        if (this.paths.length < 1) {  
          return;  
        }  
        this.paths.pop()!!;  
        this.context.clearRect(0, 0, this.context.width, this.context.height);  
        // 重绘保存的路径  
        this.paths.forEach((path) => {  
          if (path.points.length < 1) {  
            return;  
          }  
          this.context.beginPath();  
          this.context.moveTo(path.points[0].x, path.points[0].y);  
          for (let i = 0; i < path.points.length; i++) {  
            this.context.lineTo(path.points[i].x, path.points[i].y);  
            this.context.stroke();  
          }  
          this.context.closePath();  
        });  
      });  
  }.width('100%')  
  .justifyContent(FlexAlign.Center)  
  .margin(20);  
}.padding(16);  

}
}
https://coub.com/embed/kwki4m8ihy
https://coub.com/embed/1dc59ftpsz
https://coub.com/embed/1eg6k9knxq
https://coub.com/embed/ffcvsoqgy9
https://coub.com/embed/mtf20e2xfs
https://coub.com/embed/vcdyj2hsux
https://coub.com/embed/p8u435h7k0
https://coub.com/embed/53uc97iqzh
https://coub.com/embed/slklk9dip7
https://coub.com/embed/x5r8qua9dn
https://coub.com/embed/suzzf1tabm
https://coub.com/embed/iwlz1k4x5c
https://coub.com/embed/esio81b16u
https://coub.com/embed/08isxtzg6y
https://coub.com/embed/5t2ou1h0u1
https://coub.com/embed/2i826b72cf
https://coub.com/embed/wl4g0a80wd
https://coub.com/embed/noxtktrsld
https://coub.com/embed/4arm33ehej
https://coub.com/embed/4m5uhixzc6
https://coub.com/embed/z4hni578js
https://coub.com/embed/vc1lp4h9wv
https://coub.com/embed/ahq7q7smb9
https://coub.com/embed/s3agn3nkdx
https://coub.com/embed/0tpmvd839f
https://coub.com/embed/a52spj6rs2
https://coub.com/embed/mytvxbwrus
https://coub.com/embed/hto4op5jpb
https://coub.com/embed/5i13w22bho
https://coub.com/embed/ng8whwdnmh
https://coub.com/embed/4tepeaanko
https://coub.com/embed/gq6dpvlatn
https://coub.com/embed/n0xesp3qe7
https://coub.com/embed/i4ciaef3z2
https://coub.com/embed/h6tzz4imsh
https://coub.com/embed/5oh9y0o849
https://coub.com/embed/di1lvxi6rs
https://coub.com/embed/dvr0d1spum
https://coub.com/embed/a0chxn6q37
https://coub.com/embed/9kdpqt8gi3
https://coub.com/embed/wwtpzdcshr
https://coub.com/embed/i3j774nb78
https://coub.com/embed/9vdne44f87
https://coub.com/embed/di14nasb4i
https://coub.com/embed/tjdbi2z10f
https://coub.com/embed/xymgbl98l3
https://coub.com/embed/d46w9vg6y2
https://coub.com/embed/7kjd2r109r
https://coub.com/embed/ewtrtmuecq
https://coub.com/embed/x44fcmuuwa
https://coub.com/embed/ajx3cjloat
https://coub.com/embed/s9xf0u8gl8
https://coub.com/embed/9ogtzmbj73
https://coub.com/embed/2th49b4ms0
https://coub.com/embed/ke6047rz7q
https://coub.com/embed/m2oqeg89q8
https://coub.com/embed/774g3ow4sb
https://coub.com/embed/7548j9l7nl
https://coub.com/embed/zjz1qsyin2
https://coub.com/embed/xik7e7nili
https://coub.com/embed/zp95r4y15i
https://coub.com/embed/adde9a38t1
https://coub.com/embed/1qarvak9lx
https://coub.com/embed/dko4y9u0yf
https://coub.com/embed/ypw9dtvmab
https://coub.com/embed/8ewvlnn2b6
https://coub.com/embed/5s2dev0lra
https://coub.com/embed/c9fihrkf9u
https://coub.com/embed/xzeak1mhkt
https://coub.com/embed/t9dmppwno6
https://coub.com/embed/fz18zmri11
https://coub.com/embed/75espd6kkd
https://coub.com/embed/x2bbs1j2jb
https://coub.com/embed/fe3fwdnu88
https://coub.com/embed/ckzbjutpkj
https://coub.com/embed/qf0w3gxvy2
https://coub.com/embed/ux4gip7l5a
https://coub.com/embed/qavq5q9b1e
https://coub.com/embed/06dq1lpfmv
https://coub.com/embed/h9okejkccd
https://coub.com/embed/z59288b3bc
https://coub.com/embed/jdsgw25pj5
https://coub.com/embed/20ms41ljxh
https://coub.com/embed/cyjima3av9
https://coub.com/embed/xhymuinpm8
https://coub.com/embed/gqqk4335y1
https://coub.com/embed/9uflyvtfzv
https://coub.com/embed/ogd56p24ng
https://coub.com/embed/zydhfymmc3
https://coub.com/embed/pzd0ac0kg5
https://coub.com/embed/obdv3r4cy0
https://coub.com/embed/ka2mdh07zg
https://coub.com/embed/k8417q6t33
https://coub.com/embed/b797hoxnvb
https://coub.com/embed/p3wb9ayf0i
https://coub.com/embed/cylp42xa95
https://coub.com/embed/5vyv5859hb
https://coub.com/embed/uolo157r87
https://coub.com/embed/7bmp0nxmze
https://coub.com/embed/d2q6yzkrli
https://coub.com/embed/zs1hg1bhrv
https://coub.com/embed/yz4tqllp0u
https://coub.com/embed/a2kvibvldp
https://coub.com/embed/09kqx4xcfx
https://coub.com/embed/2c1zyd3kx3
https://coub.com/embed/wep5eg226t
https://coub.com/embed/su365ycwsu
https://coub.com/embed/nzdz5k3kr0
https://coub.com/embed/qg568ebu9j
https://coub.com/embed/md5pqvk50f
https://coub.com/embed/0gt0ay1zk3
https://coub.com/embed/hecylej9t3
https://coub.com/embed/wmxvxv1kkj
https://coub.com/embed/bxxpl0brtv
https://coub.com/embed/pholjf70ut
https://coub.com/embed/m8l2xwwy39
https://coub.com/embed/1c1x2w7t08
https://coub.com/embed/53mubpegym
https://coub.com/embed/on6slxqrt5
https://coub.com/embed/55q881xx6l
https://coub.com/embed/s77w2ok24r
https://coub.com/embed/4e9hy0237n
https://coub.com/embed/3entpjmdbw
https://coub.com/embed/8besg49zbq
https://coub.com/embed/usqat13gtl
https://coub.com/embed/66yxwx6676
https://coub.com/embed/np30bv52rn
https://coub.com/embed/3nk88ivl0b
https://coub.com/embed/83eyn5yj6r
https://coub.com/embed/bymmywq8fr
https://coub.com/embed/9bj0bp5xii
https://coub.com/embed/agvyb0zhme
https://coub.com/embed/8bcvp8upb3
https://coub.com/embed/c8p7zv5zan
https://coub.com/embed/yry7la9tco
https://coub.com/embed/ho2dopbp7j
https://coub.com/embed/amubk2uwuc
https://coub.com/embed/2jwoy1e4u4
https://coub.com/embed/ezz7hv58aq
https://coub.com/embed/sxb8y6f7cm
https://coub.com/embed/y4kvuhob20
https://coub.com/embed/t3iyg4zbk6
https://coub.com/embed/hlu3q922ul
https://coub.com/embed/wr5t1i5hjn
https://coub.com/embed/ig12bx6jem
https://coub.com/embed/elh7b4dm2v
https://coub.com/embed/p3aozbkqis
https://coub.com/embed/1x387gqa9h
https://coub.com/embed/11vq0o0urq
https://coub.com/embed/n95945i2xa
https://coub.com/embed/kwxot0bqpd
https://coub.com/embed/0bzsnthjf8
https://coub.com/embed/l6n7cci0v1
https://coub.com/embed/e33t42b2e8
https://coub.com/embed/uufau92iew
https://coub.com/embed/8nu2gzpi87
https://coub.com/embed/bzvc0eyha5
https://coub.com/embed/m7i31eulsq
https://coub.com/embed/1r27esow61
https://coub.com/embed/v18hqlwcpz
https://coub.com/embed/hltc1g5iav
https://coub.com/embed/eq093ohvb8
https://coub.com/embed/i44t54uooo
https://coub.com/embed/1p2epqlik8
https://coub.com/embed/r7s85h5tgs
https://coub.com/embed/x3a913pgtg
https://coub.com/embed/rm3vt5vimg
https://coub.com/embed/15wu728znl
https://coub.com/embed/2iwy8dlso1
https://coub.com/embed/9ecofudaq2
https://coub.com/embed/odtibqvqqb
https://coub.com/embed/kfit182kgd
https://coub.com/embed/oyuutd8klx
https://coub.com/embed/2ab3tdnsq2
https://coub.com/embed/0lxrs8kij0
https://coub.com/embed/o1b4imc38s
https://coub.com/embed/hmvpibka9g
https://coub.com/embed/pvj08u1amk
https://coub.com/embed/g3fxbal3j0
https://coub.com/embed/hunlqq2jwi
https://coub.com/embed/wsrr0y4x1q
https://coub.com/embed/kg284yqkff
https://coub.com/embed/hbiuts4e7e
https://coub.com/embed/cl8t4qvz2x
https://coub.com/embed/ydk9cyiff7
https://coub.com/embed/ek17k4yytm
https://coub.com/embed/y6t00hom1a
https://coub.com/embed/19q7wp5i00
https://coub.com/embed/4h8bxs9zz7
https://coub.com/embed/jwflpmph2z
https://coub.com/embed/f213jay0fp
https://coub.com/embed/bsf3lctm94
https://coub.com/embed/e83n1arbod
https://coub.com/embed/swembduv5h
https://coub.com/embed/n3fq7zzf5w
https://coub.com/embed/9lio0yr585
https://coub.com/embed/o4fakj61o0
https://coub.com/embed/1hjvwr455z
https://coub.com/embed/xzugpc1qds
https://coub.com/embed/ev7098ev03
https://coub.com/embed/kx4fsmb4b6
https://coub.com/embed/gcorgj2lp8
https://coub.com/embed/gsk38h0x1t
https://coub.com/embed/1gzcnq0drf
https://coub.com/embed/8kk1rohlkq
https://coub.com/embed/95lk677xts
https://coub.com/embed/klzoirlr0i
https://coub.com/embed/ruh9e6e23z
https://coub.com/embed/kaffn57ll3
https://coub.com/embed/wtwakxr59k
https://coub.com/embed/oq7e9iq28m
https://coub.com/embed/xf0mc6ti8q
https://coub.com/embed/ptjtimwt1u
https://coub.com/embed/uu6kt9484i
https://coub.com/embed/1upd83q7xu
https://coub.com/embed/kry12uzw3v
https://coub.com/embed/dtg6xjn98z
https://coub.com/embed/sn0yhm79wv
https://coub.com/embed/9gqnb8624w
https://coub.com/embed/p32o9d4k7v
https://coub.com/embed/e4p0k8n96b
https://coub.com/embed/ordooqx1q4
https://coub.com/embed/w0en3j9kmt
https://coub.com/embed/z925vaal0x
https://coub.com/embed/llsxxio2o3
https://coub.com/embed/xydiujzwro
https://coub.com/embed/2e486z583s
https://coub.com/embed/b850qpvdwf
https://coub.com/embed/uasf7vsd00
https://coub.com/embed/ne9r9ql0ke
https://coub.com/embed/wrb0dawhk3
https://coub.com/embed/e7vo2n3rv0
https://coub.com/embed/sq5lb2quya
https://coub.com/embed/q7er5zdf9c
https://coub.com/embed/hijacyi6zc

继续阅读 »

问题现象
在移动应用开发中,屏幕涂抹交互(如签名、涂鸦、擦除)是提升用户体验的关键功能。HarmonyOS的Canvas组件结合触摸事件监听与手势识别,为开发者提供了低门槛、高性能的2D图形绘制能力。本文将通过以下常见应用场景,详解如何在Canvas上实现涂抹效果:

场景一:如何结合手势或者事件实现滑动路径绘制?
场景二:如何撤销已绘制的路径?
场景三:如何擦除部分绘制内容?
背景知识
Canvas:提供画布组件,用于自定义绘制图形,开发者使用CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象在Canvas组件上进行绘制,绘制对象可以是基础形状、文本、图片等。
lineTo:从当前点到指定点进行路径连接。
globalCompositeOperation:设置合成操作的方式,默认值为source-over。
onTouch:手指触摸动作触发该回调。可以获取滑动过的路径坐标点。
PanGesture:滑动手势事件,当滑动的最小距离达到设定的最小值时触发滑动手势事件。
解决方案
场景一:结合手势或者事件实现路径绘制。
Canvas组件可以绑定触摸事件和滑动手势来获取手指按压时的坐标,在事件触发过程中可以根据event对象获取到在屏幕上触摸的点,再结合Canvas的lineTo方法就可以把手指移动过程中的路径给记录下来,达到手指滑动屏幕就绘制的效果。

onTouch实现如下:
@Entry
@Component
struct CanvasTouch {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.onTouch((event: TouchEvent) => {
// 获取到触摸的坐标点
let x = event.touches[0].x;
let y = event.touches[0].y;
if (event.type == TouchType.Down) {
// 手指按下时画布移动到当前坐标点
this.context.beginPath();
this.context.moveTo(x, y);
}
if (event.type === TouchType.Move) {
// 手指移动时画布用线条连接到当前坐标点
this.context.lineTo(x, y);
this.context.stroke();
}
if (event.type === TouchType.Up) {
// 手指抬起时生成闭合路径
this.context.closePath();
}
})
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);
};
}
}
PanGesture实现如下:
@Entry
@Component
struct CanvasPanGesture {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.gesture(
PanGesture({ fingers: 1 })
.onActionStart((event: GestureEvent) => {
let x = event.fingerList[0].localX;
let y = event.fingerList[0].localY;
// 手指按下时画布移动到当前坐标点
this.context.beginPath();
this.context.moveTo(x, y);
})
.onActionUpdate((event: GestureEvent) => {
let x = event.fingerList[0].localX;
let y = event.fingerList[0].localY;
// 手指移动时画布用线条连接到当前坐标点
this.context.lineTo(x, y);
this.context.stroke();
})
.onActionEnd(() => {
// 手指抬起时生成闭合路径
this.context.closePath();
})
)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]);
};
}
}
实现效果如下:

点击放大

场景二:撤销绘制的路径。
在一些签名场景,如果用户绘制错误需要重新绘制,直接使用clearRect方法清空画布体验不够友好,需要仅撤销最新的绘制路径,这时可以使用数组来存储绘制过程中的路径,然后点击撤销时移除最新路径,最后重绘剩余路径,这样即可实现撤销绘制功能。示例代码参考如下:

interface Point {
x: number;
y: number;
}

export class DrawingPath {
points: Point[] = [];
}

@Entry
@Component
struct CanvasCancelDraw {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private paths: DrawingPath[] = [];

build() {
Column() {
Canvas(this.context)
.width('100%')
.height('570vp')
.borderRadius(16)
.backgroundColor($r('sys.color.comp_background_focus'))
.onReady(() => {
this.context.lineWidth = 10;
this.context.strokeStyle = '#0000ff';
})
.onTouch((event: TouchEvent) => {
let x = event.touches[0].x;
let y = event.touches[0].y;
if (event.type == TouchType.Down) {
// 按下时新建一条路径
const path = new DrawingPath();
path.points.push({ x: x, y: y });
this.paths.push(path);
this.context.beginPath();
this.context.moveTo(x, y);
}
if (event.type === TouchType.Move) {
// 移动时把当前的坐标点放入最新的路径中
this.paths[this.paths.length - 1].points.push({ x: x, y: y });
this.context.lineTo(x, y);
this.context.stroke();
}
if (event.type === TouchType.Up) {
this.context.closePath();
}
});

  Row({ space: 20 }) {  
    Button('清空')  
      .layoutWeight(1)  
      .onClick(() => {  
        this.paths = [];  
        this.context.clearRect(0, 0, this.context.width, this.context.height);  
      });  
    Button('撤销')  
      .layoutWeight(1)  
      .onClick(() => {  
        if (this.paths.length < 1) {  
          return;  
        }  
        this.paths.pop()!!;  
        this.context.clearRect(0, 0, this.context.width, this.context.height);  
        // 重绘保存的路径  
        this.paths.forEach((path) => {  
          if (path.points.length < 1) {  
            return;  
          }  
          this.context.beginPath();  
          this.context.moveTo(path.points[0].x, path.points[0].y);  
          for (let i = 0; i < path.points.length; i++) {  
            this.context.lineTo(path.points[i].x, path.points[i].y);  
            this.context.stroke();  
          }  
          this.context.closePath();  
        });  
      });  
  }.width('100%')  
  .justifyContent(FlexAlign.Center)  
  .margin(20);  
}.padding(16);  

}
}
https://coub.com/embed/kwki4m8ihy
https://coub.com/embed/1dc59ftpsz
https://coub.com/embed/1eg6k9knxq
https://coub.com/embed/ffcvsoqgy9
https://coub.com/embed/mtf20e2xfs
https://coub.com/embed/vcdyj2hsux
https://coub.com/embed/p8u435h7k0
https://coub.com/embed/53uc97iqzh
https://coub.com/embed/slklk9dip7
https://coub.com/embed/x5r8qua9dn
https://coub.com/embed/suzzf1tabm
https://coub.com/embed/iwlz1k4x5c
https://coub.com/embed/esio81b16u
https://coub.com/embed/08isxtzg6y
https://coub.com/embed/5t2ou1h0u1
https://coub.com/embed/2i826b72cf
https://coub.com/embed/wl4g0a80wd
https://coub.com/embed/noxtktrsld
https://coub.com/embed/4arm33ehej
https://coub.com/embed/4m5uhixzc6
https://coub.com/embed/z4hni578js
https://coub.com/embed/vc1lp4h9wv
https://coub.com/embed/ahq7q7smb9
https://coub.com/embed/s3agn3nkdx
https://coub.com/embed/0tpmvd839f
https://coub.com/embed/a52spj6rs2
https://coub.com/embed/mytvxbwrus
https://coub.com/embed/hto4op5jpb
https://coub.com/embed/5i13w22bho
https://coub.com/embed/ng8whwdnmh
https://coub.com/embed/4tepeaanko
https://coub.com/embed/gq6dpvlatn
https://coub.com/embed/n0xesp3qe7
https://coub.com/embed/i4ciaef3z2
https://coub.com/embed/h6tzz4imsh
https://coub.com/embed/5oh9y0o849
https://coub.com/embed/di1lvxi6rs
https://coub.com/embed/dvr0d1spum
https://coub.com/embed/a0chxn6q37
https://coub.com/embed/9kdpqt8gi3
https://coub.com/embed/wwtpzdcshr
https://coub.com/embed/i3j774nb78
https://coub.com/embed/9vdne44f87
https://coub.com/embed/di14nasb4i
https://coub.com/embed/tjdbi2z10f
https://coub.com/embed/xymgbl98l3
https://coub.com/embed/d46w9vg6y2
https://coub.com/embed/7kjd2r109r
https://coub.com/embed/ewtrtmuecq
https://coub.com/embed/x44fcmuuwa
https://coub.com/embed/ajx3cjloat
https://coub.com/embed/s9xf0u8gl8
https://coub.com/embed/9ogtzmbj73
https://coub.com/embed/2th49b4ms0
https://coub.com/embed/ke6047rz7q
https://coub.com/embed/m2oqeg89q8
https://coub.com/embed/774g3ow4sb
https://coub.com/embed/7548j9l7nl
https://coub.com/embed/zjz1qsyin2
https://coub.com/embed/xik7e7nili
https://coub.com/embed/zp95r4y15i
https://coub.com/embed/adde9a38t1
https://coub.com/embed/1qarvak9lx
https://coub.com/embed/dko4y9u0yf
https://coub.com/embed/ypw9dtvmab
https://coub.com/embed/8ewvlnn2b6
https://coub.com/embed/5s2dev0lra
https://coub.com/embed/c9fihrkf9u
https://coub.com/embed/xzeak1mhkt
https://coub.com/embed/t9dmppwno6
https://coub.com/embed/fz18zmri11
https://coub.com/embed/75espd6kkd
https://coub.com/embed/x2bbs1j2jb
https://coub.com/embed/fe3fwdnu88
https://coub.com/embed/ckzbjutpkj
https://coub.com/embed/qf0w3gxvy2
https://coub.com/embed/ux4gip7l5a
https://coub.com/embed/qavq5q9b1e
https://coub.com/embed/06dq1lpfmv
https://coub.com/embed/h9okejkccd
https://coub.com/embed/z59288b3bc
https://coub.com/embed/jdsgw25pj5
https://coub.com/embed/20ms41ljxh
https://coub.com/embed/cyjima3av9
https://coub.com/embed/xhymuinpm8
https://coub.com/embed/gqqk4335y1
https://coub.com/embed/9uflyvtfzv
https://coub.com/embed/ogd56p24ng
https://coub.com/embed/zydhfymmc3
https://coub.com/embed/pzd0ac0kg5
https://coub.com/embed/obdv3r4cy0
https://coub.com/embed/ka2mdh07zg
https://coub.com/embed/k8417q6t33
https://coub.com/embed/b797hoxnvb
https://coub.com/embed/p3wb9ayf0i
https://coub.com/embed/cylp42xa95
https://coub.com/embed/5vyv5859hb
https://coub.com/embed/uolo157r87
https://coub.com/embed/7bmp0nxmze
https://coub.com/embed/d2q6yzkrli
https://coub.com/embed/zs1hg1bhrv
https://coub.com/embed/yz4tqllp0u
https://coub.com/embed/a2kvibvldp
https://coub.com/embed/09kqx4xcfx
https://coub.com/embed/2c1zyd3kx3
https://coub.com/embed/wep5eg226t
https://coub.com/embed/su365ycwsu
https://coub.com/embed/nzdz5k3kr0
https://coub.com/embed/qg568ebu9j
https://coub.com/embed/md5pqvk50f
https://coub.com/embed/0gt0ay1zk3
https://coub.com/embed/hecylej9t3
https://coub.com/embed/wmxvxv1kkj
https://coub.com/embed/bxxpl0brtv
https://coub.com/embed/pholjf70ut
https://coub.com/embed/m8l2xwwy39
https://coub.com/embed/1c1x2w7t08
https://coub.com/embed/53mubpegym
https://coub.com/embed/on6slxqrt5
https://coub.com/embed/55q881xx6l
https://coub.com/embed/s77w2ok24r
https://coub.com/embed/4e9hy0237n
https://coub.com/embed/3entpjmdbw
https://coub.com/embed/8besg49zbq
https://coub.com/embed/usqat13gtl
https://coub.com/embed/66yxwx6676
https://coub.com/embed/np30bv52rn
https://coub.com/embed/3nk88ivl0b
https://coub.com/embed/83eyn5yj6r
https://coub.com/embed/bymmywq8fr
https://coub.com/embed/9bj0bp5xii
https://coub.com/embed/agvyb0zhme
https://coub.com/embed/8bcvp8upb3
https://coub.com/embed/c8p7zv5zan
https://coub.com/embed/yry7la9tco
https://coub.com/embed/ho2dopbp7j
https://coub.com/embed/amubk2uwuc
https://coub.com/embed/2jwoy1e4u4
https://coub.com/embed/ezz7hv58aq
https://coub.com/embed/sxb8y6f7cm
https://coub.com/embed/y4kvuhob20
https://coub.com/embed/t3iyg4zbk6
https://coub.com/embed/hlu3q922ul
https://coub.com/embed/wr5t1i5hjn
https://coub.com/embed/ig12bx6jem
https://coub.com/embed/elh7b4dm2v
https://coub.com/embed/p3aozbkqis
https://coub.com/embed/1x387gqa9h
https://coub.com/embed/11vq0o0urq
https://coub.com/embed/n95945i2xa
https://coub.com/embed/kwxot0bqpd
https://coub.com/embed/0bzsnthjf8
https://coub.com/embed/l6n7cci0v1
https://coub.com/embed/e33t42b2e8
https://coub.com/embed/uufau92iew
https://coub.com/embed/8nu2gzpi87
https://coub.com/embed/bzvc0eyha5
https://coub.com/embed/m7i31eulsq
https://coub.com/embed/1r27esow61
https://coub.com/embed/v18hqlwcpz
https://coub.com/embed/hltc1g5iav
https://coub.com/embed/eq093ohvb8
https://coub.com/embed/i44t54uooo
https://coub.com/embed/1p2epqlik8
https://coub.com/embed/r7s85h5tgs
https://coub.com/embed/x3a913pgtg
https://coub.com/embed/rm3vt5vimg
https://coub.com/embed/15wu728znl
https://coub.com/embed/2iwy8dlso1
https://coub.com/embed/9ecofudaq2
https://coub.com/embed/odtibqvqqb
https://coub.com/embed/kfit182kgd
https://coub.com/embed/oyuutd8klx
https://coub.com/embed/2ab3tdnsq2
https://coub.com/embed/0lxrs8kij0
https://coub.com/embed/o1b4imc38s
https://coub.com/embed/hmvpibka9g
https://coub.com/embed/pvj08u1amk
https://coub.com/embed/g3fxbal3j0
https://coub.com/embed/hunlqq2jwi
https://coub.com/embed/wsrr0y4x1q
https://coub.com/embed/kg284yqkff
https://coub.com/embed/hbiuts4e7e
https://coub.com/embed/cl8t4qvz2x
https://coub.com/embed/ydk9cyiff7
https://coub.com/embed/ek17k4yytm
https://coub.com/embed/y6t00hom1a
https://coub.com/embed/19q7wp5i00
https://coub.com/embed/4h8bxs9zz7
https://coub.com/embed/jwflpmph2z
https://coub.com/embed/f213jay0fp
https://coub.com/embed/bsf3lctm94
https://coub.com/embed/e83n1arbod
https://coub.com/embed/swembduv5h
https://coub.com/embed/n3fq7zzf5w
https://coub.com/embed/9lio0yr585
https://coub.com/embed/o4fakj61o0
https://coub.com/embed/1hjvwr455z
https://coub.com/embed/xzugpc1qds
https://coub.com/embed/ev7098ev03
https://coub.com/embed/kx4fsmb4b6
https://coub.com/embed/gcorgj2lp8
https://coub.com/embed/gsk38h0x1t
https://coub.com/embed/1gzcnq0drf
https://coub.com/embed/8kk1rohlkq
https://coub.com/embed/95lk677xts
https://coub.com/embed/klzoirlr0i
https://coub.com/embed/ruh9e6e23z
https://coub.com/embed/kaffn57ll3
https://coub.com/embed/wtwakxr59k
https://coub.com/embed/oq7e9iq28m
https://coub.com/embed/xf0mc6ti8q
https://coub.com/embed/ptjtimwt1u
https://coub.com/embed/uu6kt9484i
https://coub.com/embed/1upd83q7xu
https://coub.com/embed/kry12uzw3v
https://coub.com/embed/dtg6xjn98z
https://coub.com/embed/sn0yhm79wv
https://coub.com/embed/9gqnb8624w
https://coub.com/embed/p32o9d4k7v
https://coub.com/embed/e4p0k8n96b
https://coub.com/embed/ordooqx1q4
https://coub.com/embed/w0en3j9kmt
https://coub.com/embed/z925vaal0x
https://coub.com/embed/llsxxio2o3
https://coub.com/embed/xydiujzwro
https://coub.com/embed/2e486z583s
https://coub.com/embed/b850qpvdwf
https://coub.com/embed/uasf7vsd00
https://coub.com/embed/ne9r9ql0ke
https://coub.com/embed/wrb0dawhk3
https://coub.com/embed/e7vo2n3rv0
https://coub.com/embed/sq5lb2quya
https://coub.com/embed/q7er5zdf9c
https://coub.com/embed/hijacyi6zc

收起阅读 »

列表选择弹窗ActionSheet的sheets属性无法实时更新

问题现象
动态改变列表选择弹窗ActionSheet的sheets属性,弹窗内容无法实时更新。问题代码示例参考如下:

@Entry
@Component
struct showActionSheetExample {
@State sheets: Array<SheetInfo> = [{
title: 'apples',
action: () => {
}
}];
@State count: number = 0;

build() {
Column() {
Button('showActionSheet')
.margin(30)
.onClick(() => {
setInterval(() => {
this.count++;
this.sheets.push({
title: 'bananas' + this.count,
action: () => {
}
});
}, 1000);

      this.getUIContext().showActionSheet({  
        title: 'ActionSheet title',  
        message: 'message',  
        confirm: {  
          value: 'Confirm button',  
          action: () => {  
            console.info('Get Alert Dialog handled');  
          }  
        },  
        alignment: DialogAlignment.Center,  
        sheets: this.sheets  
      });  
    });  
}.width('100%');  

}
}
背景知识
列表选择弹窗 (ActionSheet)是一个列表选择器弹窗适用于呈现多个操作选项,尤其当界面中仅需展示操作列表而无其他内容时。固定样式,当用户需要关注或确认的信息存在列表选择时使用。
当用户需要自定义弹出框内动态更新弹出框属性和内容时,使用不依赖UI组件的自定义弹出框 (openCustomDialog)。存在两种入参方式创建自定义弹出框:
openCustomDialog(传参为ComponentContent形式):通过ComponentContent封装内容可以与UI界面解耦,调用更加灵活,可以满足开发者的封装诉求。具有较高的灵活性,弹出框样式完全自定义,并且在弹出框打开后可以使用updateCustomDialog方法动态更新弹出框的参数。
openCustomDialog(传builder的形式):相对于ComponentContent,builder必须要与上下文做绑定,与UI存在一定耦合。此方法有默认的弹出框样式,适合于开发者想要实现与系统弹窗默认风格一致的效果。
问题定位
在showActionSheet打开列表选择弹窗后,对sheets属性进行动态修改,弹窗UI未变化。

分析结论
ActionSheet列表选择弹窗不支持动态更新属性,需要通过自定义弹出框实现。

修改建议
下面通过openCustomDialog实现,并且以传builder的形式为例,实现动态更新自定义弹出框的内容。

import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
private customDialogComponentId: number = 0;
@State count: number = 0;
intervalId?: number;

@Builder
customDialogComponent() {
Column() {
Text('充电详情').fontSize(25);
Text(已充电时间:${this.count}秒);
Row({ space: 50 }) {
Button('确认').onClick(() => {
this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogComponentId);
clearInterval(this.intervalId);
});
Button('取消').onClick(() => {
this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogComponentId);
clearInterval(this.intervalId);
});
};
}.height(200).padding(5).justifyContent(FlexAlign.SpaceAround);
}

build() {
Row() {
Column({ space: 20 }) {
Text('组件内弹窗')
.fontSize(30)
.onClick(() => {
this.getUIContext()
.getPromptAction()
.openCustomDialog({
builder: () => {
this.customDialogComponent();
}
})
.then((dialogId: number) => {
this.customDialogComponentId = dialogId;
})
.catch((error: BusinessError) => {
console.error(openCustomDialog error code is ${error.code}, message is ${error.message});
});

        this.intervalId = setInterval(() => {  
          this.count++;  
        }, 1 * 1000);  
      });  
  }  
  .width('100%');  
}  
.height('100%');  

}
}
https://coub.com/view/06dq1lpfmv
https://coub.com/view/h9okejkccd
https://coub.com/view/z59288b3bc
https://coub.com/view/jdsgw25pj5
https://coub.com/view/20ms41ljxh
https://coub.com/view/cyjima3av9
https://coub.com/view/xhymuinpm8
https://coub.com/view/gqqk4335y1
https://coub.com/view/9uflyvtfzv
https://coub.com/view/ogd56p24ng
https://coub.com/view/zydhfymmc3
https://coub.com/view/pzd0ac0kg5
https://coub.com/view/obdv3r4cy0
https://coub.com/view/ka2mdh07zg
https://coub.com/view/k8417q6t33
https://coub.com/view/b797hoxnvb
https://coub.com/view/p3wb9ayf0i
https://coub.com/view/cylp42xa95
https://coub.com/view/5vyv5859hb
https://coub.com/view/uolo157r87
https://coub.com/view/7bmp0nxmze
https://coub.com/view/d2q6yzkrli
https://coub.com/view/zs1hg1bhrv
https://coub.com/view/yz4tqllp0u
https://coub.com/view/a2kvibvldp
https://coub.com/view/09kqx4xcfx
https://coub.com/view/2c1zyd3kx3
https://coub.com/view/wep5eg226t
https://coub.com/view/su365ycwsu
https://coub.com/view/nzdz5k3kr0
https://coub.com/view/qg568ebu9j
https://coub.com/view/md5pqvk50f
https://coub.com/view/0gt0ay1zk3
https://coub.com/view/hecylej9t3
https://coub.com/view/wmxvxv1kkj
https://coub.com/view/bxxpl0brtv
https://coub.com/view/pholjf70ut
https://coub.com/view/m8l2xwwy39
https://coub.com/view/1c1x2w7t08
https://coub.com/view/53mubpegym
https://coub.com/view/on6slxqrt5
https://coub.com/view/55q881xx6l
https://coub.com/view/s77w2ok24r
https://coub.com/view/4e9hy0237n
https://coub.com/view/3entpjmdbw
https://coub.com/view/8besg49zbq
https://coub.com/view/usqat13gtl
https://coub.com/view/66yxwx6676
https://coub.com/view/np30bv52rn
https://coub.com/view/3nk88ivl0b
https://coub.com/view/83eyn5yj6r
https://coub.com/view/bymmywq8fr
https://coub.com/view/9bj0bp5xii
https://coub.com/view/agvyb0zhme
https://coub.com/view/8bcvp8upb3
https://coub.com/view/c8p7zv5zan
https://coub.com/view/yry7la9tco
https://coub.com/view/ho2dopbp7j
https://coub.com/view/amubk2uwuc
https://coub.com/view/2jwoy1e4u4
https://coub.com/view/ezz7hv58aq
https://coub.com/view/sxb8y6f7cm
https://coub.com/view/y4kvuhob20
https://coub.com/view/t3iyg4zbk6
https://coub.com/view/hlu3q922ul
https://coub.com/view/wr5t1i5hjn
https://coub.com/view/ig12bx6jem
https://coub.com/view/elh7b4dm2v
https://coub.com/view/p3aozbkqis
https://coub.com/view/1x387gqa9h
https://coub.com/view/11vq0o0urq
https://coub.com/view/n95945i2xa
https://coub.com/view/kwxot0bqpd
https://coub.com/view/0bzsnthjf8
https://coub.com/view/l6n7cci0v1
https://coub.com/view/e33t42b2e8
https://coub.com/view/uufau92iew
https://coub.com/view/8nu2gzpi87
https://coub.com/view/bzvc0eyha5
https://coub.com/view/m7i31eulsq

继续阅读 »

问题现象
动态改变列表选择弹窗ActionSheet的sheets属性,弹窗内容无法实时更新。问题代码示例参考如下:

@Entry
@Component
struct showActionSheetExample {
@State sheets: Array<SheetInfo> = [{
title: 'apples',
action: () => {
}
}];
@State count: number = 0;

build() {
Column() {
Button('showActionSheet')
.margin(30)
.onClick(() => {
setInterval(() => {
this.count++;
this.sheets.push({
title: 'bananas' + this.count,
action: () => {
}
});
}, 1000);

      this.getUIContext().showActionSheet({  
        title: 'ActionSheet title',  
        message: 'message',  
        confirm: {  
          value: 'Confirm button',  
          action: () => {  
            console.info('Get Alert Dialog handled');  
          }  
        },  
        alignment: DialogAlignment.Center,  
        sheets: this.sheets  
      });  
    });  
}.width('100%');  

}
}
背景知识
列表选择弹窗 (ActionSheet)是一个列表选择器弹窗适用于呈现多个操作选项,尤其当界面中仅需展示操作列表而无其他内容时。固定样式,当用户需要关注或确认的信息存在列表选择时使用。
当用户需要自定义弹出框内动态更新弹出框属性和内容时,使用不依赖UI组件的自定义弹出框 (openCustomDialog)。存在两种入参方式创建自定义弹出框:
openCustomDialog(传参为ComponentContent形式):通过ComponentContent封装内容可以与UI界面解耦,调用更加灵活,可以满足开发者的封装诉求。具有较高的灵活性,弹出框样式完全自定义,并且在弹出框打开后可以使用updateCustomDialog方法动态更新弹出框的参数。
openCustomDialog(传builder的形式):相对于ComponentContent,builder必须要与上下文做绑定,与UI存在一定耦合。此方法有默认的弹出框样式,适合于开发者想要实现与系统弹窗默认风格一致的效果。
问题定位
在showActionSheet打开列表选择弹窗后,对sheets属性进行动态修改,弹窗UI未变化。

分析结论
ActionSheet列表选择弹窗不支持动态更新属性,需要通过自定义弹出框实现。

修改建议
下面通过openCustomDialog实现,并且以传builder的形式为例,实现动态更新自定义弹出框的内容。

import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
private customDialogComponentId: number = 0;
@State count: number = 0;
intervalId?: number;

@Builder
customDialogComponent() {
Column() {
Text('充电详情').fontSize(25);
Text(已充电时间:${this.count}秒);
Row({ space: 50 }) {
Button('确认').onClick(() => {
this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogComponentId);
clearInterval(this.intervalId);
});
Button('取消').onClick(() => {
this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogComponentId);
clearInterval(this.intervalId);
});
};
}.height(200).padding(5).justifyContent(FlexAlign.SpaceAround);
}

build() {
Row() {
Column({ space: 20 }) {
Text('组件内弹窗')
.fontSize(30)
.onClick(() => {
this.getUIContext()
.getPromptAction()
.openCustomDialog({
builder: () => {
this.customDialogComponent();
}
})
.then((dialogId: number) => {
this.customDialogComponentId = dialogId;
})
.catch((error: BusinessError) => {
console.error(openCustomDialog error code is ${error.code}, message is ${error.message});
});

        this.intervalId = setInterval(() => {  
          this.count++;  
        }, 1 * 1000);  
      });  
  }  
  .width('100%');  
}  
.height('100%');  

}
}
https://coub.com/view/06dq1lpfmv
https://coub.com/view/h9okejkccd
https://coub.com/view/z59288b3bc
https://coub.com/view/jdsgw25pj5
https://coub.com/view/20ms41ljxh
https://coub.com/view/cyjima3av9
https://coub.com/view/xhymuinpm8
https://coub.com/view/gqqk4335y1
https://coub.com/view/9uflyvtfzv
https://coub.com/view/ogd56p24ng
https://coub.com/view/zydhfymmc3
https://coub.com/view/pzd0ac0kg5
https://coub.com/view/obdv3r4cy0
https://coub.com/view/ka2mdh07zg
https://coub.com/view/k8417q6t33
https://coub.com/view/b797hoxnvb
https://coub.com/view/p3wb9ayf0i
https://coub.com/view/cylp42xa95
https://coub.com/view/5vyv5859hb
https://coub.com/view/uolo157r87
https://coub.com/view/7bmp0nxmze
https://coub.com/view/d2q6yzkrli
https://coub.com/view/zs1hg1bhrv
https://coub.com/view/yz4tqllp0u
https://coub.com/view/a2kvibvldp
https://coub.com/view/09kqx4xcfx
https://coub.com/view/2c1zyd3kx3
https://coub.com/view/wep5eg226t
https://coub.com/view/su365ycwsu
https://coub.com/view/nzdz5k3kr0
https://coub.com/view/qg568ebu9j
https://coub.com/view/md5pqvk50f
https://coub.com/view/0gt0ay1zk3
https://coub.com/view/hecylej9t3
https://coub.com/view/wmxvxv1kkj
https://coub.com/view/bxxpl0brtv
https://coub.com/view/pholjf70ut
https://coub.com/view/m8l2xwwy39
https://coub.com/view/1c1x2w7t08
https://coub.com/view/53mubpegym
https://coub.com/view/on6slxqrt5
https://coub.com/view/55q881xx6l
https://coub.com/view/s77w2ok24r
https://coub.com/view/4e9hy0237n
https://coub.com/view/3entpjmdbw
https://coub.com/view/8besg49zbq
https://coub.com/view/usqat13gtl
https://coub.com/view/66yxwx6676
https://coub.com/view/np30bv52rn
https://coub.com/view/3nk88ivl0b
https://coub.com/view/83eyn5yj6r
https://coub.com/view/bymmywq8fr
https://coub.com/view/9bj0bp5xii
https://coub.com/view/agvyb0zhme
https://coub.com/view/8bcvp8upb3
https://coub.com/view/c8p7zv5zan
https://coub.com/view/yry7la9tco
https://coub.com/view/ho2dopbp7j
https://coub.com/view/amubk2uwuc
https://coub.com/view/2jwoy1e4u4
https://coub.com/view/ezz7hv58aq
https://coub.com/view/sxb8y6f7cm
https://coub.com/view/y4kvuhob20
https://coub.com/view/t3iyg4zbk6
https://coub.com/view/hlu3q922ul
https://coub.com/view/wr5t1i5hjn
https://coub.com/view/ig12bx6jem
https://coub.com/view/elh7b4dm2v
https://coub.com/view/p3aozbkqis
https://coub.com/view/1x387gqa9h
https://coub.com/view/11vq0o0urq
https://coub.com/view/n95945i2xa
https://coub.com/view/kwxot0bqpd
https://coub.com/view/0bzsnthjf8
https://coub.com/view/l6n7cci0v1
https://coub.com/view/e33t42b2e8
https://coub.com/view/uufau92iew
https://coub.com/view/8nu2gzpi87
https://coub.com/view/bzvc0eyha5
https://coub.com/view/m7i31eulsq

收起阅读 »

Hbuilderx云打包IOS,修改manifest.json版本号无效的问题

iOS打包 云打包

Hbuilderx云打包应用时

云打包完成后,发现安卓版本号正常,ios却还是识别到了之前的版本号;

解决方式

1.将uni统计关了

"uniStatistics" : {  
      "enable" : false  
  }  

2.将 unpackage/cache/ 目录下关于ios的删了,很可能和使用hbuilder打包出的ios本地基座有关;

暂时我是这么解决的,如果有其他根治的问题可以多留言交流

继续阅读 »

Hbuilderx云打包应用时

云打包完成后,发现安卓版本号正常,ios却还是识别到了之前的版本号;

解决方式

1.将uni统计关了

"uniStatistics" : {  
      "enable" : false  
  }  

2.将 unpackage/cache/ 目录下关于ios的删了,很可能和使用hbuilder打包出的ios本地基座有关;

暂时我是这么解决的,如果有其他根治的问题可以多留言交流

收起阅读 »

基于vite8+react19+electron41+antd桌面端中后台admin管理系统

admin vite

基于react19+electron41+ant-design+echarts+mockjs从0-1手搓桌面端通用后台管理Exe系统。内置3种常用布局模板,支持暗黑+亮色主题,多语言/面包屑导航/tab页签,实现图表/表格/表单/列表/编辑器/错误页等常用功能。

使用技术

  • 跨平台框架:electron^41.1.0
  • 技术框架:react^19.2.8+react-router-dom^7.18.2
  • 前端构建工具:vite^8.2.2
  • 组件库:antd^6.6.3
  • 状态管理:zustand^5.0.15
  • 图表组件:echarts^6.1.0
  • 数据模拟:axios^1.20.0+mockjs^1.1.0
  • 打包工具:electron-builder^26.15.3
  • 样式管理:sass^1.104.0

项目功能性

✅ 最新跨平台技术栈Electron41、React19、Vite8、Zustand5、AntDesign、Echarts
✅ 支持浅色+深色主题界面
✅ 支持中英文/繁体三种国际化语言
✅ 支持动态路由、面包屑导航、tab标签栏路由
✅ 提供3种通用布局模板、随意变换布局风格
✅ 整合常用的图表、表格、表单、列表、编辑器、错误处理等业务模块
✅ 基于electron41+react封装多开窗口管理

项目结构框架

项目入口配置

import { createRoot } from 'react-dom/client'  
import './style.scss'  
import App from './App.jsx'  

import { launchApp } from '@/windows/actions'  

launchApp().then(config => {  
  if(config) {  
    // 全局存储窗口配置  
    window.config = config  
  }  

  createRoot(document.getElementById('root')).render(  
    <App />  
  )  
})

App.jsx主模板

import { useMemo } from 'react'  
import { HashRouter } from 'react-router-dom'  
import { ConfigProvider, theme, App as AntApp } from 'antd'  

// 引入组件语言  
import enUS from 'antd/locale/en_US'  
import zhCN from 'antd/locale/zh_CN'  
import zhTW from 'antd/locale/zh_TW'  

// antd组件默认文案是英文,修改为中文  
import dayjs from 'dayjs'  
import 'dayjs/locale/zh-cn'  
dayjs.locale('zh-cn')  

import { ipcLoaded } from '@/windows/actions'  
import { appStore } from '@/store/app'  

// 引入路由配置  
import Router, { AuthRouter } from './router'  

function App() {  
  ipcLoaded()  
  const { lang, config: { isDark, themeColor } } = appStore()  

  const locale = useMemo(() => {  
    switch(lang) {  
      case 'en':  
        return enUS  
      case 'zh-CN':  
        return zhCN  
      case 'zh-TW':  
        return zhTW  
      default:  
        return zhCN  
    }  
  }, [lang])  

  return (  
    <ConfigProvider  
      // 国际化  
      locale={locale}  
      // 主题  
      theme={{  
        token: {  
          colorPrimary: themeColor || '#1677ff',  
          fontFamily: "Inter, system-ui, Avenir, Helvetica, Arial, sans-serif",  
        },  
        algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm  
      }}  
    >  
      <AntApp>  
        <HashRouter>  
          <AuthRouter>  
            <Router />  
          </AuthRouter>  
        </HashRouter>  
      </AntApp>  
    </ConfigProvider>  
  )  
}

项目布局模板

/**  
 * 主布局模板  
 * @author andy  
*/  

import { useMemo } from 'react'  
import clsx from 'clsx'  
import { appStore } from '@/store/app'  

// 引入布局模板  
import Columns from './template/columns'  
import Vertical from './template/vertical'  
import Transverse from './template/transverse'  

function MainLayout() {  
  const { config: { skin, layout, fullContent } } = appStore()  

  // 布局模板  
  const LayoutComponent = useMemo(() => {  
    switch (layout) {  
      case 'columns':  
        return Columns  
      case 'vertical':  
        return Vertical  
      case 'transverse':  
        return Transverse  
      default:  
        return Columns  
    }  
  }, [layout])  

  return (  
    <div className={clsx('radmin__container', { 'is-full': fullContent })}>  
      <LayoutComponent />  
    </div>  
  )  
}  

export default MainLayout

作者:xiaoyan2017
链接:https://www.cnblogs.com/xiaoyan2017/p/22912373
来源:博客园
本文原创发布于博客园 ,转载请注明出处,谢谢合作

继续阅读 »

基于react19+electron41+ant-design+echarts+mockjs从0-1手搓桌面端通用后台管理Exe系统。内置3种常用布局模板,支持暗黑+亮色主题,多语言/面包屑导航/tab页签,实现图表/表格/表单/列表/编辑器/错误页等常用功能。

使用技术

  • 跨平台框架:electron^41.1.0
  • 技术框架:react^19.2.8+react-router-dom^7.18.2
  • 前端构建工具:vite^8.2.2
  • 组件库:antd^6.6.3
  • 状态管理:zustand^5.0.15
  • 图表组件:echarts^6.1.0
  • 数据模拟:axios^1.20.0+mockjs^1.1.0
  • 打包工具:electron-builder^26.15.3
  • 样式管理:sass^1.104.0

项目功能性

✅ 最新跨平台技术栈Electron41、React19、Vite8、Zustand5、AntDesign、Echarts
✅ 支持浅色+深色主题界面
✅ 支持中英文/繁体三种国际化语言
✅ 支持动态路由、面包屑导航、tab标签栏路由
✅ 提供3种通用布局模板、随意变换布局风格
✅ 整合常用的图表、表格、表单、列表、编辑器、错误处理等业务模块
✅ 基于electron41+react封装多开窗口管理

项目结构框架

项目入口配置

import { createRoot } from 'react-dom/client'  
import './style.scss'  
import App from './App.jsx'  

import { launchApp } from '@/windows/actions'  

launchApp().then(config => {  
  if(config) {  
    // 全局存储窗口配置  
    window.config = config  
  }  

  createRoot(document.getElementById('root')).render(  
    <App />  
  )  
})

App.jsx主模板

import { useMemo } from 'react'  
import { HashRouter } from 'react-router-dom'  
import { ConfigProvider, theme, App as AntApp } from 'antd'  

// 引入组件语言  
import enUS from 'antd/locale/en_US'  
import zhCN from 'antd/locale/zh_CN'  
import zhTW from 'antd/locale/zh_TW'  

// antd组件默认文案是英文,修改为中文  
import dayjs from 'dayjs'  
import 'dayjs/locale/zh-cn'  
dayjs.locale('zh-cn')  

import { ipcLoaded } from '@/windows/actions'  
import { appStore } from '@/store/app'  

// 引入路由配置  
import Router, { AuthRouter } from './router'  

function App() {  
  ipcLoaded()  
  const { lang, config: { isDark, themeColor } } = appStore()  

  const locale = useMemo(() => {  
    switch(lang) {  
      case 'en':  
        return enUS  
      case 'zh-CN':  
        return zhCN  
      case 'zh-TW':  
        return zhTW  
      default:  
        return zhCN  
    }  
  }, [lang])  

  return (  
    <ConfigProvider  
      // 国际化  
      locale={locale}  
      // 主题  
      theme={{  
        token: {  
          colorPrimary: themeColor || '#1677ff',  
          fontFamily: "Inter, system-ui, Avenir, Helvetica, Arial, sans-serif",  
        },  
        algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm  
      }}  
    >  
      <AntApp>  
        <HashRouter>  
          <AuthRouter>  
            <Router />  
          </AuthRouter>  
        </HashRouter>  
      </AntApp>  
    </ConfigProvider>  
  )  
}

项目布局模板

/**  
 * 主布局模板  
 * @author andy  
*/  

import { useMemo } from 'react'  
import clsx from 'clsx'  
import { appStore } from '@/store/app'  

// 引入布局模板  
import Columns from './template/columns'  
import Vertical from './template/vertical'  
import Transverse from './template/transverse'  

function MainLayout() {  
  const { config: { skin, layout, fullContent } } = appStore()  

  // 布局模板  
  const LayoutComponent = useMemo(() => {  
    switch (layout) {  
      case 'columns':  
        return Columns  
      case 'vertical':  
        return Vertical  
      case 'transverse':  
        return Transverse  
      default:  
        return Columns  
    }  
  }, [layout])  

  return (  
    <div className={clsx('radmin__container', { 'is-full': fullContent })}>  
      <LayoutComponent />  
    </div>  
  )  
}  

export default MainLayout

作者:xiaoyan2017
链接:https://www.cnblogs.com/xiaoyan2017/p/22912373
来源:博客园
本文原创发布于博客园 ,转载请注明出处,谢谢合作

收起阅读 »

如何解决显式动画过程中点击事件无法触发的问题

问题现象
当组件在动画执行过程中,对组件当前显示位置进行点击操作,无法触发onClick事件,唯有当动画结束、组件停止在最终位置后,该组件的点击操作才可正常响应。如何在动画执行过程中使组件能正确响应onClick事件?

@Entry
@Component
struct Problem {
@State title: string = '点击更新时间参数:';
@State x: number = 0;

startAnimation() {
this.getUIContext().animateTo({ duration: 5000, playMode: PlayMode.Normal }, () => {
this.x = 100;
});
}

build() {
Stack() {
Text(this.title)
.fontSize(18)
.position({ x: this.x, y: 40 })
.animation({
duration: 5000,
curve: Curve.Linear,
playMode: PlayMode.Normal
})
.onClick(() => {
this.title = '点击更新时间参数:' + new Date().getTime();
})
.onAppear(() => {
this.startAnimation();
});
}
.height(100)
.width('100%');
}
}
背景知识
@ohos.animator(动画):提供组件动画效果,包括定义动画、启动动画和以相反的顺序播放动画等。
显式动画(animateTo):提供全局animateTo显式动画接口来指定由于闭包代码导致的状态变化插入过渡动效。
解决方案
由于显式动画的特性,目标对象在动画执行期间所处的位置仅在动画结束时被识别和记录。当用户在动画播放过程中尝试点击该对象时,点击事件无法被正确识别和响应。
为了解决这一问题,可以采用帧动画的方式来实现,帧动画通过逐帧定义对象的状态,系统会在每一帧更新对象的位置和状态,并据此响应用户交互事件。完整示例代码参考如下:
import { AnimatorResult } from '@kit.ArkUI';

@Entry
@Component
struct SolutionOne {
@State title: string = '点击更新时间参数:';
@State x: number = 0;
private backAnimator: AnimatorResult = this.getUIContext().createAnimator({
duration: 5000,
easing: 'ease',
delay: 0,
fill: 'forwards',
direction: 'normal',
iterations: -1,
begin: 0,
end: 100
});

build() {
Stack() {
Text(this.title)
.fontSize(18)
.position({ x: this.x, y: 40 })
.onClick(() => {
this.title = '点击更新时间参数:' + new Date().getTime();
})
.onAppear(() => {
this.backAnimator.play();
this.backAnimator.onFrame = (value: number) => {
this.x = value;
};
});
}
.height(100)
.width('100%');
}
}
https://coub.com/view/x0xla7q1ai
https://coub.com/view/g1czsl0enm
https://coub.com/view/b08cx7zoe3
https://coub.com/view/bxn2yqwexg
https://coub.com/view/m9qeccobgj
https://coub.com/view/wdvoecbi4r
https://coub.com/view/x81cx0az2t
https://coub.com/view/0slfmtknp8
https://coub.com/view/2q5tc8yg66
https://coub.com/view/dt2oigt51x
https://coub.com/view/nrlgm47wti
https://coub.com/view/sp90u3ia89
https://coub.com/view/o78qotj8bn
https://coub.com/view/1duw99ytde
https://coub.com/view/wjcskrvoln
https://coub.com/view/8obmr0ccrs
https://coub.com/view/sw5f17cga5
https://coub.com/view/s1q3otk2wu
https://coub.com/view/83z3yr5cy4
https://coub.com/view/brgme2h0fi
https://coub.com/view/jkh8n5pj1b
https://coub.com/view/vode602lzv
https://coub.com/view/ue3i8rhiey
https://coub.com/view/76wrcka2m1
https://coub.com/view/casxikde15
https://coub.com/view/lb2qd32pue
https://coub.com/view/vqz0ngc7lp
https://coub.com/view/5x8wrqa4sk
https://coub.com/view/s22g4m3o4b
https://coub.com/view/a7oblam77e
https://coub.com/view/nkne5vbzgv
https://coub.com/view/92pdw2n47o
https://coub.com/view/wi0fyfnyj6
https://coub.com/view/lrcrn6sklh
https://coub.com/view/ty636zpx1j
https://coub.com/view/1p4s8n800f
https://coub.com/view/2ewu7lqpse
https://coub.com/view/ees1i4x52y
https://coub.com/view/esuw3humxk
https://coub.com/view/ttof5yluqt
https://coub.com/view/3788oh5mhd
https://coub.com/view/zprnnzdm9y
https://coub.com/view/1758nsm1bp
https://coub.com/view/85robtp98p
https://coub.com/view/pwhyjoe2qk
https://coub.com/view/fk76ykwiph
https://coub.com/view/2qpw8qa4v6
https://coub.com/view/qmich3xb6k
https://coub.com/view/0fpkrydd0k
https://coub.com/view/bvzgv7jbfc
https://coub.com/view/nvjqk9hmiz
https://coub.com/view/0le4asq8nh
https://coub.com/view/rkrsrwn90s
https://coub.com/view/f2uu2z8u1s
https://coub.com/view/715y2xacjj
https://coub.com/view/6v5dzfjzbn
https://coub.com/view/wxk5a8mxbn
https://coub.com/view/o05ggpa1bk
https://coub.com/view/v04o0y81aa
https://coub.com/view/e0fsub9p1f
https://coub.com/view/tkv88d5t0f
https://coub.com/view/xz6i7n2qay
https://coub.com/view/d211sbq00l
https://coub.com/view/zypkg0eyho
https://coub.com/view/8r0gx25nyg
https://coub.com/view/63tzntskz6
https://coub.com/view/3jsg85l39s
https://coub.com/view/hr4idq4466
https://coub.com/view/h4drvxmmmt
https://coub.com/view/schlv1biel
https://coub.com/view/f4uuetk2yf
https://coub.com/view/m2z1rd0e4k
https://coub.com/view/s94flprsy0
https://coub.com/view/smusvxthpl
https://coub.com/view/p0qj7yhx1p
https://coub.com/view/5ahyecg1hb
https://coub.com/view/hgbpl5bpg1
https://coub.com/view/0xmq3ond07
https://coub.com/view/zf7r0w2yyb
https://coub.com/view/bd0elp9k2p

继续阅读 »

问题现象
当组件在动画执行过程中,对组件当前显示位置进行点击操作,无法触发onClick事件,唯有当动画结束、组件停止在最终位置后,该组件的点击操作才可正常响应。如何在动画执行过程中使组件能正确响应onClick事件?

@Entry
@Component
struct Problem {
@State title: string = '点击更新时间参数:';
@State x: number = 0;

startAnimation() {
this.getUIContext().animateTo({ duration: 5000, playMode: PlayMode.Normal }, () => {
this.x = 100;
});
}

build() {
Stack() {
Text(this.title)
.fontSize(18)
.position({ x: this.x, y: 40 })
.animation({
duration: 5000,
curve: Curve.Linear,
playMode: PlayMode.Normal
})
.onClick(() => {
this.title = '点击更新时间参数:' + new Date().getTime();
})
.onAppear(() => {
this.startAnimation();
});
}
.height(100)
.width('100%');
}
}
背景知识
@ohos.animator(动画):提供组件动画效果,包括定义动画、启动动画和以相反的顺序播放动画等。
显式动画(animateTo):提供全局animateTo显式动画接口来指定由于闭包代码导致的状态变化插入过渡动效。
解决方案
由于显式动画的特性,目标对象在动画执行期间所处的位置仅在动画结束时被识别和记录。当用户在动画播放过程中尝试点击该对象时,点击事件无法被正确识别和响应。
为了解决这一问题,可以采用帧动画的方式来实现,帧动画通过逐帧定义对象的状态,系统会在每一帧更新对象的位置和状态,并据此响应用户交互事件。完整示例代码参考如下:
import { AnimatorResult } from '@kit.ArkUI';

@Entry
@Component
struct SolutionOne {
@State title: string = '点击更新时间参数:';
@State x: number = 0;
private backAnimator: AnimatorResult = this.getUIContext().createAnimator({
duration: 5000,
easing: 'ease',
delay: 0,
fill: 'forwards',
direction: 'normal',
iterations: -1,
begin: 0,
end: 100
});

build() {
Stack() {
Text(this.title)
.fontSize(18)
.position({ x: this.x, y: 40 })
.onClick(() => {
this.title = '点击更新时间参数:' + new Date().getTime();
})
.onAppear(() => {
this.backAnimator.play();
this.backAnimator.onFrame = (value: number) => {
this.x = value;
};
});
}
.height(100)
.width('100%');
}
}
https://coub.com/view/x0xla7q1ai
https://coub.com/view/g1czsl0enm
https://coub.com/view/b08cx7zoe3
https://coub.com/view/bxn2yqwexg
https://coub.com/view/m9qeccobgj
https://coub.com/view/wdvoecbi4r
https://coub.com/view/x81cx0az2t
https://coub.com/view/0slfmtknp8
https://coub.com/view/2q5tc8yg66
https://coub.com/view/dt2oigt51x
https://coub.com/view/nrlgm47wti
https://coub.com/view/sp90u3ia89
https://coub.com/view/o78qotj8bn
https://coub.com/view/1duw99ytde
https://coub.com/view/wjcskrvoln
https://coub.com/view/8obmr0ccrs
https://coub.com/view/sw5f17cga5
https://coub.com/view/s1q3otk2wu
https://coub.com/view/83z3yr5cy4
https://coub.com/view/brgme2h0fi
https://coub.com/view/jkh8n5pj1b
https://coub.com/view/vode602lzv
https://coub.com/view/ue3i8rhiey
https://coub.com/view/76wrcka2m1
https://coub.com/view/casxikde15
https://coub.com/view/lb2qd32pue
https://coub.com/view/vqz0ngc7lp
https://coub.com/view/5x8wrqa4sk
https://coub.com/view/s22g4m3o4b
https://coub.com/view/a7oblam77e
https://coub.com/view/nkne5vbzgv
https://coub.com/view/92pdw2n47o
https://coub.com/view/wi0fyfnyj6
https://coub.com/view/lrcrn6sklh
https://coub.com/view/ty636zpx1j
https://coub.com/view/1p4s8n800f
https://coub.com/view/2ewu7lqpse
https://coub.com/view/ees1i4x52y
https://coub.com/view/esuw3humxk
https://coub.com/view/ttof5yluqt
https://coub.com/view/3788oh5mhd
https://coub.com/view/zprnnzdm9y
https://coub.com/view/1758nsm1bp
https://coub.com/view/85robtp98p
https://coub.com/view/pwhyjoe2qk
https://coub.com/view/fk76ykwiph
https://coub.com/view/2qpw8qa4v6
https://coub.com/view/qmich3xb6k
https://coub.com/view/0fpkrydd0k
https://coub.com/view/bvzgv7jbfc
https://coub.com/view/nvjqk9hmiz
https://coub.com/view/0le4asq8nh
https://coub.com/view/rkrsrwn90s
https://coub.com/view/f2uu2z8u1s
https://coub.com/view/715y2xacjj
https://coub.com/view/6v5dzfjzbn
https://coub.com/view/wxk5a8mxbn
https://coub.com/view/o05ggpa1bk
https://coub.com/view/v04o0y81aa
https://coub.com/view/e0fsub9p1f
https://coub.com/view/tkv88d5t0f
https://coub.com/view/xz6i7n2qay
https://coub.com/view/d211sbq00l
https://coub.com/view/zypkg0eyho
https://coub.com/view/8r0gx25nyg
https://coub.com/view/63tzntskz6
https://coub.com/view/3jsg85l39s
https://coub.com/view/hr4idq4466
https://coub.com/view/h4drvxmmmt
https://coub.com/view/schlv1biel
https://coub.com/view/f4uuetk2yf
https://coub.com/view/m2z1rd0e4k
https://coub.com/view/s94flprsy0
https://coub.com/view/smusvxthpl
https://coub.com/view/p0qj7yhx1p
https://coub.com/view/5ahyecg1hb
https://coub.com/view/hgbpl5bpg1
https://coub.com/view/0xmq3ond07
https://coub.com/view/zf7r0w2yyb
https://coub.com/view/bd0elp9k2p

收起阅读 »

如何实现拖拽时列表项占位动画的效果

grid

拖拽Grid时,列表项显示占位动画效果。实现步骤如下:

在Grid组件下设置属性editMode(true),使Grid进入编辑模式。进入编辑模式后,可以拖拽Grid组件内部的GridItem。
在onItemDragStart回调中设置拖拽时显示的组件。
在onItemDrop中获取拖拽起始位置和拖拽插入位置,并完成数组位置交换逻辑。
@Entry
@Component
struct GridExample {
@State numbers: string[] = [];
scroller: Scroller = new Scroller();
@State text: string = 'drag';

@Builder
pixelMapBuilder() {
Column() {
Text(this.text)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
}
}

aboutToAppear() {
for (let i = 1; i <= 15; i++) {
this.numbers.push(i + '');
}
}

changeIndex(index1: number, index2: number) {
// Swap array positions
let temp = this.numbers[index1];
this.numbers[index1] = this.numbers[index2];
this.numbers[index2] = temp;
}

build() {
Column({ space: 5 }) {
Grid(this.scroller) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Up) {
this.text = day;
}
})
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.onScrollIndex((first: number) => {
console.info(first.toString());
})
.width('90%')
.backgroundColor(0xFAEEE0)
.height(300)
.editMode(true) // Set whether the Grid enters editing mode. When entering editing mode, you can drag and drop the GridItem inside the Grid component
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // When dragging the component bound to this event for the first time, a callback is triggered.
return this.pixelMapBuilder(); //Set the image displayed during the drag and drop process.
})
// The component bound to this event can be used as a drag and drop release target. When the drag behavior stops within the scope of this component, a callback is triggered.
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
// Drag the starting position of itemIndex, drag the insertion position of insertIndex
this.changeIndex(itemIndex, insertIndex)
})
}.width('100%').margin({ top: 5 })
}
}
https://sites.google.com/view/7wtipu12/home
https://sites.google.com/view/4duref63/home
https://sites.google.com/view/7wvqsg34/home
https://sites.google.com/view/0xxrqf74/home
https://sites.google.com/view/1ruukx88/home
https://sites.google.com/view/1byjvn60/home
https://sites.google.com/view/4xgvng21/home
https://sites.google.com/view/9pciml46/home
https://sites.google.com/view/2hbbnf70/home
https://sites.google.com/view/9rghya88/home
https://sites.google.com/view/7vwdre23/home
https://sites.google.com/view/7przvh02/home
https://sites.google.com/view/3hktye35/home
https://sites.google.com/view/0xykfj78/home
https://sites.google.com/view/0asapr45/home
https://sites.google.com/view/0rgboz72/home
https://sites.google.com/view/0qrzbw79/home
https://sites.google.com/view/4pszed21/home
https://sites.google.com/view/5rjciu59/home
https://sites.google.com/view/0ssfsw28/home
https://sites.google.com/view/7pmhty48/home
https://sites.google.com/view/2dceln83/home
https://sites.google.com/view/1mmlgm03/home
https://sites.google.com/view/1tpbqp74/home
https://sites.google.com/view/9sfedj48/home
https://sites.google.com/view/3ibsri54/home
https://sites.google.com/view/6kncht37/home
https://sites.google.com/view/1aczwe01/home
https://sites.google.com/view/3sxmeg54/home
https://sites.google.com/view/7eprfq47/home
https://sites.google.com/view/1ebnwt11/home
https://sites.google.com/view/5cywba48/home
https://sites.google.com/view/8vqwer81/home
https://sites.google.com/view/8wetjp74/home
https://sites.google.com/view/8goxua35/home
https://sites.google.com/view/4kslyk33/home
https://sites.google.com/view/4essyn94/home
https://sites.google.com/view/2jvyst79/home
https://sites.google.com/view/1roras95/home
https://sites.google.com/view/1ktawz57/home
https://sites.google.com/view/0rgcul98/home
https://sites.google.com/view/0nycaz65/home
https://sites.google.com/view/1omztt55/home
https://sites.google.com/view/7gdqsw63/home
https://sites.google.com/view/4pxdgz83/home
https://sites.google.com/view/0gwyrr33/home
https://sites.google.com/view/2ijimf20/home
https://sites.google.com/view/8wykzr07/home
https://sites.google.com/view/7nudnf27/home
https://sites.google.com/view/3kcowk62/home
https://sites.google.com/view/5pytzw49/home
https://sites.google.com/view/2slklr71/home
https://sites.google.com/view/2ikmgs30/home
https://sites.google.com/view/5hqxfe84/home
https://sites.google.com/view/0kqdmc12/home
https://sites.google.com/view/5axjec74/home
https://sites.google.com/view/5gixkd52/home
https://sites.google.com/view/6kgtbh91/home
https://sites.google.com/view/1dtvwv05/home
https://sites.google.com/view/3nbvob04/home
https://sites.google.com/view/2fpwug45/home
https://sites.google.com/view/6xjygc54/home
https://sites.google.com/view/4uqced58/home
https://sites.google.com/view/5whcuq51/home
https://sites.google.com/view/2rgfcv68/home
https://sites.google.com/view/5pwouh86/home
https://sites.google.com/view/6dwmpc30/home
https://sites.google.com/view/3wihcp64/home
https://sites.google.com/view/2huocs89/home
https://sites.google.com/view/3mmiei39/home
https://sites.google.com/view/1rvjfn27/home
https://sites.google.com/view/6lcinp92/home
https://sites.google.com/view/6gxkse35/home
https://sites.google.com/view/3wahfy11/home
https://coub.com/view/5rqufxinzg
https://coub.com/view/ropziici0u
https://coub.com/view/7or1trdg1c
https://coub.com/view/vcozxsnzer
https://coub.com/view/7kd7t3oj2c
https://coub.com/view/8aqyygye2e
https://coub.com/view/gllow4joor
https://coub.com/view/39dnitz99r
https://coub.com/view/1t0lqxrzi8
https://coub.com/view/vnj2yxw549
https://coub.com/view/3s1pmq3ilx
https://coub.com/view/a1ytt3pw3a
https://coub.com/view/cawk3s04k4
https://coub.com/view/kmvzuhkhjd
https://coub.com/view/ej9o0adzwx
https://coub.com/view/octx2ubxfv
https://coub.com/view/aa9gmfdu3q
https://coub.com/view/9bgaznk1ck
https://coub.com/view/rchkbwofuc
https://coub.com/view/9s2jppcl9u
https://coub.com/view/5sj5dui5qx
https://coub.com/view/m2ajgr2j7k
https://coub.com/view/0yfa17r86g
https://coub.com/view/gr0kpo0pmh
https://coub.com/view/l3hqw0zqc5
https://coub.com/view/88qknf3fk6
https://coub.com/view/isb8wfeda2
https://coub.com/view/yvsr9c6x01
https://coub.com/view/mb687s5mws
https://coub.com/view/rgb9qqxyvj
https://coub.com/view/03xbg531k6
https://coub.com/view/0p29ulxra3
https://coub.com/view/8815w7t7de
https://coub.com/view/shicmahfum
https://coub.com/view/gpbeot9j3l
https://coub.com/view/2dfpjx3czu
https://coub.com/view/xs3ukwnjf3
https://coub.com/view/g71qbn6jru
https://coub.com/view/nshhnvyttc
https://coub.com/view/jmz1bnvuks
https://coub.com/view/fc80zsrnh1
https://coub.com/view/ppu1e0wlob
https://coub.com/view/o8b8ik13xp
https://coub.com/view/wjc31q3bpo
https://coub.com/view/s3h67xw7jb
https://coub.com/view/zofzxw59k4
https://coub.com/view/kdi3wrrueu
https://coub.com/view/98nrx1y16i
https://coub.com/view/agp7euis6f
https://coub.com/view/alaeuzimeo
https://coub.com/view/6y73g5cr1s
https://coub.com/view/vfnl3o6o5c
https://coub.com/view/de4e7iqa60
https://coub.com/view/c4htjqg88m
https://coub.com/view/kz3ieaosfi
https://coub.com/view/d6msuxbq4a
https://coub.com/view/77tzitriot
https://coub.com/view/lpsk81odp9
https://coub.com/view/2nmdnujl00
https://coub.com/view/l8mq0g4760
https://coub.com/view/2mkc8ze5zx
https://coub.com/view/ig0wfzltc6
https://coub.com/view/1ycu3zf1lh
https://coub.com/view/wzat8knm2z
https://coub.com/view/64fv7jr83g
https://coub.com/view/a3did3vro0
https://coub.com/view/ti645azepy
https://coub.com/view/g1vuwgj26b
https://coub.com/view/ffzr61ask8
https://coub.com/view/p5bbm0hek0
https://coub.com/view/g3cd11h0y6
https://coub.com/view/3sf0ida4ix
https://coub.com/view/7bu9ihff89
https://coub.com/view/xjn62txhsl
https://coub.com/view/nlk1uu6rts
https://coub.com/view/t0d2e48ya0
https://coub.com/view/nseh2ehyd9
https://coub.com/view/5v4pyo0v0k
https://coub.com/view/rierf8fjyo

继续阅读 »

拖拽Grid时,列表项显示占位动画效果。实现步骤如下:

在Grid组件下设置属性editMode(true),使Grid进入编辑模式。进入编辑模式后,可以拖拽Grid组件内部的GridItem。
在onItemDragStart回调中设置拖拽时显示的组件。
在onItemDrop中获取拖拽起始位置和拖拽插入位置,并完成数组位置交换逻辑。
@Entry
@Component
struct GridExample {
@State numbers: string[] = [];
scroller: Scroller = new Scroller();
@State text: string = 'drag';

@Builder
pixelMapBuilder() {
Column() {
Text(this.text)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
}
}

aboutToAppear() {
for (let i = 1; i <= 15; i++) {
this.numbers.push(i + '');
}
}

changeIndex(index1: number, index2: number) {
// Swap array positions
let temp = this.numbers[index1];
this.numbers[index1] = this.numbers[index2];
this.numbers[index2] = temp;
}

build() {
Column({ space: 5 }) {
Grid(this.scroller) {
ForEach(this.numbers, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width(80)
.height(80)
.textAlign(TextAlign.Center)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Up) {
this.text = day;
}
})
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.onScrollIndex((first: number) => {
console.info(first.toString());
})
.width('90%')
.backgroundColor(0xFAEEE0)
.height(300)
.editMode(true) // Set whether the Grid enters editing mode. When entering editing mode, you can drag and drop the GridItem inside the Grid component
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => { // When dragging the component bound to this event for the first time, a callback is triggered.
return this.pixelMapBuilder(); //Set the image displayed during the drag and drop process.
})
// The component bound to this event can be used as a drag and drop release target. When the drag behavior stops within the scope of this component, a callback is triggered.
.onItemDrop((event: ItemDragInfo, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
// Drag the starting position of itemIndex, drag the insertion position of insertIndex
this.changeIndex(itemIndex, insertIndex)
})
}.width('100%').margin({ top: 5 })
}
}
https://sites.google.com/view/7wtipu12/home
https://sites.google.com/view/4duref63/home
https://sites.google.com/view/7wvqsg34/home
https://sites.google.com/view/0xxrqf74/home
https://sites.google.com/view/1ruukx88/home
https://sites.google.com/view/1byjvn60/home
https://sites.google.com/view/4xgvng21/home
https://sites.google.com/view/9pciml46/home
https://sites.google.com/view/2hbbnf70/home
https://sites.google.com/view/9rghya88/home
https://sites.google.com/view/7vwdre23/home
https://sites.google.com/view/7przvh02/home
https://sites.google.com/view/3hktye35/home
https://sites.google.com/view/0xykfj78/home
https://sites.google.com/view/0asapr45/home
https://sites.google.com/view/0rgboz72/home
https://sites.google.com/view/0qrzbw79/home
https://sites.google.com/view/4pszed21/home
https://sites.google.com/view/5rjciu59/home
https://sites.google.com/view/0ssfsw28/home
https://sites.google.com/view/7pmhty48/home
https://sites.google.com/view/2dceln83/home
https://sites.google.com/view/1mmlgm03/home
https://sites.google.com/view/1tpbqp74/home
https://sites.google.com/view/9sfedj48/home
https://sites.google.com/view/3ibsri54/home
https://sites.google.com/view/6kncht37/home
https://sites.google.com/view/1aczwe01/home
https://sites.google.com/view/3sxmeg54/home
https://sites.google.com/view/7eprfq47/home
https://sites.google.com/view/1ebnwt11/home
https://sites.google.com/view/5cywba48/home
https://sites.google.com/view/8vqwer81/home
https://sites.google.com/view/8wetjp74/home
https://sites.google.com/view/8goxua35/home
https://sites.google.com/view/4kslyk33/home
https://sites.google.com/view/4essyn94/home
https://sites.google.com/view/2jvyst79/home
https://sites.google.com/view/1roras95/home
https://sites.google.com/view/1ktawz57/home
https://sites.google.com/view/0rgcul98/home
https://sites.google.com/view/0nycaz65/home
https://sites.google.com/view/1omztt55/home
https://sites.google.com/view/7gdqsw63/home
https://sites.google.com/view/4pxdgz83/home
https://sites.google.com/view/0gwyrr33/home
https://sites.google.com/view/2ijimf20/home
https://sites.google.com/view/8wykzr07/home
https://sites.google.com/view/7nudnf27/home
https://sites.google.com/view/3kcowk62/home
https://sites.google.com/view/5pytzw49/home
https://sites.google.com/view/2slklr71/home
https://sites.google.com/view/2ikmgs30/home
https://sites.google.com/view/5hqxfe84/home
https://sites.google.com/view/0kqdmc12/home
https://sites.google.com/view/5axjec74/home
https://sites.google.com/view/5gixkd52/home
https://sites.google.com/view/6kgtbh91/home
https://sites.google.com/view/1dtvwv05/home
https://sites.google.com/view/3nbvob04/home
https://sites.google.com/view/2fpwug45/home
https://sites.google.com/view/6xjygc54/home
https://sites.google.com/view/4uqced58/home
https://sites.google.com/view/5whcuq51/home
https://sites.google.com/view/2rgfcv68/home
https://sites.google.com/view/5pwouh86/home
https://sites.google.com/view/6dwmpc30/home
https://sites.google.com/view/3wihcp64/home
https://sites.google.com/view/2huocs89/home
https://sites.google.com/view/3mmiei39/home
https://sites.google.com/view/1rvjfn27/home
https://sites.google.com/view/6lcinp92/home
https://sites.google.com/view/6gxkse35/home
https://sites.google.com/view/3wahfy11/home
https://coub.com/view/5rqufxinzg
https://coub.com/view/ropziici0u
https://coub.com/view/7or1trdg1c
https://coub.com/view/vcozxsnzer
https://coub.com/view/7kd7t3oj2c
https://coub.com/view/8aqyygye2e
https://coub.com/view/gllow4joor
https://coub.com/view/39dnitz99r
https://coub.com/view/1t0lqxrzi8
https://coub.com/view/vnj2yxw549
https://coub.com/view/3s1pmq3ilx
https://coub.com/view/a1ytt3pw3a
https://coub.com/view/cawk3s04k4
https://coub.com/view/kmvzuhkhjd
https://coub.com/view/ej9o0adzwx
https://coub.com/view/octx2ubxfv
https://coub.com/view/aa9gmfdu3q
https://coub.com/view/9bgaznk1ck
https://coub.com/view/rchkbwofuc
https://coub.com/view/9s2jppcl9u
https://coub.com/view/5sj5dui5qx
https://coub.com/view/m2ajgr2j7k
https://coub.com/view/0yfa17r86g
https://coub.com/view/gr0kpo0pmh
https://coub.com/view/l3hqw0zqc5
https://coub.com/view/88qknf3fk6
https://coub.com/view/isb8wfeda2
https://coub.com/view/yvsr9c6x01
https://coub.com/view/mb687s5mws
https://coub.com/view/rgb9qqxyvj
https://coub.com/view/03xbg531k6
https://coub.com/view/0p29ulxra3
https://coub.com/view/8815w7t7de
https://coub.com/view/shicmahfum
https://coub.com/view/gpbeot9j3l
https://coub.com/view/2dfpjx3czu
https://coub.com/view/xs3ukwnjf3
https://coub.com/view/g71qbn6jru
https://coub.com/view/nshhnvyttc
https://coub.com/view/jmz1bnvuks
https://coub.com/view/fc80zsrnh1
https://coub.com/view/ppu1e0wlob
https://coub.com/view/o8b8ik13xp
https://coub.com/view/wjc31q3bpo
https://coub.com/view/s3h67xw7jb
https://coub.com/view/zofzxw59k4
https://coub.com/view/kdi3wrrueu
https://coub.com/view/98nrx1y16i
https://coub.com/view/agp7euis6f
https://coub.com/view/alaeuzimeo
https://coub.com/view/6y73g5cr1s
https://coub.com/view/vfnl3o6o5c
https://coub.com/view/de4e7iqa60
https://coub.com/view/c4htjqg88m
https://coub.com/view/kz3ieaosfi
https://coub.com/view/d6msuxbq4a
https://coub.com/view/77tzitriot
https://coub.com/view/lpsk81odp9
https://coub.com/view/2nmdnujl00
https://coub.com/view/l8mq0g4760
https://coub.com/view/2mkc8ze5zx
https://coub.com/view/ig0wfzltc6
https://coub.com/view/1ycu3zf1lh
https://coub.com/view/wzat8knm2z
https://coub.com/view/64fv7jr83g
https://coub.com/view/a3did3vro0
https://coub.com/view/ti645azepy
https://coub.com/view/g1vuwgj26b
https://coub.com/view/ffzr61ask8
https://coub.com/view/p5bbm0hek0
https://coub.com/view/g3cd11h0y6
https://coub.com/view/3sf0ida4ix
https://coub.com/view/7bu9ihff89
https://coub.com/view/xjn62txhsl
https://coub.com/view/nlk1uu6rts
https://coub.com/view/t0d2e48ya0
https://coub.com/view/nseh2ehyd9
https://coub.com/view/5v4pyo0v0k
https://coub.com/view/rierf8fjyo

收起阅读 »

如何实现多行文本行中省略的效果

问题现象
Text文本多行时,可以利用textOverflow来设置省略效果,但是设置EllipsisMode.CENTER不生效。

效果预览
点击放大

背景知识
根据ellipsisMode使用说明,可以看到EllipsisMode.START和EllipsisMode.CENTER仅在单行超长文本生效,所以文本多行时设置该属性不生效,需要使用其他方式实现。
measureTextSize可以计算超长文本的宽度,利用文本计算可以算出超长文本的首行字符,然后把剩余的文本设置成一行即可使用ellipsisMode属性。
解决方案
首先利用MeasureUtils.measureTextSize算得文本长度(px单位)。
textSize: SizeOptions = this.uiContextMeasure.measureTextSize({
textContent: this.textContent,
fontSize: 16,
});
然后利用vp2px方法可以算得Text组件一行所占宽度(px单位)。
this.textWidthPx = this.getUIContext().vp2px(this.textWidth);
再然后根据公式文本字符数(Text组件一行所占宽度/文本总长度)即可算出一行所占字符数。
this.oneLineLength = this.textContent.length
this.textWidthPx / (this.textSize.width as number);
最后把文本分成两部分,第一个Text组件完整展示一行所占字符,第二个Text组件展示剩余字符,并且设置ellipsisMode效果。
完整示例参考如下:

import { MeasureUtils } from '@kit.ArkUI';

@Entry
@Component
struct MultipleLinesTest {
private textContent: string = '超长的文本在这里展示出来了超长的文本在这里展示出来了超长的文本在这里展示出来了';
uiContextMeasure: MeasureUtils = this.getUIContext().getMeasureUtils();
textSize: SizeOptions = this.uiContextMeasure.measureTextSize({
textContent: this.textContent,
fontSize: 16,
});
// Text组件宽度
private textWidth: number = 200;
private textWidthPx: number = 0;
private oneLineLength: number = 0;

aboutToAppear(): void {
this.textWidthPx = this.getUIContext().vp2px(this.textWidth);
this.oneLineLength = this.textContent.length * this.textWidthPx / (this.textSize.width as number);
}

build() {
Row() {
Column() {
// 计算出来的第一行文本
Text(this.textContent.slice(0, this.oneLineLength))
.width(this.textWidth)
.fontSize(16);
// 除去第一行文本的其他文本
Text(this.textContent.slice(this.oneLineLength, this.textContent.length))
.width(this.textWidth)
.fontSize(16)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.wordBreak(WordBreak.BREAK_ALL)
.ellipsisMode(EllipsisMode.CENTER);
}
.width('100%');
}
.height('100%');
}
}
https://coub.com/view/5v4pyo0v0k
https://coub.com/view/rierf8fjyo
https://coub.com/view/0dhjlm9xm0
https://coub.com/view/izcki9umfp
https://coub.com/view/ogctl14bzy
https://coub.com/view/5iv3mgfo5w
https://coub.com/view/or2itm0r6f
https://coub.com/view/31bcj3aoh9
https://coub.com/view/ka3j9gfp0s
https://coub.com/view/6w3l43cl7b
https://coub.com/view/7ps2t8jww5
https://coub.com/view/xqbzsvnaky
https://coub.com/view/ex4dqcp8ra
https://coub.com/view/3rvua29wz3
https://coub.com/view/vuy1vyht19
https://coub.com/view/h4z41gf4f5
https://coub.com/view/b9j0j12t46
https://coub.com/view/1g1hdexidq
https://coub.com/view/hs7ulipjdo
https://coub.com/view/y9dhmamozu
https://coub.com/view/521iddwwes
https://coub.com/view/i574bcgkim
https://coub.com/view/ss3b5hkg1b
https://coub.com/view/gbpdbrhdlj
https://coub.com/view/xpyqxswiqd
https://coub.com/view/906rc1c852
https://coub.com/view/pkfznr3x92
https://coub.com/view/ud24qdpcfm
https://coub.com/view/v4v85jtyfz
https://coub.com/view/t54aepmoa3
https://coub.com/view/54qvh3fchk
https://coub.com/view/nf0sbfqlrs
https://coub.com/view/ltpgbnenh3
https://coub.com/view/uhe9ku1592
https://coub.com/view/r3jcvscb08
https://coub.com/view/zq0t05lsn1
https://coub.com/view/r35ls78si0
https://coub.com/view/h17w3im0wx
https://coub.com/view/vy1snskqo0
https://coub.com/view/m61gvu0o5m
https://coub.com/view/16yv7fbys1
https://coub.com/view/70uonw64yn
https://coub.com/view/9e54dokt57
https://coub.com/view/xssaqaoc94
https://coub.com/view/1zc6m5fx3q
https://coub.com/view/ei9h7v4d35
https://coub.com/view/84w2f7fl2y
https://coub.com/view/n6k6hiq4ty
https://coub.com/view/rgy7nndubm
https://coub.com/view/0arengtwst
https://coub.com/view/bd9b0wm479
https://coub.com/view/7u8yw80l0s
https://coub.com/view/5xyj48uskd
https://coub.com/view/bqwh5400qo
https://coub.com/view/w0vl2wfxoi
https://coub.com/view/0o77da159j
https://coub.com/view/dvxdh2r3g3
https://coub.com/view/ar0ad94t2j
https://coub.com/view/y10xeqbelx
https://coub.com/view/v0w1bm4jyt
https://coub.com/view/ts4n90inns
https://coub.com/view/6g5ubpxqkp
https://coub.com/view/m5afs183y0
https://coub.com/view/osle10a0bm
https://coub.com/view/xhv7ovwru3
https://coub.com/view/sjh0ih5i0j
https://coub.com/view/0kolygd9ss
https://coub.com/view/hhvsrn818k
https://coub.com/view/edbis9abo6
https://coub.com/view/nf03ydb9t6
https://coub.com/view/qepjqhopxi
https://coub.com/view/jj1vcbqlv3
https://coub.com/view/f5lzvz3anu
https://coub.com/view/5sntc6322p
https://coub.com/view/9xo18kza0i
https://coub.com/view/cegg7pc7zm
https://coub.com/view/0k5no9zjhp
https://coub.com/view/z86vdbazck
https://coub.com/view/m0kmg9wabe

继续阅读 »

问题现象
Text文本多行时,可以利用textOverflow来设置省略效果,但是设置EllipsisMode.CENTER不生效。

效果预览
点击放大

背景知识
根据ellipsisMode使用说明,可以看到EllipsisMode.START和EllipsisMode.CENTER仅在单行超长文本生效,所以文本多行时设置该属性不生效,需要使用其他方式实现。
measureTextSize可以计算超长文本的宽度,利用文本计算可以算出超长文本的首行字符,然后把剩余的文本设置成一行即可使用ellipsisMode属性。
解决方案
首先利用MeasureUtils.measureTextSize算得文本长度(px单位)。
textSize: SizeOptions = this.uiContextMeasure.measureTextSize({
textContent: this.textContent,
fontSize: 16,
});
然后利用vp2px方法可以算得Text组件一行所占宽度(px单位)。
this.textWidthPx = this.getUIContext().vp2px(this.textWidth);
再然后根据公式文本字符数(Text组件一行所占宽度/文本总长度)即可算出一行所占字符数。
this.oneLineLength = this.textContent.length
this.textWidthPx / (this.textSize.width as number);
最后把文本分成两部分,第一个Text组件完整展示一行所占字符,第二个Text组件展示剩余字符,并且设置ellipsisMode效果。
完整示例参考如下:

import { MeasureUtils } from '@kit.ArkUI';

@Entry
@Component
struct MultipleLinesTest {
private textContent: string = '超长的文本在这里展示出来了超长的文本在这里展示出来了超长的文本在这里展示出来了';
uiContextMeasure: MeasureUtils = this.getUIContext().getMeasureUtils();
textSize: SizeOptions = this.uiContextMeasure.measureTextSize({
textContent: this.textContent,
fontSize: 16,
});
// Text组件宽度
private textWidth: number = 200;
private textWidthPx: number = 0;
private oneLineLength: number = 0;

aboutToAppear(): void {
this.textWidthPx = this.getUIContext().vp2px(this.textWidth);
this.oneLineLength = this.textContent.length * this.textWidthPx / (this.textSize.width as number);
}

build() {
Row() {
Column() {
// 计算出来的第一行文本
Text(this.textContent.slice(0, this.oneLineLength))
.width(this.textWidth)
.fontSize(16);
// 除去第一行文本的其他文本
Text(this.textContent.slice(this.oneLineLength, this.textContent.length))
.width(this.textWidth)
.fontSize(16)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.wordBreak(WordBreak.BREAK_ALL)
.ellipsisMode(EllipsisMode.CENTER);
}
.width('100%');
}
.height('100%');
}
}
https://coub.com/view/5v4pyo0v0k
https://coub.com/view/rierf8fjyo
https://coub.com/view/0dhjlm9xm0
https://coub.com/view/izcki9umfp
https://coub.com/view/ogctl14bzy
https://coub.com/view/5iv3mgfo5w
https://coub.com/view/or2itm0r6f
https://coub.com/view/31bcj3aoh9
https://coub.com/view/ka3j9gfp0s
https://coub.com/view/6w3l43cl7b
https://coub.com/view/7ps2t8jww5
https://coub.com/view/xqbzsvnaky
https://coub.com/view/ex4dqcp8ra
https://coub.com/view/3rvua29wz3
https://coub.com/view/vuy1vyht19
https://coub.com/view/h4z41gf4f5
https://coub.com/view/b9j0j12t46
https://coub.com/view/1g1hdexidq
https://coub.com/view/hs7ulipjdo
https://coub.com/view/y9dhmamozu
https://coub.com/view/521iddwwes
https://coub.com/view/i574bcgkim
https://coub.com/view/ss3b5hkg1b
https://coub.com/view/gbpdbrhdlj
https://coub.com/view/xpyqxswiqd
https://coub.com/view/906rc1c852
https://coub.com/view/pkfznr3x92
https://coub.com/view/ud24qdpcfm
https://coub.com/view/v4v85jtyfz
https://coub.com/view/t54aepmoa3
https://coub.com/view/54qvh3fchk
https://coub.com/view/nf0sbfqlrs
https://coub.com/view/ltpgbnenh3
https://coub.com/view/uhe9ku1592
https://coub.com/view/r3jcvscb08
https://coub.com/view/zq0t05lsn1
https://coub.com/view/r35ls78si0
https://coub.com/view/h17w3im0wx
https://coub.com/view/vy1snskqo0
https://coub.com/view/m61gvu0o5m
https://coub.com/view/16yv7fbys1
https://coub.com/view/70uonw64yn
https://coub.com/view/9e54dokt57
https://coub.com/view/xssaqaoc94
https://coub.com/view/1zc6m5fx3q
https://coub.com/view/ei9h7v4d35
https://coub.com/view/84w2f7fl2y
https://coub.com/view/n6k6hiq4ty
https://coub.com/view/rgy7nndubm
https://coub.com/view/0arengtwst
https://coub.com/view/bd9b0wm479
https://coub.com/view/7u8yw80l0s
https://coub.com/view/5xyj48uskd
https://coub.com/view/bqwh5400qo
https://coub.com/view/w0vl2wfxoi
https://coub.com/view/0o77da159j
https://coub.com/view/dvxdh2r3g3
https://coub.com/view/ar0ad94t2j
https://coub.com/view/y10xeqbelx
https://coub.com/view/v0w1bm4jyt
https://coub.com/view/ts4n90inns
https://coub.com/view/6g5ubpxqkp
https://coub.com/view/m5afs183y0
https://coub.com/view/osle10a0bm
https://coub.com/view/xhv7ovwru3
https://coub.com/view/sjh0ih5i0j
https://coub.com/view/0kolygd9ss
https://coub.com/view/hhvsrn818k
https://coub.com/view/edbis9abo6
https://coub.com/view/nf03ydb9t6
https://coub.com/view/qepjqhopxi
https://coub.com/view/jj1vcbqlv3
https://coub.com/view/f5lzvz3anu
https://coub.com/view/5sntc6322p
https://coub.com/view/9xo18kza0i
https://coub.com/view/cegg7pc7zm
https://coub.com/view/0k5no9zjhp
https://coub.com/view/z86vdbazck
https://coub.com/view/m0kmg9wabe

收起阅读 »

UI布局默认是多少vp为基准,以达到不同机器自适应

无论屏幕分辨率或密度如何,组件的视觉效果保持一致。

vp具体计算公式为:vp= px/(DPI/160)

px 是屏幕的真实物理像素值,densityDPI 通常指系统屏幕密度,densityPixels是屏幕密度与标准DPI的比率,常见取值有 0.75、1.0、1.5、2.0、3.0 等。在HarmonyOS中,标准DPI为160。以华为Mate 40 Pro为例,densityDPI为 560,densityPixels为3.5。要查看真机的DPI,可以调用屏幕属性中的display接口查询。

import { display } from '@kit.ArkUI';

let displayClass: display.Display | null = null;
try {
displayClass = display.getDefaultDisplaySync();
} catch (exception) {
console.error('Failed to obtain the default display object. Code: ' + JSON.stringify(exception));
}
AdaptiveForDifferentmMachines.ets
如果原型图没有提供vp单位的布局,开发者可以根据densityPixels把px转为vp,HarmonyOS也封装了现成的接口px2vp()和vp2px()供开发者直接调用。
https://coub.com/view/1ssmnmys5l
https://coub.com/view/b8vv1qlbt7
https://coub.com/view/4njv0pb1sf
https://coub.com/view/2osl0d6206
https://coub.com/view/8hcuett3go
https://coub.com/view/wb4oa55cid
https://coub.com/view/3cnyxlt5cu
https://coub.com/view/jyzx0cl5pb
https://coub.com/view/rzh0ovu8wc
https://coub.com/view/601vumd0bk
https://coub.com/view/kryjg96qpm
https://coub.com/view/zxp8nixohu
https://coub.com/view/s3zzuk1pfh
https://coub.com/view/th5gpreu9p
https://coub.com/view/vvp0k5p5g2
https://coub.com/view/ug9i0nx3ry
https://coub.com/view/a6hg663761
https://coub.com/view/r2ybmjgcb0
https://coub.com/view/qyxo2oo0vm
https://coub.com/view/eqrncvrd0k
https://coub.com/view/f60efr44zc
https://coub.com/view/8yqp6z887o
https://coub.com/view/l931hfzcac
https://coub.com/view/tit85csau0
https://coub.com/view/k2qbxk9zw4
https://coub.com/view/ag4fxvk8j8
https://coub.com/view/079frfgd9z
https://coub.com/view/h0uny3c8xm
https://coub.com/view/9xqoqiqfbs
https://coub.com/view/3wfhfva45l
https://coub.com/view/c5jmnpf7bo
https://coub.com/view/qvuopnvg0p
https://coub.com/view/qkdi3a72io
https://coub.com/view/vmk22b54yi
https://coub.com/view/a0f6c8kp4s
https://coub.com/view/wadaaaejro
https://coub.com/view/txg69p365y
https://coub.com/view/sm74dy9gq1
https://coub.com/view/5x3owd7gnj
https://coub.com/view/ahwe5wmhis
https://coub.com/view/q6ulkpx5zl
https://coub.com/view/i3utbril6v
https://coub.com/view/hdtgtno7cv
https://coub.com/view/5zgydpfi1g
https://coub.com/view/81ruiq2gsm
https://coub.com/view/djhntig6et
https://coub.com/view/g6ompv5j9v
https://coub.com/view/ogd7m4t2nv
https://coub.com/view/uvlxpdhlx8
https://coub.com/view/gfpc1o7dy8
https://coub.com/view/6iljnuq8rp
https://coub.com/view/vof5o7imk9
https://coub.com/view/hthbnpqg3u
https://coub.com/view/5ljk3310jb
https://coub.com/view/xqlksk0uwr
https://coub.com/view/us08m31g3o
https://coub.com/view/xt8c9f9ik6
https://coub.com/view/nwwjnlg6u3
https://coub.com/view/r8zxzny4n6
https://coub.com/view/80fbenpftn
https://coub.com/view/69127p3k65
https://coub.com/view/uzi71doc2a
https://coub.com/view/imxc29ism8
https://coub.com/view/c14zdeevut
https://coub.com/view/biphkw6uhd
https://coub.com/view/luqv2os2af
https://coub.com/view/chz2n7wk98
https://coub.com/view/c7f3b1dses
https://coub.com/view/t66yfm5wcq
https://coub.com/view/pn27l55do8
https://coub.com/view/sz40uzshaw
https://coub.com/view/o7j89d8rwn
https://coub.com/view/aa3k89ktsk
https://coub.com/view/lf0d5hsld6
https://coub.com/view/gu5537brfz
https://coub.com/view/6uio4ilcrf
https://coub.com/view/ms6kce573a
https://coub.com/view/0lbw9ytytc
https://coub.com/view/gxlgdqofxz
https://coub.com/view/3pln3aqq2k

继续阅读 »

无论屏幕分辨率或密度如何,组件的视觉效果保持一致。

vp具体计算公式为:vp= px/(DPI/160)

px 是屏幕的真实物理像素值,densityDPI 通常指系统屏幕密度,densityPixels是屏幕密度与标准DPI的比率,常见取值有 0.75、1.0、1.5、2.0、3.0 等。在HarmonyOS中,标准DPI为160。以华为Mate 40 Pro为例,densityDPI为 560,densityPixels为3.5。要查看真机的DPI,可以调用屏幕属性中的display接口查询。

import { display } from '@kit.ArkUI';

let displayClass: display.Display | null = null;
try {
displayClass = display.getDefaultDisplaySync();
} catch (exception) {
console.error('Failed to obtain the default display object. Code: ' + JSON.stringify(exception));
}
AdaptiveForDifferentmMachines.ets
如果原型图没有提供vp单位的布局,开发者可以根据densityPixels把px转为vp,HarmonyOS也封装了现成的接口px2vp()和vp2px()供开发者直接调用。
https://coub.com/view/1ssmnmys5l
https://coub.com/view/b8vv1qlbt7
https://coub.com/view/4njv0pb1sf
https://coub.com/view/2osl0d6206
https://coub.com/view/8hcuett3go
https://coub.com/view/wb4oa55cid
https://coub.com/view/3cnyxlt5cu
https://coub.com/view/jyzx0cl5pb
https://coub.com/view/rzh0ovu8wc
https://coub.com/view/601vumd0bk
https://coub.com/view/kryjg96qpm
https://coub.com/view/zxp8nixohu
https://coub.com/view/s3zzuk1pfh
https://coub.com/view/th5gpreu9p
https://coub.com/view/vvp0k5p5g2
https://coub.com/view/ug9i0nx3ry
https://coub.com/view/a6hg663761
https://coub.com/view/r2ybmjgcb0
https://coub.com/view/qyxo2oo0vm
https://coub.com/view/eqrncvrd0k
https://coub.com/view/f60efr44zc
https://coub.com/view/8yqp6z887o
https://coub.com/view/l931hfzcac
https://coub.com/view/tit85csau0
https://coub.com/view/k2qbxk9zw4
https://coub.com/view/ag4fxvk8j8
https://coub.com/view/079frfgd9z
https://coub.com/view/h0uny3c8xm
https://coub.com/view/9xqoqiqfbs
https://coub.com/view/3wfhfva45l
https://coub.com/view/c5jmnpf7bo
https://coub.com/view/qvuopnvg0p
https://coub.com/view/qkdi3a72io
https://coub.com/view/vmk22b54yi
https://coub.com/view/a0f6c8kp4s
https://coub.com/view/wadaaaejro
https://coub.com/view/txg69p365y
https://coub.com/view/sm74dy9gq1
https://coub.com/view/5x3owd7gnj
https://coub.com/view/ahwe5wmhis
https://coub.com/view/q6ulkpx5zl
https://coub.com/view/i3utbril6v
https://coub.com/view/hdtgtno7cv
https://coub.com/view/5zgydpfi1g
https://coub.com/view/81ruiq2gsm
https://coub.com/view/djhntig6et
https://coub.com/view/g6ompv5j9v
https://coub.com/view/ogd7m4t2nv
https://coub.com/view/uvlxpdhlx8
https://coub.com/view/gfpc1o7dy8
https://coub.com/view/6iljnuq8rp
https://coub.com/view/vof5o7imk9
https://coub.com/view/hthbnpqg3u
https://coub.com/view/5ljk3310jb
https://coub.com/view/xqlksk0uwr
https://coub.com/view/us08m31g3o
https://coub.com/view/xt8c9f9ik6
https://coub.com/view/nwwjnlg6u3
https://coub.com/view/r8zxzny4n6
https://coub.com/view/80fbenpftn
https://coub.com/view/69127p3k65
https://coub.com/view/uzi71doc2a
https://coub.com/view/imxc29ism8
https://coub.com/view/c14zdeevut
https://coub.com/view/biphkw6uhd
https://coub.com/view/luqv2os2af
https://coub.com/view/chz2n7wk98
https://coub.com/view/c7f3b1dses
https://coub.com/view/t66yfm5wcq
https://coub.com/view/pn27l55do8
https://coub.com/view/sz40uzshaw
https://coub.com/view/o7j89d8rwn
https://coub.com/view/aa3k89ktsk
https://coub.com/view/lf0d5hsld6
https://coub.com/view/gu5537brfz
https://coub.com/view/6uio4ilcrf
https://coub.com/view/ms6kce573a
https://coub.com/view/0lbw9ytytc
https://coub.com/view/gxlgdqofxz
https://coub.com/view/3pln3aqq2k

收起阅读 »

滚动与滑动组件实现吸顶效果

问题现象
在HarmonyOS中,如何使用不同的滚动与滑动组件,实现标题吸顶效果?

背景知识
吸顶效果是网页开发中的一种常见交互设计,指当用户滚动页面时,某个元素(如导航栏、标题栏、工具栏等)会固定在浏览器窗口的顶部(或其他指定位置),保持始终可见,不会随着页面滚动而消失。在开发过程中,吸顶效果通常用于需要保持关键元素始终可见的场景,以提升用户体验和操作效率。

在HarmonyOS中有多种实现吸顶效果的方案,以及不同的吸顶效果,不同方案及其需要了解的知识如下:

Tabs:通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
nestedScroll:设置前后两个方向的嵌套滚动模式,实现与父组件的滚动联动。
List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
sticky:配合ListItemGroup组件使用,设置ListItemGroup中header是否要吸顶或footer是否要吸底。sticky属性可以设置为StickyStyle.Header|StickyStyle.Footer以同时支持header吸顶和footer吸底。
解决方案
本文主要介绍实现普通吸顶效果、单标题滚动嵌套吸顶效果和多标题滚动嵌套吸顶效果的方案。

普通吸顶效果
方案一:通过Tabs组件的tabBar属性实现吸顶效果。可以自定义实现Tabs效果。
示例代码如下:
@Entry
@Component
export struct CommonSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {
TabContent() {
List({ space: 10 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Row() {
Text('item' + item)
.fontSize(16)
.height(72)
.fontColor(Color.Black);
}
.width('100%')
.height(72)
.justifyContent(FlexAlign.Center);
}
.borderRadius(15)
.backgroundColor('#F1F3F5');
}, (item: string) => item);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.padding({ left: 10, right: 10 })
.width('100%')
.height('100%')
.scrollBar(BarState.Off);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.tabBar('关注')
.backgroundColor('#ffffff');

  TabContent() {  
    Text('推荐');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .tabBar('推荐')  
  .backgroundColor('#ffffff');  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.backgroundColor('#ffffff');  

}
}
实现效果如下:

点击放大

方案二:自定义标题的吸顶实现。若要实现Tabs页面切换效果,需要自定义页面切换逻辑。
参考上文中方案一,示例代码如下:
@Entry
@Component
export struct CommonSolutionTwo {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Column() {
Row() {
// 吸顶标题
Text('自定义占位标题')
.width('100%')
.height(60)
.textAlign(TextAlign.Center)
.backgroundColor('#66666666');
};

  List({ space: 10 }) {  
    ForEach(this.arr, (item: number) => {  
      ListItem() {  
        Row() {  
          Text('item' + item)  
            .fontSize(16)  
            .height(72)  
            .fontColor(Color.Black);  
        }  
        .width('100%')  
        .height(72)  
        .justifyContent(FlexAlign.Center);  
      }  
      .borderRadius(15)  
      .backgroundColor('#F1F3F5');  
    }, (item: string) => item);  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .padding({ left: 10, right: 10 })  
  .width('100%')  
  .scrollBar(BarState.Off)  
  .layoutWeight(1); // 更换List组件height属性,高度限制以layoutWeight(1)的形式自动填充父组件高度  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.width('100%')  
.height('100%') // 普通吸顶效果时,滚动组件的父组件可以不设置高度,但是滚动嵌套吸顶时一定要设置高度限制为100%  
.backgroundColor('#ffffff');  

}
}
实现效果如下:

点击放大

单标题滚动嵌套吸顶效果
方案一: 通过nestedScroll属性,实现吸顶效果。由于nestedScroll属性是滚动与滑动组件的通用属性,理论上在滚动组件嵌套的过程中,支持该属性的滚动组件都可以实现吸顶效果。以Tabs组件为例,示例代码如下:
@Entry
@Component
struct SingleTitleSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Scroll() {
Column() {
Column() {
Text('自定义占位标题')
.fontSize(30)
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])

    Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {  
      TabContent() {  
        List({ space: 10 }) {  
          ForEach(this.arr, (item: number) => {  
            ListItem() {  
              Row() {  
                Text('item' + item)  
                  .fontSize(16)  
                  .height(72)  
                  .fontColor(Color.Black);  
              }  
              .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
              .width('100%')  
              .height(72)  
              .justifyContent(FlexAlign.Center);  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .borderRadius(15)  
            .backgroundColor('#F1F3F5');  
          }, (item: string) => item);  
        }  
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
        .padding({ left: 10, right: 10 })  
        .width('100%')  
        .height('100%')  
        .scrollBar(BarState.Off)  
        .nestedScroll({  
          scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
          scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
        });  
      }  
      .clip(false)  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .tabBar('关注')  
      .backgroundColor('#ffffff');  

      TabContent() {  
        Text('推荐');  
      }  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .tabBar('推荐')  
      .backgroundColor('#ffffff');  
    }  
    .clip(false)  
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
    .backgroundColor('#ffffff');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .width('100%');  
}  
.clip(false)  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.edgeEffect(EdgeEffect.Spring)  
.friction(0.6)  
.backgroundColor('#ffffff')  
.scrollBar(BarState.Off)  
.width('100%')  
.height('100%');  

}
}
效果预览:

点击放大

说明
自定义标题的实现方式,此处主要讲解滚动组件嵌套实现方式。若是想要以自定义标题实现滚动嵌套吸顶效果,滚动组件的父组件一定要设置高度限制为100%,若不设置高度限制,父组件自适应高度会导致吸顶失效,可详见下文中“多标题滚动嵌套吸顶效果”的方案一,其内第一个标题为自定义标题,及其父组件高度注释。

方案二: 由于Waterflow布局本身不支持吸顶,如需使用Waterflow布局实现吸顶效果,可以在滚动组件上通过Stack叠加一个组件实现,示例代码如下:
// 瀑布流布局项数据模型(用于描述每个子项的布局属性)
export class WaterFlowItemModel {
fullWidth: boolean = false;
stickyTop: number | undefined = undefined;
height: number = 0;
width: number = 0;
bgColor: string = '#f0f0f0';
}

// 瀑布流数据源实现类
export class WaterFlowDataSource implements IDataSource {
private dataArray: WaterFlowItemModel[] = [];
private listeners: DataChangeListener[] = [];

constructor() {
this.dataArray = [];
// 生成10个瀑布流测试数据项
for (let i = 0; i < 10; i++) {
const model = new WaterFlowItemModel();
model.fullWidth = i % 5 === 4;
model.height = 120;
model.bgColor = '#f0f0f0';
this.dataArray.push(model);
}
}

// 获取数据总量
totalCount(): number {
return this.dataArray.length;
}

// 根据索引获取单个数据项
getData(index: number) {
return this.dataArray[index];
}

// 注册数据变更监听器
registerDataChangeListener(listener: DataChangeListener) {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}

// 移除已注册的数据变更监听器
unregisterDataChangeListener(listener: DataChangeListener) {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
}

@Entry
@Component
struct SingleTitleSolutionTwo {
scroller: Scroller = new Scroller();
datasource: WaterFlowDataSource = new WaterFlowDataSource();
@State scrollOffset: number = 0;

build() {
Stack({ alignContent: Alignment.TopStart }) {
WaterFlow({ scroller: this.scroller }) {
LazyForEach(this.datasource, (item: WaterFlowItemModel, index) => {
FlowItem() {
// 当索引不为1,展示当前索引值
if (index !== 1) {
Text(${index});
}
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.margin({ right:20,bottom:12,left:19 })
.borderRadius(15)
.onClick(() => {
console.info('onClick ---- ', index);
})
.width('90%')
.height(item.height)
// .backgroundColor(index === 1 ? '#00000000' : Color.White);
.backgroundColor('#F1F3F5')
}, (key: string) => key);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.edgeEffect(EdgeEffect.Spring)
.onWillScroll((offset: number) => {
this.scrollOffset = this.scroller.currentOffset().yOffset + offset;
})
.width('100%')
.height('100%')
.backgroundColor('#ffffff');

  // 在索引为1的WaterFlowItem上方叠加一个组件  
  Stack() {  
    Text('自定义占位标题')  
      .width('100%')  
      .height(this.datasource.getData(1).height)  
      .fontColor(Color.White)  
      .backgroundColor('#66666666')  
      .fontSize(17)  
      .textAlign(TextAlign.Center);  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .hitTestBehavior(HitTestMode.Transparent)  
  .position({ x: 0, y: this.scrollOffset >= 120 ? 0 : 120 - this.scrollOffset });  

}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.width('100%')  
.height('100%')  
.clip(true);  

}
}
实现效果如下:

点击放大

多标题滚动嵌套吸顶效果
方案一:通过nestedScroll属性对滚动组件多次滚动嵌套,参考单标题滚动嵌套吸顶效果,示例代码如下:
@Entry
@Component
struct MultipleTitleSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3'];
@State arr1: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Scroll() {
Column() {
Column() {
Text('自定义占位标题')
.fontSize(30)
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
};

    Column() {  
      Row() {  
        Text('自定义占位标题') // 自定义实现标题吸顶,依旧采用Tabs组件也适用  
          .fontSize(16)  
          .height(60)  
          .width('100%')  
          .textAlign(TextAlign.Center);  
      }.backgroundColor('#6dbab8b8')  

      Scroll() {  
        Column() {  
          ForEach(this.arr, (item: number) => {  
            Row() {  
              Text('item' + item)  
                .fontSize(16)  
                .height(72)  
                .fontColor(Color.Black);  
            }  
            .width('100%')  
            .height(72)  
            .borderRadius(15)  
            .margin({  
              bottom: 10  
            })  
            .justifyContent(FlexAlign.Center);  
          }, (item: string) => item);  

          Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {  
            TabContent() {  
              List({ space: 10 }) {  
                ForEach(this.arr1, (item: number) => {  
                  ListItem() {  
                    Row() {  
                      Text('item' + item)  
                        .fontSize(16)  
                        .height(72)  
                        .fontColor(Color.Black);  
                    }  
                    .width('100%')  
                    .height(72)  
                    .justifyContent(FlexAlign.Center);  
                  }  
                  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
                  .borderRadius(15)  
                  .backgroundColor(Color.White);  
                }, (item: string) => item);  
              }  
              .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
              .padding({ left: 10, right: 10 })  
              .width('100%')  
              .height('100%')  
              .scrollBar(BarState.Off)  
              .nestedScroll({  
                scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
                scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
              });  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .tabBar('关注')  
            .backgroundColor('#F1F3F5');  

            TabContent() {  
              Text('推荐');  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .tabBar('推荐')  
            .backgroundColor('#F1F3F5');  
          }  
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
          .backgroundColor('#6dbab8b8');  
        }  
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
        .backgroundColor(Color.White)  
        .width('100%');  
      }  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .width('100%')  
      .layoutWeight(1)  
      .scrollBar(BarState.Off)  
      .nestedScroll({  
        scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
        scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
      });  
    }  
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
    .height('100%') // 由于是自定义的标题,所以容纳标题和列表的父组件一定要设置高度为100%  
    .backgroundColor('#6dbab8b8');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .width('100%');  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.edgeEffect(EdgeEffect.Spring)  
.friction(0.6)  
.backgroundColor(Color.White)  
.scrollBar(BarState.Off)  
.width('100%')  
.height('100%');  

}
}
实现效果如下:

点击放大

方案二:通过List组件的sticky属性,实现吸顶效果。
常见场景一:header吸顶和footer吸底,该方案仅适用于List组件,具体实施方案参考官方示例:吸顶/吸底。

常见场景二:多标题多tab菜单吸顶时,实现滑动时自动切换tab栏。通过onDidScroll方法,监听滚动事件,根据滚动偏移量动态计算当前处于哪个区域,再根据当前区块切换标题内容。示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionOnePlus {
@State data1: string[] = ['标题1', '标题2', '标题3', '标题4'];
@State data2: string[] = ['1', '2', '3', '4'];
@State selectIndex1: number = 0;
private listController: ListScroller = new ListScroller();

@Builder
topBuilder1() {
Row() {
ForEach(this.data1, (item: string, index: number) => {
Text(item)
.fontColor(index == this.selectIndex1 ? Color.Blue : '#000')
.fontSize(index == this.selectIndex1 ? 20 : 16)
.fontWeight(index == this.selectIndex1 ? 500 : 400)
.onClick(() => {
this.listController.scrollToItemInGroup(0, index, true);
});
});
}
.backgroundColor(0xAABBCC)
.height(60)
.width('100%')
.justifyContent(FlexAlign.SpaceAround);
}

@Builder
topBuilder2() {
Row() {
Text('第二组吸顶')
.padding(5)
.textAlign(TextAlign.Center);
}
.backgroundColor(0xAABBCC)
.justifyContent(FlexAlign.SpaceAround)
.height(60)
.width('100%');
}

build() {
Column() {
List({ space: 10, scroller: this.listController }) {
ListItemGroup({ header: this.topBuilder1, space: 10 }) {
ForEach(this.data1, (item: string) => {
ListItem() {
Text(item)
.width('90%')
.height(200)
.backgroundColor('#f1f3f5')
.borderRadius(10)
.textAlign(TextAlign.Center);
};
});
};

    ListItemGroup() {  
      ListItem() {  
        Text('其他内容')  
          .width('90%')  
          .height(400)  
          .backgroundColor('#f1f3f5')  
          .borderRadius(10)  
          .fontSize(16)  
          .textAlign(TextAlign.Center);  
      };  
    };  

    ListItemGroup({ header: this.topBuilder2, space: 10 }) {  
      ForEach(this.data2, (item: string) => {  
        ListItem() {  
          Text(item)  
            .width('90%')  
            .height(200)  
            .backgroundColor('#f1f3f5')  
            .borderRadius(10)  
            .textAlign(TextAlign.Center);  
        };  
      });  
    };  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .onDidScroll(() => {  
    let currentOffset = this.listController.currentOffset().yOffset;  
    this.selectIndex1 = Math.floor(currentOffset / 200);  
  })  
  .scrollBar(BarState.Off)  
  .sticky(StickyStyle.Header)  
  .alignListItem(ListItemAlign.Center);  
};  

}
}
实现效果如下:

点击放大

二级标题滚动嵌套吸顶效果
第一层吸顶用Scroll嵌套List实现,第二层是利用ListItemGroup的sticky属性实现,示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionTwo {
private timeTable: TimeTable[] = [
{
title: 'Header2-0',
projects: ['内容1']
},
{
title: 'Header2-1',
projects: ['内容2']
},
{
title: 'Header2-2',
projects: ['内容3']
}
];

@Builder
itemHead(text: string, index: number) {
if (index === 0) {
Text().height(0);
} else {
Text(text)
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.height(index === 1 ? 100 : 200)
.backgroundColor('#d1d1d6');
}
}

build() {
Scroll() {
Column() {
Text('自定义占位标题')
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
Column() {
// 第一层吸顶效果主要是通过设置nestedScroll属性以及父组件设置高度100%实现
Text('Header1')
.width('100%')
.height(100)
.backgroundColor('#4d666666')
.textAlign(TextAlign.Center)
.fontColor(Color.Black);
List() {
ForEach(this.timeTable, (item: TimeTable, index) => {
ListItemGroup({ header: this.itemHead(item.title, index) }) {
ForEach(item.projects, (project: string) => {
ListItem() {
Text(project)
.width('100%')
.height(800)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor('#FFFFFF');
}
}, (item: string) => item);
}.clip(false);
});
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.sticky(StickyStyle.Header)
.scrollBar(BarState.Off)
.width('100%')
.height('calc(100% - 100vp)')
.backgroundColor('#ffffff')
.edgeEffect(EdgeEffect.Spring)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
});
}.clip(false).expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]).height('100%')
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.clip(false)
.edgeEffect(EdgeEffect.Spring)
.friction(0.6)
.backgroundColor(Color.White)
.scrollBar(BarState.Off)
.width('100%')
.height('100%');
}
}

interface TimeTable {
title: string;
projects: string[];
}
实现效果如下:

点击放大

动态控制二级标题滚动嵌套吸顶效果:
在List组件onDidScroll方法里判断索引为2的组件滚动偏移量,动态调整Header1的position属性和List组件的contentStartOffset属性,示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionTwoDynamic {
@State header1Position: number = 0;
@State listStartOffset: number = 100;
private scroller = new Scroller();
private timeTable: TimeTableDynamic[] = [
{
title: 'Header2-0',
projects: ['内容1']
},
{
title: 'Header2-1',
projects: ['内容2']
},
{
title: 'Header2-2',
projects: ['内容3']
}
];

@Builder
itemHead(text: string, index: number) {
if (index === 0) {
Text().height(0);
} else {
Text(text)
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.height(index === 1 ? 100 : 200)
.backgroundColor('#d1d1d6');
}
}

build() {
Scroll() {
Column() {
Text('二级标题滚动嵌套吸顶')
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
Column() {
// 第一层吸顶效果主要是通过设置nestedScroll属性以及父组件设置高度100%实现
Text('Header1')
.width('100%')
.height(100)
.backgroundColor('#4d666666')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.position({ y: this.header1Position })
.zIndex(10);
List({ scroller: this.scroller }) {
ForEach(this.timeTable, (item: TimeTableDynamic, index) => {
ListItemGroup({ header: this.itemHead(item.title, index) }) {
ForEach(item.projects, (project: string) => {
ListItem() {
Text(project)
.width('100%')
.height(800)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor('#ffffff');
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]);
}, (item: string) => item);
};
});
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.sticky(StickyStyle.Header)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
.backgroundColor('#ffffff')
.edgeEffect(EdgeEffect.Spring)
.contentStartOffset(this.listStartOffset)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
.onDidScroll(() => {
let secondRect = this.scroller.getItemRect(2);
if (secondRect.x === 0 && secondRect.y === 0 && secondRect.width === 0 && secondRect.height === 0) {
console.info(索引为${2}的组件不在屏幕上);
return;
}
this.header1Position = Math.min(0, secondRect.y - 200);
this.listStartOffset = Math.max(0, 100 + this.header1Position);
});
}.height('100%');
};
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.clip(false)
.edgeEffect(EdgeEffect.Spring)
.friction(0.6)
.backgroundColor(Color.White)
.scrollBar(BarState.Off)
.width('100%')
.height('100%');
}
}

interface TimeTableDynamic {
title: string;
projects: string[];
}
https://coub.com/view/gylbvhqrkz
https://coub.com/view/gqrdnsqbsm
https://coub.com/view/0a3thuu2b4
https://coub.com/view/lg6lw08ivp
https://coub.com/view/qzxlw9ogx1
https://coub.com/view/68z9behrh7
https://coub.com/view/4iijkgxtaa
https://coub.com/view/xuelqryeob
https://coub.com/view/037iwxjpf9
https://coub.com/view/ydu5rfse9r
https://coub.com/view/g71sfv35n0
https://coub.com/view/hmkl7icf96
https://coub.com/view/7gf8jpjeqf
https://coub.com/view/6ar7vzg3jt
https://coub.com/view/x6brb1zhst
https://coub.com/view/esc2tpa50i
https://coub.com/view/ij4i54b4su
https://coub.com/view/6csj522lvg
https://coub.com/view/oaspyursw3
https://coub.com/view/gvem0zleaj
https://coub.com/view/xs8g570e03
https://coub.com/view/suwbkm015s
https://coub.com/view/74yubqnb80
https://coub.com/view/6wh2qfuaws
https://coub.com/view/2yv1gta5d2
https://coub.com/view/hopm4eqo5c
https://coub.com/view/q1tm3sdodd
https://coub.com/view/7udyb7fyhi
https://coub.com/view/1z3anr32zq
https://coub.com/view/jhh5mhj3y0
https://coub.com/view/dvq9c4eyfc
https://coub.com/view/wv83dp9qeo
https://coub.com/view/a9i5tkfl9w
https://coub.com/view/v372pqbiis
https://coub.com/view/ig9545no10
https://coub.com/view/w79ltp6ou9
https://coub.com/view/u9s0qk1akf
https://coub.com/view/damsu4njzt
https://coub.com/view/v2vriht1bd
https://coub.com/view/e93br3m9qa
https://coub.com/view/xd56t399ne
https://coub.com/view/dnvfpx6l5b
https://coub.com/view/pehxieghx6
https://coub.com/view/6w14zzs972
https://coub.com/view/46b628kwgi
https://coub.com/view/o5bg5eawhx
https://coub.com/view/rrinp7vw3c
https://coub.com/view/mamo602b5a
https://coub.com/view/hlvm2nw499
https://coub.com/view/jzu48fqzw5
https://coub.com/view/ncc5pejn2y
https://coub.com/view/o61yarijz2
https://coub.com/view/xt572wfwwl
https://coub.com/view/t4z6lnd7n8
https://coub.com/view/cxz5p1rtil
https://coub.com/view/a630gf5urg
https://coub.com/view/7khrln5czp
https://coub.com/view/u52ikkf9j8
https://coub.com/view/6ispqsd85w
https://coub.com/view/g5ndpkf32e
https://coub.com/view/nf11v6hl00
https://coub.com/view/g2va9aj5ue
https://coub.com/view/qe1rsfh7ey
https://coub.com/view/ldvx0v2kr2
https://coub.com/view/w3bct13r9i
https://coub.com/view/3zyw0cjxo1
https://coub.com/view/ny9foponj5
https://coub.com/view/g4q545h3da
https://coub.com/view/kekg8y04v7
https://coub.com/view/ms20cjnzki
https://coub.com/view/6hg6xx4f3u
https://coub.com/view/4gd6mjpi41
https://coub.com/view/91selywfey
https://coub.com/view/d9lltiovbh
https://coub.com/view/j6dhi1cqlu
https://coub.com/view/pupns1qvyy
https://coub.com/view/42eqvna2sj
https://coub.com/view/213yl91c0r
https://coub.com/view/ligxe8niat
https://sites.google.com/view/4npswg73/home
https://sites.google.com/view/1cqggg42/home
https://sites.google.com/view/7hwthv95/home
https://sites.google.com/view/8ftpag65/home
https://sites.google.com/view/9hupnv16/home
https://sites.google.com/view/9rcwcc44/home
https://sites.google.com/view/4ygxfw66/home
https://sites.google.com/view/7yjaoo71/home
https://sites.google.com/view/6euxnz02/home
https://sites.google.com/view/2xrcwv82/home
https://sites.google.com/view/3bruzf22/home
https://sites.google.com/view/1fampb85/home
https://sites.google.com/view/0uqjgm22/home
https://sites.google.com/view/8lytyx96/home
https://sites.google.com/view/2rcvcx10/home
https://sites.google.com/view/0wxard26/home
https://sites.google.com/view/0rhcrm43/home
https://sites.google.com/view/1cilam64/home
https://sites.google.com/view/2dsnqw73/home
https://sites.google.com/view/0vgsrq46/home
https://sites.google.com/view/8nqpzw07/home
https://sites.google.com/view/2zlysr71/home
https://sites.google.com/view/6krwym27/home
https://sites.google.com/view/6xuung21/home
https://sites.google.com/view/5irzjv73/home
https://sites.google.com/view/7lukby92/home
https://sites.google.com/view/3oyfhr95/home
https://sites.google.com/view/2srcwe43/home
https://sites.google.com/view/0setkv89/home
https://sites.google.com/view/5rvxgj88/home
https://sites.google.com/view/7tfahr42/home
https://sites.google.com/view/7yboep62/home
https://sites.google.com/view/4upmsh64/home
https://sites.google.com/view/0zwrrv97/home
https://sites.google.com/view/2gawzu13/home
https://sites.google.com/view/6qeyzl51/home
https://sites.google.com/view/3ievss43/home
https://sites.google.com/view/0ibpqm02/home
https://sites.google.com/view/3awrap71/home
https://sites.google.com/view/0tvqkg18/home
https://sites.google.com/view/5sbyjy90/home
https://sites.google.com/view/6ehwco13/home
https://sites.google.com/view/7tjzdg23/home
https://sites.google.com/view/6rakcq50/home
https://sites.google.com/view/7hexol86/home
https://sites.google.com/view/0cyonp67/home
https://sites.google.com/view/7cpmvf18/home
https://sites.google.com/view/7qroxh49/home
https://sites.google.com/view/2qshnp57/home
https://sites.google.com/view/6dwyim71/home
https://sites.google.com/view/4cegdc43/home
https://sites.google.com/view/9kbwox44/home
https://sites.google.com/view/6sciwd21/home
https://sites.google.com/view/6bjrsb10/home
https://sites.google.com/view/3qxmma28/home
https://sites.google.com/view/7cbftv08/home
https://sites.google.com/view/4kkccj88/home
https://sites.google.com/view/5pjonl99/home
https://sites.google.com/view/4aeacr58/home
https://sites.google.com/view/6fiuxa97/home
https://sites.google.com/view/5brhgy37/home
https://sites.google.com/view/1mzbfo35/home
https://sites.google.com/view/9kbckv34/home
https://sites.google.com/view/9cobir53/home
https://sites.google.com/view/6qgbcm33/home
https://sites.google.com/view/8lgcnx33/home
https://sites.google.com/view/9jzmzc49/home
https://sites.google.com/view/0bujrj34/home
https://sites.google.com/view/1heuxf15/home
https://sites.google.com/view/0przrr24/home
https://sites.google.com/view/8gmodp38/home
https://sites.google.com/view/1qgplx06/home
https://sites.google.com/view/1vbfom31/home
https://sites.google.com/view/4fgfqr84/home
https://sites.google.com/view/6oybbx22/home
https://sites.google.com/view/1knpbg15/home
https://sites.google.com/view/0opgke85/home
https://sites.google.com/view/7kirma83/home
https://sites.google.com/view/1zzbdj74/home
https://sites.google.com/view/8csmsa36/home
https://sites.google.com/view/1edswe69/home
https://sites.google.com/view/8kyxaz85/home
https://sites.google.com/view/8bplrw32/home
https://sites.google.com/view/7pdmfv03/home
https://sites.google.com/view/8qkoih30/home
https://sites.google.com/view/4noxpv38/home
https://sites.google.com/view/4itlbr61/home
https://sites.google.com/view/5rjefu09/home
https://sites.google.com/view/5ujnxe58/home
https://sites.google.com/view/3gqlqd93/home
https://sites.google.com/view/4ijybb89/home
https://sites.google.com/view/6ipevr32/home
https://sites.google.com/view/5mhxux08/home
https://sites.google.com/view/8wojcb88/home
https://sites.google.com/view/0riyrm78/home

继续阅读 »

问题现象
在HarmonyOS中,如何使用不同的滚动与滑动组件,实现标题吸顶效果?

背景知识
吸顶效果是网页开发中的一种常见交互设计,指当用户滚动页面时,某个元素(如导航栏、标题栏、工具栏等)会固定在浏览器窗口的顶部(或其他指定位置),保持始终可见,不会随着页面滚动而消失。在开发过程中,吸顶效果通常用于需要保持关键元素始终可见的场景,以提升用户体验和操作效率。

在HarmonyOS中有多种实现吸顶效果的方案,以及不同的吸顶效果,不同方案及其需要了解的知识如下:

Tabs:通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。
nestedScroll:设置前后两个方向的嵌套滚动模式,实现与父组件的滚动联动。
List:列表包含一系列相同宽度的列表项。适合连续、多行呈现同类数据,例如图片和文本。
sticky:配合ListItemGroup组件使用,设置ListItemGroup中header是否要吸顶或footer是否要吸底。sticky属性可以设置为StickyStyle.Header|StickyStyle.Footer以同时支持header吸顶和footer吸底。
解决方案
本文主要介绍实现普通吸顶效果、单标题滚动嵌套吸顶效果和多标题滚动嵌套吸顶效果的方案。

普通吸顶效果
方案一:通过Tabs组件的tabBar属性实现吸顶效果。可以自定义实现Tabs效果。
示例代码如下:
@Entry
@Component
export struct CommonSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {
TabContent() {
List({ space: 10 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Row() {
Text('item' + item)
.fontSize(16)
.height(72)
.fontColor(Color.Black);
}
.width('100%')
.height(72)
.justifyContent(FlexAlign.Center);
}
.borderRadius(15)
.backgroundColor('#F1F3F5');
}, (item: string) => item);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.padding({ left: 10, right: 10 })
.width('100%')
.height('100%')
.scrollBar(BarState.Off);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.tabBar('关注')
.backgroundColor('#ffffff');

  TabContent() {  
    Text('推荐');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .tabBar('推荐')  
  .backgroundColor('#ffffff');  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.backgroundColor('#ffffff');  

}
}
实现效果如下:

点击放大

方案二:自定义标题的吸顶实现。若要实现Tabs页面切换效果,需要自定义页面切换逻辑。
参考上文中方案一,示例代码如下:
@Entry
@Component
export struct CommonSolutionTwo {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Column() {
Row() {
// 吸顶标题
Text('自定义占位标题')
.width('100%')
.height(60)
.textAlign(TextAlign.Center)
.backgroundColor('#66666666');
};

  List({ space: 10 }) {  
    ForEach(this.arr, (item: number) => {  
      ListItem() {  
        Row() {  
          Text('item' + item)  
            .fontSize(16)  
            .height(72)  
            .fontColor(Color.Black);  
        }  
        .width('100%')  
        .height(72)  
        .justifyContent(FlexAlign.Center);  
      }  
      .borderRadius(15)  
      .backgroundColor('#F1F3F5');  
    }, (item: string) => item);  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .padding({ left: 10, right: 10 })  
  .width('100%')  
  .scrollBar(BarState.Off)  
  .layoutWeight(1); // 更换List组件height属性,高度限制以layoutWeight(1)的形式自动填充父组件高度  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.width('100%')  
.height('100%') // 普通吸顶效果时,滚动组件的父组件可以不设置高度,但是滚动嵌套吸顶时一定要设置高度限制为100%  
.backgroundColor('#ffffff');  

}
}
实现效果如下:

点击放大

单标题滚动嵌套吸顶效果
方案一: 通过nestedScroll属性,实现吸顶效果。由于nestedScroll属性是滚动与滑动组件的通用属性,理论上在滚动组件嵌套的过程中,支持该属性的滚动组件都可以实现吸顶效果。以Tabs组件为例,示例代码如下:
@Entry
@Component
struct SingleTitleSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Scroll() {
Column() {
Column() {
Text('自定义占位标题')
.fontSize(30)
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])

    Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {  
      TabContent() {  
        List({ space: 10 }) {  
          ForEach(this.arr, (item: number) => {  
            ListItem() {  
              Row() {  
                Text('item' + item)  
                  .fontSize(16)  
                  .height(72)  
                  .fontColor(Color.Black);  
              }  
              .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
              .width('100%')  
              .height(72)  
              .justifyContent(FlexAlign.Center);  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .borderRadius(15)  
            .backgroundColor('#F1F3F5');  
          }, (item: string) => item);  
        }  
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
        .padding({ left: 10, right: 10 })  
        .width('100%')  
        .height('100%')  
        .scrollBar(BarState.Off)  
        .nestedScroll({  
          scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
          scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
        });  
      }  
      .clip(false)  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .tabBar('关注')  
      .backgroundColor('#ffffff');  

      TabContent() {  
        Text('推荐');  
      }  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .tabBar('推荐')  
      .backgroundColor('#ffffff');  
    }  
    .clip(false)  
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
    .backgroundColor('#ffffff');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .width('100%');  
}  
.clip(false)  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.edgeEffect(EdgeEffect.Spring)  
.friction(0.6)  
.backgroundColor('#ffffff')  
.scrollBar(BarState.Off)  
.width('100%')  
.height('100%');  

}
}
效果预览:

点击放大

说明
自定义标题的实现方式,此处主要讲解滚动组件嵌套实现方式。若是想要以自定义标题实现滚动嵌套吸顶效果,滚动组件的父组件一定要设置高度限制为100%,若不设置高度限制,父组件自适应高度会导致吸顶失效,可详见下文中“多标题滚动嵌套吸顶效果”的方案一,其内第一个标题为自定义标题,及其父组件高度注释。

方案二: 由于Waterflow布局本身不支持吸顶,如需使用Waterflow布局实现吸顶效果,可以在滚动组件上通过Stack叠加一个组件实现,示例代码如下:
// 瀑布流布局项数据模型(用于描述每个子项的布局属性)
export class WaterFlowItemModel {
fullWidth: boolean = false;
stickyTop: number | undefined = undefined;
height: number = 0;
width: number = 0;
bgColor: string = '#f0f0f0';
}

// 瀑布流数据源实现类
export class WaterFlowDataSource implements IDataSource {
private dataArray: WaterFlowItemModel[] = [];
private listeners: DataChangeListener[] = [];

constructor() {
this.dataArray = [];
// 生成10个瀑布流测试数据项
for (let i = 0; i < 10; i++) {
const model = new WaterFlowItemModel();
model.fullWidth = i % 5 === 4;
model.height = 120;
model.bgColor = '#f0f0f0';
this.dataArray.push(model);
}
}

// 获取数据总量
totalCount(): number {
return this.dataArray.length;
}

// 根据索引获取单个数据项
getData(index: number) {
return this.dataArray[index];
}

// 注册数据变更监听器
registerDataChangeListener(listener: DataChangeListener) {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}

// 移除已注册的数据变更监听器
unregisterDataChangeListener(listener: DataChangeListener) {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
}

@Entry
@Component
struct SingleTitleSolutionTwo {
scroller: Scroller = new Scroller();
datasource: WaterFlowDataSource = new WaterFlowDataSource();
@State scrollOffset: number = 0;

build() {
Stack({ alignContent: Alignment.TopStart }) {
WaterFlow({ scroller: this.scroller }) {
LazyForEach(this.datasource, (item: WaterFlowItemModel, index) => {
FlowItem() {
// 当索引不为1,展示当前索引值
if (index !== 1) {
Text(${index});
}
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.margin({ right:20,bottom:12,left:19 })
.borderRadius(15)
.onClick(() => {
console.info('onClick ---- ', index);
})
.width('90%')
.height(item.height)
// .backgroundColor(index === 1 ? '#00000000' : Color.White);
.backgroundColor('#F1F3F5')
}, (key: string) => key);
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.edgeEffect(EdgeEffect.Spring)
.onWillScroll((offset: number) => {
this.scrollOffset = this.scroller.currentOffset().yOffset + offset;
})
.width('100%')
.height('100%')
.backgroundColor('#ffffff');

  // 在索引为1的WaterFlowItem上方叠加一个组件  
  Stack() {  
    Text('自定义占位标题')  
      .width('100%')  
      .height(this.datasource.getData(1).height)  
      .fontColor(Color.White)  
      .backgroundColor('#66666666')  
      .fontSize(17)  
      .textAlign(TextAlign.Center);  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .hitTestBehavior(HitTestMode.Transparent)  
  .position({ x: 0, y: this.scrollOffset >= 120 ? 0 : 120 - this.scrollOffset });  

}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.width('100%')  
.height('100%')  
.clip(true);  

}
}
实现效果如下:

点击放大

多标题滚动嵌套吸顶效果
方案一:通过nestedScroll属性对滚动组件多次滚动嵌套,参考单标题滚动嵌套吸顶效果,示例代码如下:
@Entry
@Component
struct MultipleTitleSolutionOne {
subsController: TabsController = new TabsController();
@State arr: string[] = ['1', '2', '3'];
@State arr1: string[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15'];

build() {
Scroll() {
Column() {
Column() {
Text('自定义占位标题')
.fontSize(30)
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
};

    Column() {  
      Row() {  
        Text('自定义占位标题') // 自定义实现标题吸顶,依旧采用Tabs组件也适用  
          .fontSize(16)  
          .height(60)  
          .width('100%')  
          .textAlign(TextAlign.Center);  
      }.backgroundColor('#6dbab8b8')  

      Scroll() {  
        Column() {  
          ForEach(this.arr, (item: number) => {  
            Row() {  
              Text('item' + item)  
                .fontSize(16)  
                .height(72)  
                .fontColor(Color.Black);  
            }  
            .width('100%')  
            .height(72)  
            .borderRadius(15)  
            .margin({  
              bottom: 10  
            })  
            .justifyContent(FlexAlign.Center);  
          }, (item: string) => item);  

          Tabs({ barPosition: BarPosition.Start, controller: this.subsController }) {  
            TabContent() {  
              List({ space: 10 }) {  
                ForEach(this.arr1, (item: number) => {  
                  ListItem() {  
                    Row() {  
                      Text('item' + item)  
                        .fontSize(16)  
                        .height(72)  
                        .fontColor(Color.Black);  
                    }  
                    .width('100%')  
                    .height(72)  
                    .justifyContent(FlexAlign.Center);  
                  }  
                  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
                  .borderRadius(15)  
                  .backgroundColor(Color.White);  
                }, (item: string) => item);  
              }  
              .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
              .padding({ left: 10, right: 10 })  
              .width('100%')  
              .height('100%')  
              .scrollBar(BarState.Off)  
              .nestedScroll({  
                scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
                scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
              });  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .tabBar('关注')  
            .backgroundColor('#F1F3F5');  

            TabContent() {  
              Text('推荐');  
            }  
            .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
            .tabBar('推荐')  
            .backgroundColor('#F1F3F5');  
          }  
          .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
          .backgroundColor('#6dbab8b8');  
        }  
        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
        .backgroundColor(Color.White)  
        .width('100%');  
      }  
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
      .width('100%')  
      .layoutWeight(1)  
      .scrollBar(BarState.Off)  
      .nestedScroll({  
        scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动PARENT_FIRST:父组件先滚动,父组件滚动到边缘以后自身滚动。  
        scrollBackward: NestedScrollMode.SELF_FIRST // 向下滚动SELF_FIRST:自身先滚动,自身滚动到边缘以后父组件滚动。  
      });  
    }  
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
    .height('100%') // 由于是自定义的标题,所以容纳标题和列表的父组件一定要设置高度为100%  
    .backgroundColor('#6dbab8b8');  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .width('100%');  
}  
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
.edgeEffect(EdgeEffect.Spring)  
.friction(0.6)  
.backgroundColor(Color.White)  
.scrollBar(BarState.Off)  
.width('100%')  
.height('100%');  

}
}
实现效果如下:

点击放大

方案二:通过List组件的sticky属性,实现吸顶效果。
常见场景一:header吸顶和footer吸底,该方案仅适用于List组件,具体实施方案参考官方示例:吸顶/吸底。

常见场景二:多标题多tab菜单吸顶时,实现滑动时自动切换tab栏。通过onDidScroll方法,监听滚动事件,根据滚动偏移量动态计算当前处于哪个区域,再根据当前区块切换标题内容。示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionOnePlus {
@State data1: string[] = ['标题1', '标题2', '标题3', '标题4'];
@State data2: string[] = ['1', '2', '3', '4'];
@State selectIndex1: number = 0;
private listController: ListScroller = new ListScroller();

@Builder
topBuilder1() {
Row() {
ForEach(this.data1, (item: string, index: number) => {
Text(item)
.fontColor(index == this.selectIndex1 ? Color.Blue : '#000')
.fontSize(index == this.selectIndex1 ? 20 : 16)
.fontWeight(index == this.selectIndex1 ? 500 : 400)
.onClick(() => {
this.listController.scrollToItemInGroup(0, index, true);
});
});
}
.backgroundColor(0xAABBCC)
.height(60)
.width('100%')
.justifyContent(FlexAlign.SpaceAround);
}

@Builder
topBuilder2() {
Row() {
Text('第二组吸顶')
.padding(5)
.textAlign(TextAlign.Center);
}
.backgroundColor(0xAABBCC)
.justifyContent(FlexAlign.SpaceAround)
.height(60)
.width('100%');
}

build() {
Column() {
List({ space: 10, scroller: this.listController }) {
ListItemGroup({ header: this.topBuilder1, space: 10 }) {
ForEach(this.data1, (item: string) => {
ListItem() {
Text(item)
.width('90%')
.height(200)
.backgroundColor('#f1f3f5')
.borderRadius(10)
.textAlign(TextAlign.Center);
};
});
};

    ListItemGroup() {  
      ListItem() {  
        Text('其他内容')  
          .width('90%')  
          .height(400)  
          .backgroundColor('#f1f3f5')  
          .borderRadius(10)  
          .fontSize(16)  
          .textAlign(TextAlign.Center);  
      };  
    };  

    ListItemGroup({ header: this.topBuilder2, space: 10 }) {  
      ForEach(this.data2, (item: string) => {  
        ListItem() {  
          Text(item)  
            .width('90%')  
            .height(200)  
            .backgroundColor('#f1f3f5')  
            .borderRadius(10)  
            .textAlign(TextAlign.Center);  
        };  
      });  
    };  
  }  
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])  
  .onDidScroll(() => {  
    let currentOffset = this.listController.currentOffset().yOffset;  
    this.selectIndex1 = Math.floor(currentOffset / 200);  
  })  
  .scrollBar(BarState.Off)  
  .sticky(StickyStyle.Header)  
  .alignListItem(ListItemAlign.Center);  
};  

}
}
实现效果如下:

点击放大

二级标题滚动嵌套吸顶效果
第一层吸顶用Scroll嵌套List实现,第二层是利用ListItemGroup的sticky属性实现,示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionTwo {
private timeTable: TimeTable[] = [
{
title: 'Header2-0',
projects: ['内容1']
},
{
title: 'Header2-1',
projects: ['内容2']
},
{
title: 'Header2-2',
projects: ['内容3']
}
];

@Builder
itemHead(text: string, index: number) {
if (index === 0) {
Text().height(0);
} else {
Text(text)
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.height(index === 1 ? 100 : 200)
.backgroundColor('#d1d1d6');
}
}

build() {
Scroll() {
Column() {
Text('自定义占位标题')
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
Column() {
// 第一层吸顶效果主要是通过设置nestedScroll属性以及父组件设置高度100%实现
Text('Header1')
.width('100%')
.height(100)
.backgroundColor('#4d666666')
.textAlign(TextAlign.Center)
.fontColor(Color.Black);
List() {
ForEach(this.timeTable, (item: TimeTable, index) => {
ListItemGroup({ header: this.itemHead(item.title, index) }) {
ForEach(item.projects, (project: string) => {
ListItem() {
Text(project)
.width('100%')
.height(800)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor('#FFFFFF');
}
}, (item: string) => item);
}.clip(false);
});
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.sticky(StickyStyle.Header)
.scrollBar(BarState.Off)
.width('100%')
.height('calc(100% - 100vp)')
.backgroundColor('#ffffff')
.edgeEffect(EdgeEffect.Spring)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
});
}.clip(false).expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]).height('100%')
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.clip(false)
.edgeEffect(EdgeEffect.Spring)
.friction(0.6)
.backgroundColor(Color.White)
.scrollBar(BarState.Off)
.width('100%')
.height('100%');
}
}

interface TimeTable {
title: string;
projects: string[];
}
实现效果如下:

点击放大

动态控制二级标题滚动嵌套吸顶效果:
在List组件onDidScroll方法里判断索引为2的组件滚动偏移量,动态调整Header1的position属性和List组件的contentStartOffset属性,示例代码如下:

@Entry
@Component
struct MultipleTitleSolutionTwoDynamic {
@State header1Position: number = 0;
@State listStartOffset: number = 100;
private scroller = new Scroller();
private timeTable: TimeTableDynamic[] = [
{
title: 'Header2-0',
projects: ['内容1']
},
{
title: 'Header2-1',
projects: ['内容2']
},
{
title: 'Header2-2',
projects: ['内容3']
}
];

@Builder
itemHead(text: string, index: number) {
if (index === 0) {
Text().height(0);
} else {
Text(text)
.width('100%')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.height(index === 1 ? 100 : 200)
.backgroundColor('#d1d1d6');
}
}

build() {
Scroll() {
Column() {
Text('二级标题滚动嵌套吸顶')
.width('100%')
.height(200)
.backgroundColor('#66666666')
.textAlign(TextAlign.Center);
Column() {
// 第一层吸顶效果主要是通过设置nestedScroll属性以及父组件设置高度100%实现
Text('Header1')
.width('100%')
.height(100)
.backgroundColor('#4d666666')
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.position({ y: this.header1Position })
.zIndex(10);
List({ scroller: this.scroller }) {
ForEach(this.timeTable, (item: TimeTableDynamic, index) => {
ListItemGroup({ header: this.itemHead(item.title, index) }) {
ForEach(item.projects, (project: string) => {
ListItem() {
Text(project)
.width('100%')
.height(800)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor('#ffffff');
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]);
}, (item: string) => item);
};
});
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.sticky(StickyStyle.Header)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
.backgroundColor('#ffffff')
.edgeEffect(EdgeEffect.Spring)
.contentStartOffset(this.listStartOffset)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
.onDidScroll(() => {
let secondRect = this.scroller.getItemRect(2);
if (secondRect.x === 0 && secondRect.y === 0 && secondRect.width === 0 && secondRect.height === 0) {
console.info(索引为${2}的组件不在屏幕上);
return;
}
this.header1Position = Math.min(0, secondRect.y - 200);
this.listStartOffset = Math.max(0, 100 + this.header1Position);
});
}.height('100%');
};
}.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.clip(false)
.edgeEffect(EdgeEffect.Spring)
.friction(0.6)
.backgroundColor(Color.White)
.scrollBar(BarState.Off)
.width('100%')
.height('100%');
}
}

interface TimeTableDynamic {
title: string;
projects: string[];
}
https://coub.com/view/gylbvhqrkz
https://coub.com/view/gqrdnsqbsm
https://coub.com/view/0a3thuu2b4
https://coub.com/view/lg6lw08ivp
https://coub.com/view/qzxlw9ogx1
https://coub.com/view/68z9behrh7
https://coub.com/view/4iijkgxtaa
https://coub.com/view/xuelqryeob
https://coub.com/view/037iwxjpf9
https://coub.com/view/ydu5rfse9r
https://coub.com/view/g71sfv35n0
https://coub.com/view/hmkl7icf96
https://coub.com/view/7gf8jpjeqf
https://coub.com/view/6ar7vzg3jt
https://coub.com/view/x6brb1zhst
https://coub.com/view/esc2tpa50i
https://coub.com/view/ij4i54b4su
https://coub.com/view/6csj522lvg
https://coub.com/view/oaspyursw3
https://coub.com/view/gvem0zleaj
https://coub.com/view/xs8g570e03
https://coub.com/view/suwbkm015s
https://coub.com/view/74yubqnb80
https://coub.com/view/6wh2qfuaws
https://coub.com/view/2yv1gta5d2
https://coub.com/view/hopm4eqo5c
https://coub.com/view/q1tm3sdodd
https://coub.com/view/7udyb7fyhi
https://coub.com/view/1z3anr32zq
https://coub.com/view/jhh5mhj3y0
https://coub.com/view/dvq9c4eyfc
https://coub.com/view/wv83dp9qeo
https://coub.com/view/a9i5tkfl9w
https://coub.com/view/v372pqbiis
https://coub.com/view/ig9545no10
https://coub.com/view/w79ltp6ou9
https://coub.com/view/u9s0qk1akf
https://coub.com/view/damsu4njzt
https://coub.com/view/v2vriht1bd
https://coub.com/view/e93br3m9qa
https://coub.com/view/xd56t399ne
https://coub.com/view/dnvfpx6l5b
https://coub.com/view/pehxieghx6
https://coub.com/view/6w14zzs972
https://coub.com/view/46b628kwgi
https://coub.com/view/o5bg5eawhx
https://coub.com/view/rrinp7vw3c
https://coub.com/view/mamo602b5a
https://coub.com/view/hlvm2nw499
https://coub.com/view/jzu48fqzw5
https://coub.com/view/ncc5pejn2y
https://coub.com/view/o61yarijz2
https://coub.com/view/xt572wfwwl
https://coub.com/view/t4z6lnd7n8
https://coub.com/view/cxz5p1rtil
https://coub.com/view/a630gf5urg
https://coub.com/view/7khrln5czp
https://coub.com/view/u52ikkf9j8
https://coub.com/view/6ispqsd85w
https://coub.com/view/g5ndpkf32e
https://coub.com/view/nf11v6hl00
https://coub.com/view/g2va9aj5ue
https://coub.com/view/qe1rsfh7ey
https://coub.com/view/ldvx0v2kr2
https://coub.com/view/w3bct13r9i
https://coub.com/view/3zyw0cjxo1
https://coub.com/view/ny9foponj5
https://coub.com/view/g4q545h3da
https://coub.com/view/kekg8y04v7
https://coub.com/view/ms20cjnzki
https://coub.com/view/6hg6xx4f3u
https://coub.com/view/4gd6mjpi41
https://coub.com/view/91selywfey
https://coub.com/view/d9lltiovbh
https://coub.com/view/j6dhi1cqlu
https://coub.com/view/pupns1qvyy
https://coub.com/view/42eqvna2sj
https://coub.com/view/213yl91c0r
https://coub.com/view/ligxe8niat
https://sites.google.com/view/4npswg73/home
https://sites.google.com/view/1cqggg42/home
https://sites.google.com/view/7hwthv95/home
https://sites.google.com/view/8ftpag65/home
https://sites.google.com/view/9hupnv16/home
https://sites.google.com/view/9rcwcc44/home
https://sites.google.com/view/4ygxfw66/home
https://sites.google.com/view/7yjaoo71/home
https://sites.google.com/view/6euxnz02/home
https://sites.google.com/view/2xrcwv82/home
https://sites.google.com/view/3bruzf22/home
https://sites.google.com/view/1fampb85/home
https://sites.google.com/view/0uqjgm22/home
https://sites.google.com/view/8lytyx96/home
https://sites.google.com/view/2rcvcx10/home
https://sites.google.com/view/0wxard26/home
https://sites.google.com/view/0rhcrm43/home
https://sites.google.com/view/1cilam64/home
https://sites.google.com/view/2dsnqw73/home
https://sites.google.com/view/0vgsrq46/home
https://sites.google.com/view/8nqpzw07/home
https://sites.google.com/view/2zlysr71/home
https://sites.google.com/view/6krwym27/home
https://sites.google.com/view/6xuung21/home
https://sites.google.com/view/5irzjv73/home
https://sites.google.com/view/7lukby92/home
https://sites.google.com/view/3oyfhr95/home
https://sites.google.com/view/2srcwe43/home
https://sites.google.com/view/0setkv89/home
https://sites.google.com/view/5rvxgj88/home
https://sites.google.com/view/7tfahr42/home
https://sites.google.com/view/7yboep62/home
https://sites.google.com/view/4upmsh64/home
https://sites.google.com/view/0zwrrv97/home
https://sites.google.com/view/2gawzu13/home
https://sites.google.com/view/6qeyzl51/home
https://sites.google.com/view/3ievss43/home
https://sites.google.com/view/0ibpqm02/home
https://sites.google.com/view/3awrap71/home
https://sites.google.com/view/0tvqkg18/home
https://sites.google.com/view/5sbyjy90/home
https://sites.google.com/view/6ehwco13/home
https://sites.google.com/view/7tjzdg23/home
https://sites.google.com/view/6rakcq50/home
https://sites.google.com/view/7hexol86/home
https://sites.google.com/view/0cyonp67/home
https://sites.google.com/view/7cpmvf18/home
https://sites.google.com/view/7qroxh49/home
https://sites.google.com/view/2qshnp57/home
https://sites.google.com/view/6dwyim71/home
https://sites.google.com/view/4cegdc43/home
https://sites.google.com/view/9kbwox44/home
https://sites.google.com/view/6sciwd21/home
https://sites.google.com/view/6bjrsb10/home
https://sites.google.com/view/3qxmma28/home
https://sites.google.com/view/7cbftv08/home
https://sites.google.com/view/4kkccj88/home
https://sites.google.com/view/5pjonl99/home
https://sites.google.com/view/4aeacr58/home
https://sites.google.com/view/6fiuxa97/home
https://sites.google.com/view/5brhgy37/home
https://sites.google.com/view/1mzbfo35/home
https://sites.google.com/view/9kbckv34/home
https://sites.google.com/view/9cobir53/home
https://sites.google.com/view/6qgbcm33/home
https://sites.google.com/view/8lgcnx33/home
https://sites.google.com/view/9jzmzc49/home
https://sites.google.com/view/0bujrj34/home
https://sites.google.com/view/1heuxf15/home
https://sites.google.com/view/0przrr24/home
https://sites.google.com/view/8gmodp38/home
https://sites.google.com/view/1qgplx06/home
https://sites.google.com/view/1vbfom31/home
https://sites.google.com/view/4fgfqr84/home
https://sites.google.com/view/6oybbx22/home
https://sites.google.com/view/1knpbg15/home
https://sites.google.com/view/0opgke85/home
https://sites.google.com/view/7kirma83/home
https://sites.google.com/view/1zzbdj74/home
https://sites.google.com/view/8csmsa36/home
https://sites.google.com/view/1edswe69/home
https://sites.google.com/view/8kyxaz85/home
https://sites.google.com/view/8bplrw32/home
https://sites.google.com/view/7pdmfv03/home
https://sites.google.com/view/8qkoih30/home
https://sites.google.com/view/4noxpv38/home
https://sites.google.com/view/4itlbr61/home
https://sites.google.com/view/5rjefu09/home
https://sites.google.com/view/5ujnxe58/home
https://sites.google.com/view/3gqlqd93/home
https://sites.google.com/view/4ijybb89/home
https://sites.google.com/view/6ipevr32/home
https://sites.google.com/view/5mhxux08/home
https://sites.google.com/view/8wojcb88/home
https://sites.google.com/view/0riyrm78/home

收起阅读 »

如何解决LocalStorage存储function报错问题

问题现象
使用LocalStorage传递function报错:

Error message:@Component 'owning @Component UNKNOWN': Illegal variable value error with decorated variable undefined 'clickSend': failed validation: 'undefined, null, number, boolean, string, or Object but not function, not V2 @ObservedV2 / @Trace class, and makeObserved return value either, attempt to assign value type: 'function', value: 'undefined'!
关键问题参考如下:

function openCommentsInput(title: string, hintMsg: string, clickSend: Function = (content: string) => {}) {
let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('title', title)
storage.setOrCreate('hintMsg', hintMsg)
storage.setOrCreate('clickSend', clickSend) // 程序崩溃,报错
}
背景知识
LocalStorage是页面级的UI状态存储,存储的类型有所限制。

问题定位
查看以下报错日志的关键信息可知,clickSend是非法变量值,支持undefined、null、number、boolean、string、Object等类型,但不支持function函数类型变量。

Illegal variable value error with decorated variable undefined 'clickSend': failed validation: 'undefined, null, number, boolean, string, or Object but not function ...'
分析结论
LocalStorage不支持存储function函数类型变量。

修改建议
使用类class将函数function进行封装为Object对象,LocalStorage支持存储Object对象。

let storage: LocalStorage = new LocalStorage();

function openCommentsInput(title: string, hintMsg: string, clickSend: object) {
storage.setOrCreate('title', title);
storage.setOrCreate('hintMsg', hintMsg);
storage.setOrCreate('clickSend', clickSend);
}

// 使用class对function进行一层封装
class MyFunc {
clickSend: Function = () => {
};
}

// 需要保存至LocalStorage的函数
function clickSend(ctx: string) {
console.info(ctx);
}

@Entry(storage)
@Component
export struct LocalStorageDemo {
@LocalStorageLink('clickSend') myFunc: object = []; // 获取LocalStorage中存储的clickSend对象

build() {
Column({ space: 20 }) {
Button('向storage保存数据')
.onClick(() => {
let myFunc: MyFunc = new MyFunc();
myFunc.clickSend = clickSend; // 将要保存的函数封装在class对象中
openCommentsInput('title', 'hintMsg', myFunc); // 将class对象保存至LocalStorage
});
Button('读取storage的数据')
.onClick(() => {
let tmp = this.myFunc as MyFunc; // 将Object对象转为MyFunc类
tmp.clickSend('读取storage的数据成功'); // 调用MyFunc中的clickSend函数
});
};
}
}
运行截图如下:成功调用LocalStorage中存储的类对象的函数来打印数据。

https://coub.com/view/qk692jnkyy
https://coub.com/view/nqy2r5u04j
https://coub.com/view/rxk7ib6w4i
https://coub.com/view/lfok6c5r0k
https://coub.com/view/cps6sv1832
https://coub.com/view/n4rgmqcpj6
https://coub.com/view/8naf4x8mtj
https://coub.com/view/90hd34u6j5
https://coub.com/view/xxmp511mw1
https://coub.com/view/ga20sx80nf
https://coub.com/view/496cje9x00
https://coub.com/view/e16120c698
https://coub.com/view/qpk44ggqgv
https://coub.com/view/9h64hx23cz
https://coub.com/view/fw89xni3km
https://coub.com/view/fmdlgaxtdh
https://coub.com/view/wbfdz7bao0
https://coub.com/view/jbvjml6xnb
https://coub.com/view/v2gvgqu1pn
https://coub.com/view/lukv9msonl
https://coub.com/view/wy1u881k37
https://coub.com/view/i35bvv7bjj
https://coub.com/view/79bfeowo9y
https://coub.com/view/vfn4r5gd5r
https://coub.com/view/faob3nkydm
https://coub.com/view/t20e00f911
https://coub.com/view/l0n0lv31ss
https://coub.com/view/y1gdff8iup
https://coub.com/view/325iw05lzf
https://coub.com/view/zysxnw2pyg
https://coub.com/view/ip600tbh2t
https://coub.com/view/ik4qxfnchj
https://coub.com/view/x9byx4imcd
https://coub.com/view/j7wpgvy2r9
https://coub.com/view/z9j3mk72kk
https://coub.com/view/u5jraprsbp
https://coub.com/view/18icai1qy2
https://coub.com/view/91t8lsg4vr
https://coub.com/view/zcre25zh9n
https://coub.com/view/4b4f4fhr8u
https://coub.com/view/75reunqnh5
https://coub.com/view/bb1obvuqhi
https://coub.com/view/xxwmrq1it0
https://coub.com/view/zpxfp25pwf
https://coub.com/view/w9qwirmzj7
https://coub.com/view/3he63xj1fl
https://coub.com/view/ij87impqwc
https://coub.com/view/9sllwd1dt7
https://coub.com/view/sap509vv8e
https://coub.com/view/9micgamocw
https://coub.com/view/urk86qp1iu
https://coub.com/view/4tc2me72v9
https://coub.com/view/kq6hr3az82
https://coub.com/view/e7v9yib78m
https://coub.com/view/3me1n3nsrc
https://coub.com/view/ajjah56ium
https://coub.com/view/5fmp56nirq
https://coub.com/view/ouzl95vmsy
https://coub.com/view/5joic8mnfe
https://coub.com/view/wa98vp5r4o
https://coub.com/view/mo88n15ddc
https://coub.com/view/ahpssb3udw
https://coub.com/view/bttevp21v6
https://coub.com/view/wb7er1vpik
https://coub.com/view/u95v6au0cg
https://coub.com/view/w7z0nh2aeb
https://coub.com/view/s2sv3xp1rv
https://coub.com/view/8uw0lqh5t9
https://coub.com/view/3779k4n10i
https://coub.com/view/ks34oh2lf1
https://coub.com/view/p9ypedd2wg
https://coub.com/view/jwiqh1gt58
https://coub.com/view/3mj4f1uew0
https://coub.com/view/zu17pmttav
https://coub.com/view/r2r56ewp66
https://coub.com/view/5ptqyepsog
https://coub.com/view/nfbde2nqvw
https://coub.com/view/8eatx8ni77
https://coub.com/view/4angchwxcu
https://sites.google.com/view/0ngmeh79/home
https://sites.google.com/view/3yrngq53/home
https://sites.google.com/view/3hzydd88/home
https://sites.google.com/view/1ctnmt31/home
https://sites.google.com/view/6sxmmf82/home
https://sites.google.com/view/9pmhfl95/home
https://sites.google.com/view/4jfltz88/home
https://sites.google.com/view/5tysvi63/home
https://sites.google.com/view/0zjxdz60/home
https://sites.google.com/view/0bpoqj02/home
https://sites.google.com/view/8vdldp71/home
https://sites.google.com/view/3uwslk83/home
https://sites.google.com/view/5ltzul21/home
https://sites.google.com/view/8bcvze60/home
https://sites.google.com/view/8qgykm38/home
https://sites.google.com/view/6ajgve57/home
https://sites.google.com/view/5ilxvx17/home
https://sites.google.com/view/4qvlfm93/home
https://sites.google.com/view/4mezwk86/home
https://sites.google.com/view/0atgpp88/home
https://sites.google.com/view/6zfnle48/home
https://sites.google.com/view/6wkkab73/home
https://sites.google.com/view/1uteqg45/home
https://sites.google.com/view/0glrrx12/home
https://sites.google.com/view/4zsavi57/home
https://sites.google.com/view/9oovga45/home
https://sites.google.com/view/3hzgfr61/home
https://sites.google.com/view/9vkbkl41/home
https://sites.google.com/view/6wndzs57/home
https://sites.google.com/view/6uwefj26/home
https://sites.google.com/view/0oipjv09/home
https://sites.google.com/view/4pngoo73/home
https://sites.google.com/view/6fycbf68/home
https://sites.google.com/view/5quyop60/home
https://sites.google.com/view/7oupoi39/home
https://sites.google.com/view/8otdza09/home
https://sites.google.com/view/0sqefg16/home
https://sites.google.com/view/4zrmtg15/home
https://sites.google.com/view/3cgjls08/home
https://sites.google.com/view/6oldtl78/home
https://sites.google.com/view/4ykfdd04/home
https://sites.google.com/view/9wpguc90/home
https://sites.google.com/view/6idlof66/home
https://sites.google.com/view/8omphs74/home
https://sites.google.com/view/6xofvc19/home
https://sites.google.com/view/1difbo72/home
https://sites.google.com/view/4knmhu66/home
https://sites.google.com/view/1ssqkm73/home
https://sites.google.com/view/8zavly68/home
https://sites.google.com/view/9zwrzj64/home
https://sites.google.com/view/7wqfid80/home
https://sites.google.com/view/0mzlvh16/home
https://sites.google.com/view/1hozwq52/home
https://sites.google.com/view/3eikgg48/home
https://sites.google.com/view/3skunx74/home
https://sites.google.com/view/7znrmu32/home
https://sites.google.com/view/9xniil98/home
https://sites.google.com/view/1gmoid98/home
https://sites.google.com/view/6gxmmk76/home
https://sites.google.com/view/6xdnsn40/home
https://sites.google.com/view/6qbvjn86/home
https://sites.google.com/view/9veorf12/home
https://sites.google.com/view/4udhos46/home
https://sites.google.com/view/2ynqpm49/home
https://sites.google.com/view/1pauyq10/home
https://sites.google.com/view/4mmhbb67/home
https://sites.google.com/view/5dimfw85/home
https://sites.google.com/view/7qrywi45/home
https://sites.google.com/view/7xmufr77/home
https://sites.google.com/view/0tzmwq99/home
https://sites.google.com/view/2elngz59/home
https://sites.google.com/view/1mopok42/home
https://sites.google.com/view/5galcb39/home
https://sites.google.com/view/3dplbi09/home
https://sites.google.com/view/4hxzuw21/home
https://sites.google.com/view/3ndtxe40/home
https://sites.google.com/view/1wgxgh74/home
https://sites.google.com/view/0ujdvq38/home
https://sites.google.com/view/3qtkgv13/home
https://sites.google.com/view/5jqmpf47/home
https://sites.google.com/view/6ovyem57/home
https://sites.google.com/view/3jpimw15/home
https://sites.google.com/view/1xlfel73/home
https://sites.google.com/view/8jqklu53/home
https://sites.google.com/view/2vrqgs17/home
https://sites.google.com/view/2cmkar10/home
https://sites.google.com/view/5sktrz87/home
https://sites.google.com/view/9qkbxe89/home
https://sites.google.com/view/2zlonk24/home
https://sites.google.com/view/6jnevv87/home
https://sites.google.com/view/1wrgrh89/home
https://sites.google.com/view/5yjmql75/home
https://sites.google.com/view/1eqrct60/home
https://sites.google.com/view/5bgxgx08/home
https://sites.google.com/view/5ikctu17/home
https://sites.google.com/view/8hqypq15/home
https://sites.google.com/view/9nhoum17/home
https://sites.google.com/view/0pvuts20/home
https://sites.google.com/view/9gqmnb23/home
https://sites.google.com/view/6ifnwy59/home
https://sites.google.com/view/2dacaw12/home
https://sites.google.com/view/9vhqej39/home
https://sites.google.com/view/5tmvjt88/home
https://sites.google.com/view/3mnghf05/home
https://sites.google.com/view/6ttioq69/home

继续阅读 »

问题现象
使用LocalStorage传递function报错:

Error message:@Component 'owning @Component UNKNOWN': Illegal variable value error with decorated variable undefined 'clickSend': failed validation: 'undefined, null, number, boolean, string, or Object but not function, not V2 @ObservedV2 / @Trace class, and makeObserved return value either, attempt to assign value type: 'function', value: 'undefined'!
关键问题参考如下:

function openCommentsInput(title: string, hintMsg: string, clickSend: Function = (content: string) => {}) {
let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('title', title)
storage.setOrCreate('hintMsg', hintMsg)
storage.setOrCreate('clickSend', clickSend) // 程序崩溃,报错
}
背景知识
LocalStorage是页面级的UI状态存储,存储的类型有所限制。

问题定位
查看以下报错日志的关键信息可知,clickSend是非法变量值,支持undefined、null、number、boolean、string、Object等类型,但不支持function函数类型变量。

Illegal variable value error with decorated variable undefined 'clickSend': failed validation: 'undefined, null, number, boolean, string, or Object but not function ...'
分析结论
LocalStorage不支持存储function函数类型变量。

修改建议
使用类class将函数function进行封装为Object对象,LocalStorage支持存储Object对象。

let storage: LocalStorage = new LocalStorage();

function openCommentsInput(title: string, hintMsg: string, clickSend: object) {
storage.setOrCreate('title', title);
storage.setOrCreate('hintMsg', hintMsg);
storage.setOrCreate('clickSend', clickSend);
}

// 使用class对function进行一层封装
class MyFunc {
clickSend: Function = () => {
};
}

// 需要保存至LocalStorage的函数
function clickSend(ctx: string) {
console.info(ctx);
}

@Entry(storage)
@Component
export struct LocalStorageDemo {
@LocalStorageLink('clickSend') myFunc: object = []; // 获取LocalStorage中存储的clickSend对象

build() {
Column({ space: 20 }) {
Button('向storage保存数据')
.onClick(() => {
let myFunc: MyFunc = new MyFunc();
myFunc.clickSend = clickSend; // 将要保存的函数封装在class对象中
openCommentsInput('title', 'hintMsg', myFunc); // 将class对象保存至LocalStorage
});
Button('读取storage的数据')
.onClick(() => {
let tmp = this.myFunc as MyFunc; // 将Object对象转为MyFunc类
tmp.clickSend('读取storage的数据成功'); // 调用MyFunc中的clickSend函数
});
};
}
}
运行截图如下:成功调用LocalStorage中存储的类对象的函数来打印数据。

https://coub.com/view/qk692jnkyy
https://coub.com/view/nqy2r5u04j
https://coub.com/view/rxk7ib6w4i
https://coub.com/view/lfok6c5r0k
https://coub.com/view/cps6sv1832
https://coub.com/view/n4rgmqcpj6
https://coub.com/view/8naf4x8mtj
https://coub.com/view/90hd34u6j5
https://coub.com/view/xxmp511mw1
https://coub.com/view/ga20sx80nf
https://coub.com/view/496cje9x00
https://coub.com/view/e16120c698
https://coub.com/view/qpk44ggqgv
https://coub.com/view/9h64hx23cz
https://coub.com/view/fw89xni3km
https://coub.com/view/fmdlgaxtdh
https://coub.com/view/wbfdz7bao0
https://coub.com/view/jbvjml6xnb
https://coub.com/view/v2gvgqu1pn
https://coub.com/view/lukv9msonl
https://coub.com/view/wy1u881k37
https://coub.com/view/i35bvv7bjj
https://coub.com/view/79bfeowo9y
https://coub.com/view/vfn4r5gd5r
https://coub.com/view/faob3nkydm
https://coub.com/view/t20e00f911
https://coub.com/view/l0n0lv31ss
https://coub.com/view/y1gdff8iup
https://coub.com/view/325iw05lzf
https://coub.com/view/zysxnw2pyg
https://coub.com/view/ip600tbh2t
https://coub.com/view/ik4qxfnchj
https://coub.com/view/x9byx4imcd
https://coub.com/view/j7wpgvy2r9
https://coub.com/view/z9j3mk72kk
https://coub.com/view/u5jraprsbp
https://coub.com/view/18icai1qy2
https://coub.com/view/91t8lsg4vr
https://coub.com/view/zcre25zh9n
https://coub.com/view/4b4f4fhr8u
https://coub.com/view/75reunqnh5
https://coub.com/view/bb1obvuqhi
https://coub.com/view/xxwmrq1it0
https://coub.com/view/zpxfp25pwf
https://coub.com/view/w9qwirmzj7
https://coub.com/view/3he63xj1fl
https://coub.com/view/ij87impqwc
https://coub.com/view/9sllwd1dt7
https://coub.com/view/sap509vv8e
https://coub.com/view/9micgamocw
https://coub.com/view/urk86qp1iu
https://coub.com/view/4tc2me72v9
https://coub.com/view/kq6hr3az82
https://coub.com/view/e7v9yib78m
https://coub.com/view/3me1n3nsrc
https://coub.com/view/ajjah56ium
https://coub.com/view/5fmp56nirq
https://coub.com/view/ouzl95vmsy
https://coub.com/view/5joic8mnfe
https://coub.com/view/wa98vp5r4o
https://coub.com/view/mo88n15ddc
https://coub.com/view/ahpssb3udw
https://coub.com/view/bttevp21v6
https://coub.com/view/wb7er1vpik
https://coub.com/view/u95v6au0cg
https://coub.com/view/w7z0nh2aeb
https://coub.com/view/s2sv3xp1rv
https://coub.com/view/8uw0lqh5t9
https://coub.com/view/3779k4n10i
https://coub.com/view/ks34oh2lf1
https://coub.com/view/p9ypedd2wg
https://coub.com/view/jwiqh1gt58
https://coub.com/view/3mj4f1uew0
https://coub.com/view/zu17pmttav
https://coub.com/view/r2r56ewp66
https://coub.com/view/5ptqyepsog
https://coub.com/view/nfbde2nqvw
https://coub.com/view/8eatx8ni77
https://coub.com/view/4angchwxcu
https://sites.google.com/view/0ngmeh79/home
https://sites.google.com/view/3yrngq53/home
https://sites.google.com/view/3hzydd88/home
https://sites.google.com/view/1ctnmt31/home
https://sites.google.com/view/6sxmmf82/home
https://sites.google.com/view/9pmhfl95/home
https://sites.google.com/view/4jfltz88/home
https://sites.google.com/view/5tysvi63/home
https://sites.google.com/view/0zjxdz60/home
https://sites.google.com/view/0bpoqj02/home
https://sites.google.com/view/8vdldp71/home
https://sites.google.com/view/3uwslk83/home
https://sites.google.com/view/5ltzul21/home
https://sites.google.com/view/8bcvze60/home
https://sites.google.com/view/8qgykm38/home
https://sites.google.com/view/6ajgve57/home
https://sites.google.com/view/5ilxvx17/home
https://sites.google.com/view/4qvlfm93/home
https://sites.google.com/view/4mezwk86/home
https://sites.google.com/view/0atgpp88/home
https://sites.google.com/view/6zfnle48/home
https://sites.google.com/view/6wkkab73/home
https://sites.google.com/view/1uteqg45/home
https://sites.google.com/view/0glrrx12/home
https://sites.google.com/view/4zsavi57/home
https://sites.google.com/view/9oovga45/home
https://sites.google.com/view/3hzgfr61/home
https://sites.google.com/view/9vkbkl41/home
https://sites.google.com/view/6wndzs57/home
https://sites.google.com/view/6uwefj26/home
https://sites.google.com/view/0oipjv09/home
https://sites.google.com/view/4pngoo73/home
https://sites.google.com/view/6fycbf68/home
https://sites.google.com/view/5quyop60/home
https://sites.google.com/view/7oupoi39/home
https://sites.google.com/view/8otdza09/home
https://sites.google.com/view/0sqefg16/home
https://sites.google.com/view/4zrmtg15/home
https://sites.google.com/view/3cgjls08/home
https://sites.google.com/view/6oldtl78/home
https://sites.google.com/view/4ykfdd04/home
https://sites.google.com/view/9wpguc90/home
https://sites.google.com/view/6idlof66/home
https://sites.google.com/view/8omphs74/home
https://sites.google.com/view/6xofvc19/home
https://sites.google.com/view/1difbo72/home
https://sites.google.com/view/4knmhu66/home
https://sites.google.com/view/1ssqkm73/home
https://sites.google.com/view/8zavly68/home
https://sites.google.com/view/9zwrzj64/home
https://sites.google.com/view/7wqfid80/home
https://sites.google.com/view/0mzlvh16/home
https://sites.google.com/view/1hozwq52/home
https://sites.google.com/view/3eikgg48/home
https://sites.google.com/view/3skunx74/home
https://sites.google.com/view/7znrmu32/home
https://sites.google.com/view/9xniil98/home
https://sites.google.com/view/1gmoid98/home
https://sites.google.com/view/6gxmmk76/home
https://sites.google.com/view/6xdnsn40/home
https://sites.google.com/view/6qbvjn86/home
https://sites.google.com/view/9veorf12/home
https://sites.google.com/view/4udhos46/home
https://sites.google.com/view/2ynqpm49/home
https://sites.google.com/view/1pauyq10/home
https://sites.google.com/view/4mmhbb67/home
https://sites.google.com/view/5dimfw85/home
https://sites.google.com/view/7qrywi45/home
https://sites.google.com/view/7xmufr77/home
https://sites.google.com/view/0tzmwq99/home
https://sites.google.com/view/2elngz59/home
https://sites.google.com/view/1mopok42/home
https://sites.google.com/view/5galcb39/home
https://sites.google.com/view/3dplbi09/home
https://sites.google.com/view/4hxzuw21/home
https://sites.google.com/view/3ndtxe40/home
https://sites.google.com/view/1wgxgh74/home
https://sites.google.com/view/0ujdvq38/home
https://sites.google.com/view/3qtkgv13/home
https://sites.google.com/view/5jqmpf47/home
https://sites.google.com/view/6ovyem57/home
https://sites.google.com/view/3jpimw15/home
https://sites.google.com/view/1xlfel73/home
https://sites.google.com/view/8jqklu53/home
https://sites.google.com/view/2vrqgs17/home
https://sites.google.com/view/2cmkar10/home
https://sites.google.com/view/5sktrz87/home
https://sites.google.com/view/9qkbxe89/home
https://sites.google.com/view/2zlonk24/home
https://sites.google.com/view/6jnevv87/home
https://sites.google.com/view/1wrgrh89/home
https://sites.google.com/view/5yjmql75/home
https://sites.google.com/view/1eqrct60/home
https://sites.google.com/view/5bgxgx08/home
https://sites.google.com/view/5ikctu17/home
https://sites.google.com/view/8hqypq15/home
https://sites.google.com/view/9nhoum17/home
https://sites.google.com/view/0pvuts20/home
https://sites.google.com/view/9gqmnb23/home
https://sites.google.com/view/6ifnwy59/home
https://sites.google.com/view/2dacaw12/home
https://sites.google.com/view/9vhqej39/home
https://sites.google.com/view/5tmvjt88/home
https://sites.google.com/view/3mnghf05/home
https://sites.google.com/view/6ttioq69/home

收起阅读 »