尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

HarmonyOS应用<奇妙科学乐园>开发第46篇:冷知识卡片FunFactCard——可折叠内容组件

HarmonyOS应用<奇妙科学乐园>开发第46篇:冷知识卡片FunFactCard——可折叠内容组件 引言在上一篇文章中我们深入拆解了文章详情页 TopicDetail 的 Hero 图片区通过两层嵌套的 Stack 布局、linearGradient 渐变遮罩、backdropBlur 毛玻璃效果和 textShadow 文字阴影打造了一个视觉冲击力极强的沉浸式头部区域。当用户继续向下滚动详情页在文章正文段落之后会看到一组风格鲜明的橙色卡片——这就是本文的主角FunFactCard 冷知识卡片组件。FunFactCard 是《奇妙科学乐园》中你知道吗板块的核心展示组件以左侧橙色竖条 浅橙色背景的视觉风格突出展示与科普文章相关的趣味冷知识。每一个冷知识都由一个标题如你知道吗、惊奇发现、冷知识和一段内容文字组成。当前版本中FunFactCard 以静态展开形态呈现所有冷知识内容。然而作为一篇面向进阶开发者的技术解读文章我们不仅要讲清楚现有实现更要在此基础上进阶设计可折叠版本——通过animateTo显式动画实现展开/收起的平滑过渡通过State布尔值控制折叠状态通过 margin/padding 条件渲染优化动画曲线。这种先实现基础版再设计进阶版的演进思路正是组件开发中非常实用的工程方法。源码仓库https://atomgit.com/2301_79280419/WonderSciencePark 学习目标完成本文后你将能够✅ 掌握 FunFactCard 的 Row 双栏布局左侧竖条 右侧内容区✅ 理解不对称 borderRadius 的用法左侧竖条圆角 vs 右侧内容区圆角✅ 运用 layoutWeight 实现自适应宽度的内容区✅ 设计 animateTo 显式动画驱动折叠/展开过渡效果✅ 使用 State 布尔值控制折叠状态 条件渲染✅ 通过 margin 过渡实现平滑的折叠高度动画✅ 掌握组件进阶设计思路从静态版到可折叠版的演进路径 需求分析FunFactCard 的功能定位FunFactCard 在文章详情页中的位置如下文章详情页 TopicDetail ├── Hero 图片区Stack嵌套 ├── 文章标题 阅读量 ├── 演示动画区AnimationDemo ├── 文章正文内容 ├── 你知道吗标题 └── FunFactCard ① ←你知道吗太阳已经燃烧了50亿年...FunFactCard ② ←冷知识王莲的叶子直径可达3米...FunFactCard ③ ←惊奇发现一茶匙中子星物质重约60亿吨...├── 底部操作栏组件视觉结构分析FunFactCard 的视觉结构由两个主要区域组成┌──────────────────────────────────┐ │ ┃ 你知道吗 │ ← 标题行emoji 标题文字 │ ┃ │ │ ┃ 太阳已经燃烧了大约50亿年 │ ← 内容行冷知识正文 │ ┃ 它还能继续燃烧约50亿年呢 │ 多行自动换行 │ ┃ │ └──────────────────────────────────┘ ↑ ↑4vp宽橙色竖条 右侧浅橙色背景区(左上左下圆角)(右上右下圆角)Props 设计需求Props 名称类型必填默认值说明funFactFunFact否空 FunFact 对象冷知识数据title content进阶需求可折叠版本功能描述优先级折叠状态默认收起仅显示标题行P1展开动画animateTo 平滑过渡展开P1收起动画animateTo 平滑过渡收起P1展开指示器标题行右侧显示展开/收起箭头P2最大高度限制展开后超过一定高度可滚动P2️ 核心实现步骤1: 数据模型与默认值功能说明FunFactCard 依赖FunFact数据模型该模型定义在Topic.ets中包含title冷知识标题和content冷知识正文两个字段。与 TopicCard 类似我们需要一个工厂函数生成安全的默认值。源码实现//文件路径entry/src/main/ets/model/Topic.ets /** * 冷知识数据模型 * 每条冷知识由标题和内容组成 */ export interface FunFact { title: string;//冷知识标题如你知道吗、冷知识content: string;//冷知识正文内容 }// 文件路径entry/src/main/ets/components/common/FunFactCard.etsimport{FunFact}from../../model/Topic;import{ThemeColors}from../../constants/AppConstants;/** * 生成默认的空 FunFact 对象 * 用于组件初始化时的兜底值 *returns空的 FunFact 对象 */functiongetDefaultFunFact():FunFact{constfact:FunFact {title:,content:};returnfact; }默认值设计对比// ✅ 正确工厂函数生成完整类型对象functiongetDefaultFunFact():FunFact{constfact:FunFact {title:,content:};returnfact; }// ❌ 错误直接返回字面量类型推断可能不准确functiongetDefaultFunFact() {return{title:,content:};// 类型推断为 { title: string; content: string }非 FunFact}// ❌ 错误返回 undefined组件无法兜底functiongetDefaultFunFact():FunFact|undefined{returnundefined;// 组件需要额外判空增加复杂度步骤2: 组件声明与 Prop 数据接收功能说明FunFactCard 使用Component装饰器声明为可复用组件通过Prop接收 FunFact 数据。由于 FunFactCard 是纯展示组件只展示冷知识不需要修改数据使用 Prop 而非 Link 是最合适的选择。源码实现Componentexport struct FunFactCard {// 接收冷知识数据Prop 单向传递PropfunFact: FunFact getDefaultFunFact(); build() { Row() {// 左侧橙色竖条Column() { }.width(4).backgroundColor(ThemeColors.WARNING).borderRadius({ topLeft:12, bottomLeft:12});// 右侧内容区Column() {// 标题行Row() { Text( this.funFact.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(ThemeColors.WARNING).margin({ bottom:10}); }.width(100%);// 内容行Text(this.funFact.content).fontSize(14).fontColor(ThemeColors.TEXT_SECONDARY).lineHeight(24).width(100%); }.layoutWeight(1).padding({ left:14, right:14, top:16, bottom:16}).backgroundColor(ThemeColors.WARNING_LIGHT).borderRadius({ topRight:12, bottomRight:12}); }.width(100%).backgroundColor(ThemeColors.WARNING_LIGHT).borderRadius(12); } }Props 使用对比// ✅ 正确Prop 用于纯展示组件PropfunFact: FunFact getDefaultFunFact();// ❌ 错误纯展示组件使用 Link违反单向数据流LinkfunFact: FunFact;// ✅ 正确父组件传递数据FunFactCard({ funFact: fact })// ❌ 错误父组件传递 undefinedFunFactCard({ funFact: undefined })// 组件内部有默认值兜底但不应主动传 undefined步骤3: 左侧橙色竖条——不对称圆角功能说明FunFactCard 的视觉特色之一是左侧 4vp 宽的橙色竖条。这根竖条使用borderRadius({ topLeft: 12, bottomLeft: 12 })仅设置左上角和左下角圆角与右侧内容区的右上角和右下角圆角配合共同构成完整卡片的圆角效果。这种分区域圆角的技术在 HarmonyOS 中非常实用。源码实现//左侧橙色竖条装饰条 Column() { }//空容器仅作为颜色条使用 .width(4)//4vp 宽的细条 .backgroundColor(ThemeColors.WARNING)//#ff9800 橙色.borderRadius({ topLeft:12, bottomLeft:12});//仅左上 左下圆角不对称圆角对比// ✅ 正确左侧竖条仅设置左侧圆角.borderRadius({topLeft: 12,bottomLeft: 12 })// 效果竖条左上角和左下角圆润右侧与内容区无缝衔接// ❌ 错误四角都设置圆角.borderRadius(12)// 效果竖条右侧也变圆与内容区之间出现间隙// ❌ 错误不设置圆角.borderRadius(0)// 效果竖条是直角与卡片容器 12vp 圆角不匹配// ✅ 正确右侧内容区设置右侧圆角.borderRadius({topRight: 12,bottomRight: 12 })// 效果内容区右上角和右下角圆润与竖条配合完整// ❌ 错误右侧内容区也设置左侧圆角.borderRadius({topLeft: 12,topRight: 12,bottomLeft: 12,bottomRight: 12 })// 效果内容区左侧也有圆角与竖条之间出现间隙分区域圆角原理图左侧竖条右侧内容区┌──╮┌──────────────┐││←topLeft:12││←topRight:12││││││││││││└──╯←bottomLeft:12││←bottomRight:124vp宽└──────────────┘橙色浅橙色背景步骤4: 右侧内容区——layoutWeight 自适应宽度功能说明右侧内容区使用layoutWeight(1)占据剩余空间。在 Row 布局中左侧竖条占据 4vp 固定宽度右侧内容区通过layoutWeight(1)自动填满剩余空间。内容区内部分为标题行emoji 标题文字和正文行冷知识内容。源码实现// 右侧内容区Column(){// 标题行包裹在 Row 中便于后续扩展箭头图标Row(){Text( this.funFact.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(ThemeColors.WARNING)// #ff9800 橙色.margin({ bottom:10}); } .width(100%);// 正文内容Text(this.funFact.content).fontSize(14).fontColor(ThemeColors.TEXT_SECONDARY)// #666666 灰色.lineHeight(24)// 行高 24vp适合阅读.width(100%); } .layoutWeight(1)// 占满剩余空间.padding({ left:14, right:14, top:16, bottom:16}) .backgroundColor(ThemeColors.WARNING_LIGHT)// #fff3e0 浅橙色.borderRadius({topRight: 12,bottomRight: 12 });// 右上 右下圆角内容区布局对比//✅ 正确使用 layoutWeight(1) 自适应宽度 .layoutWeight(1)//左侧竖条4vp 右侧内容区剩余宽度 100%//❌ 错误使用百分比宽度 .width(95%)//4vp 95% 100%可能导致内容区溢出//❌ 错误使用 calc 计算 .width(calc(100% - 4vp))//ArkTS 不直接支持 CSS calc 语法//✅ 正确lineHeight(24) 适合正文阅读 .fontSize(14).lineHeight(24)//行高 字号的1.7倍阅读舒适//❌ 错误不设置 lineHeight默认行间距可能过大或过小 .fontSize(14)//默认 lineHeight 约为字号的1.2倍段落文字显得拥挤步骤5: 整体卡片容器——背景色统一功能说明FunFactCard 的最外层 Row 设置了与右侧内容区相同的浅橙色背景色和 12vp 全圆角。这里有一个细节左侧竖条的 Column 本身也有背景色深橙色由于竖条在最左侧且宽度只有 4vp它的左上角和左下角圆角与外层 Row 的圆角自然吻合。右侧内容区的浅橙色背景色也与外层 Row 相同确保视觉一致性。源码实现// 最外层容器 RowRow(){// 左侧竖条Column(){ } .width(4) .backgroundColor(ThemeColors.WARNING)// #ff9800 深橙色.borderRadius({topLeft: 12,bottomLeft: 12 });// 右侧内容区Column(){/* 标题 正文 */} .layoutWeight(1).padding({ left:14, right:14, top:16, bottom:16}) .backgroundColor(ThemeColors.WARNING_LIGHT)// #fff3e0 浅橙色.borderRadius({topRight: 12,bottomRight: 12 }); } .width(100%) .backgroundColor(ThemeColors.WARNING_LIGHT)// #fff3e0 与内容区相同.borderRadius(12);// 整体 12vp 圆角背景色一致性对比//✅ 正确外层 Row 背景色与右侧内容区一致 .backgroundColor(ThemeColors.WARNING_LIGHT)//❌ 错误外层 Row 无背景色//.backgroundColor(...) 不设置//左侧竖条和右侧内容区之间可能显示页面底色//❌ 错误外层 Row 背景色与内容区不同 .backgroundColor(ThemeColors.BG_PRIMARY)//白色背景//右侧浅橙色内容区会浮在白色背景上视觉不统一整体布局结构图Row(外层容器,width:100%,backgroundColor: WARNING_LIGHT,borderRadius:12) ├──Column(左侧竖条,width:4,backgroundColor: WARNING) │ └── (空容器纯色块) └──Column(右侧内容区,layoutWeight:1,backgroundColor: WARNING_LIGHT) ├──Row(标题行) │ └──Text( 你知道吗,fontSize:15,fontColor: WARNING) └──Text(正文内容,fontSize:14,fontColor: TEXT_SECONDARY,lineHeight:24)步骤6: 父组件集成——在文章详情页中使用 FunFactCard功能说明FunFactCard 在文章详情页 TopicDetail 中通过 ForEach 遍历文章的 funFacts 数组来渲染。每条 FunFact 生成一个独立的卡片卡片之间设置 10vp 底部间距。源码实现// 文件路径entry/src/main/ets/pages/TopicDetail.ets// 你知道吗板块// 板块标题Column(){Row(){Image($r(app.media.icon_lightbulb)) .width(18) .height(18) .fillColor(ThemeColors.TEXT_PRIMARY).margin({ right:6});Text(你知道吗).fontSize(17).fontWeight(FontWeight.Bold).fontColor(ThemeColors.TEXT_PRIMARY); } .width(100%) .margin({ bottom:12});// 遍历冷知识数组渲染 FunFactCardForEach(this.topic.funFacts, (fact: FunFact) {FunFactCard({funFact:fact}).margin({ bottom:10}); },(fact: FunFact,index:number) fact- index.toString()); } .width(100%) .padding(16) .backgroundColor(ThemeColors.BG_PRIMARY).margin({ top:8, bottom:24});父组件集成对比// ✅ 正确ForEach 唯一 key margin 间距ForEach(this.topic.funFacts, (fact: FunFact) {FunFactCard({funFact:fact}).margin({ bottom:10}); },(fact: FunFact,index:number) fact- index.toString())// ❌ 错误使用 index 作为唯一 keyForEach(this.topic.funFacts, (fact: FunFact) {FunFactCard({funFact:fact}); },(_: FunFact,index:number) index.toString())// 当冷知识列表发生变化时增删排序index key 会导致渲染异常// ❌ 错误不设置卡片间距ForEach(this.topic.funFacts, (fact: FunFact) {FunFactCard({funFact:fact}); })// 所有卡片紧贴在一起视觉拥挤步骤7: 进阶设计——可折叠版 FunFactCard功能说明当前版本的 FunFactCard 始终展开显示所有内容。在冷知识数量较多时例如一篇文章有 5 条冷知识页面会变得很长。进阶方案是设计一个可折叠版本默认仅显示标题行点击后通过animateTo动画平滑展开显示完整内容。再次点击则收起。进阶版完整源码// 文件路径entry/src/main/ets/components/common/FunFactCard.ets进阶折叠版import{FunFact}from../../model/Topic;import{ThemeColors}from../../constants/AppConstants;functiongetDefaultFunFact():FunFact{constfact:FunFact {title:,content:};returnfact; }ComponentexportstructFunFactCard{PropfunFact:FunFactgetDefaultFunFact();StateisExpanded:booleanfalse;// 折叠状态false收起true展开/** * 切换折叠/展开状态 * 使用 animateTo 包裹状态变更驱动平滑过渡动画 */toggleExpand():void{animateTo({duration:300,// 动画时长 300mscurve:Curve.EaseInOut,// 缓入缓出曲线onFinish:() {// 动画完成后的回调可用于日志或埋点} },() {this.isExpanded !this.isExpanded; }); }build() {Row() {// 左侧橙色竖条装饰条Column() { } .width(4) .backgroundColor(ThemeColors.WARNING) .borderRadius({topLeft:12,bottomLeft:12});// 右侧内容区Column() {// 标题行可点击切换折叠Row() {Text( this.funFact.title) .fontSize(15) .fontWeight(FontWeight.Bold) .fontColor(ThemeColors.WARNING) .layoutWeight(1);// 展开/收起箭头指示器Text(this.isExpanded?▲:▼) .fontSize(12) .fontColor(ThemeColors.WARNING); } .width(100%) .onClick(() {this.toggleExpand(); });// 内容行通过条件渲染控制显隐if(this.isExpanded) {Text(this.funFact.content) .fontSize(14) .fontColor(ThemeColors.TEXT_SECONDARY) .lineHeight(24) .width(100%) .margin({top:8}); } } .layoutWeight(1) .padding({left:14,right:14,top:16,bottom:this.isExpanded?16:16}) .backgroundColor(ThemeColors.WARNING_LIGHT) .borderRadius({topRight:12,bottomRight:12}); } .width(100%) .backgroundColor(ThemeColors.WARNING_LIGHT) .borderRadius(12); } }animateTo 动画参数详解参数值说明duration300动画持续 300ms体感流畅curveCurve.EaseInOut缓入缓出起止柔和onFinish() {}动画完成回调可选动画曲线对比// ✅ 推荐EaseInOut起止柔和animateTo({ duration:300, curve: Curve.EaseInOut }, () {this.isExpanded !this.isExpanded; })// ❌ 过快Linear匀速但起止突兀animateTo({ duration:150, curve: Curve.Linear }, () {this.isExpanded !this.isExpanded; })// ❌ 过慢EaseOut收起时感觉拖沓animateTo({ duration:600, curve: Curve.EaseOut }, () {this.isExpanded !this.isExpanded; })// ✅ 可选弹簧效果更活泼animateTo({ duration:350, curve: Curve.Friction }, () {this.isExpanded !this.isExpanded; })条件渲染 vs 高度动画对比// ✅ 方案A条件渲染简单直接适合内容较少的场景if(this.isExpanded) {Text(this.funFact.content).fontSize(14).fontColor(ThemeColors.TEXT_SECONDARY).lineHeight(24); }// 优点实现简单// 缺点展开/收起是出现/消失效果没有高度过渡// ✅ 方案B高度过渡动画更流畅适合追求丝滑体验Column(){Text(this.funFact.content).fontSize(14).fontColor(ThemeColors.TEXT_SECONDARY).lineHeight(24); } .height(this.isExpanded ? undefined :0)// 收起时高度为0.overflow(Hidden)// 超出部分隐藏.clip(true)// 裁剪溢出.margin({ top: this.isExpanded ?8:0})// margin 过渡参与动画// 优点高度平滑过渡更丝滑// 缺点需要额外的 overflow 和 clip 配合// ❌ 错误不使用 animateTo 包裹直接切换状态this.isExpanded !this.isExpanded;// 状态切换瞬间生效无过渡动画体验生硬进阶版 margin 过渡动画方案// 更平滑的折叠动画使用 margin height 组合过渡build(){Row(){Column(){ }// 左侧竖条.width(4) .backgroundColor(ThemeColors.WARNING).borderRadius({topLeft: 12,bottomLeft: 12 });Column(){// 标题行始终显示Row(){Text( this.funFact.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(ThemeColors.WARNING).layoutWeight(1);Text(this.isExpanded? ▲ : ▼).fontSize(12).fontColor(ThemeColors.WARNING); } .width(100%) .onClick(() { this.toggleExpand(); });// 内容区通过 height margin 控制过渡Column(){Text(this.funFact.content).fontSize(14).fontColor(ThemeColors.TEXT_SECONDARY).lineHeight(24).width(100%); } .width(100%) .margin({ top: this.isExpanded ?8:0,// margin top 过渡bottom: this.isExpanded ?0:0// margin bottom 过渡}) .height(this.isExpanded ? undefined :0)// 收起时高度为0.overflow(Hidden)// 溢出隐藏.clip(true);// 裁剪} .layoutWeight(1).padding({ left:14, right:14, top:16, bottom:16}) .backgroundColor(ThemeColors.WARNING_LIGHT).borderRadius({topRight: 12,bottomRight: 12 }); } .width(100%) .backgroundColor(ThemeColors.WARNING_LIGHT).borderRadius(12); } 关键技术详解6.1 不对称 borderRadius 详解ArkTS 支持对四个角分别设置不同的圆角值//单值四角统一 .borderRadius(12)//等同于 borderRadius({ topLeft:12, topRight:12, bottomLeft:12, bottomRight:12})//不对称分别设置 .borderRadius({ topLeft:12,//左上角圆角 topRight:0,//右上角直角 bottomLeft:12,//左下角圆角 bottomRight:0//右下角直角 })//胶囊形状常用技巧 .borderRadius(9999)//等同于宽度的一半形成胶囊形6.2 layoutWeight 工作原理layoutWeight 是 ArkTS 中实现弹性布局的关键属性Row容器宽度360vp├──Column(左侧竖条):width:4vp固定宽度└──Column(右侧内容区):layoutWeight:1弹性宽度实际宽度360vp-4vp356vp如果改为layoutWeight:2且左侧也是layoutWeight:1左侧宽度360vp*(1/3)120vp右侧宽度360vp*(2/3)240vp6.3 animateTo 显式动画 vs 隐式动画特性animateTo显式属性动画隐式触发方式手动调用 animateTo 包裹状态变更直接改变带动画的属性控制粒度精确控制动画参数依赖组件默认动画行为适用场景复杂过渡折叠、位移、缩放简单过渡透明度、颜色代码量需要包裹回调函数直接赋值即可// animateTo 显式动画animateTo({ duration:300, curve: Curve.EaseInOut }, () {this.isExpanded !this.isExpanded;// 状态变更在回调内});// 属性隐式动画对比参考Text(this.title) .opacity(this.isVisible ?1:0)// 直接改变 opacity.transition({ opacity:300})// 声明过渡时长6.4 FunFactCard 在详情页数据流rawfile/topics.json↓loadJsonData()ScienceDataService.topics:Topic[] ↓getTopicById()TopicDetail.topic:Topic|null↓topic.funFactsForEach→FunFactCard({funFact:fact})⚠️ 常见问题与排查问题1: 卡片背景色不一致现象原因解决方案左侧竖条与内容区之间有缝隙外层 Row 背景色与内容区不匹配设置外层 Row 的 backgroundColor 为 WARNING_LIGHT竖条圆角不自然borderRadius 值与外层不一致保持竖条圆角与外层 borderRadius 相同问题2: 内容文字溢出现象原因解决方案长文字超出卡片边界未设置 width(100%)给 Text 设置.width(100%)多行文字行间距过小未设置 lineHeight添加.lineHeight(24)问题3: 折叠动画不流畅现象原因解决方案展开/收起无动画未使用 animateTo 包裹用 animateTo 包裹状态变更动画过快或过慢duration 参数不合理推荐 250-350ms内容瞬间出现/消失使用了条件渲染而非高度过渡改用 height overflow 方案 性能优化ForEach 渲染优化// ✅ 正确使用唯一 key 函数ForEach(this.topic.funFacts, (fact: FunFact) {FunFactCard({funFact:fact}); },(fact: FunFact,index:number) fact- index.toString())// 性能说明FunFactCard 结构简单1个Row 2个Column 2个Text// 即使 5 条冷知识全部渲染性能影响也很小。// 仅在冷知识数量超过 20 条时需要考虑 LazyForEach。动画性能优化// ✅ 动画中避免复杂计算animateTo({ duration:300, curve: Curve.EaseInOut }, () {this.isExpanded !this.isExpanded;// 仅改变布尔值});// ❌ 动画中避免触发重新布局大量组件animateTo({ duration:300}, () {this.isExpanded !this.isExpanded;this.complexLayoutData recalculateAllLayout();// 不应在动画中做重计算}); 测试验证测试用例清单测试场景预期结果验证方式正常数据渲染标题和内容正确显示目视比对 JSON 数据空数据兜底显示空卡片不崩溃传入 getDefaultFunFact()长内容自动换行内容文字不溢出卡片传入 200 字长内容左侧竖条颜色显示 #ff9800 橙色目视检查内容区背景色显示 #fff3e0 浅橙色目视检查圆角效果左侧竖条左圆角 内容区右圆角目视检查多卡片间距卡片之间 10vp 间距传入 3 条 FunFact折叠动画进阶版展开/收起平滑过渡 300ms点击标题行箭头指示器进阶版收起显示 ▼展开显示 ▲目视检查 总结本文从基础实现到进阶设计完整拆解了 FunFactCard 冷知识卡片组件核心技术点汇总如下技术点基础版实现进阶版设计布局结构Row 双 Column竖条 内容同基础版左侧竖条4vp 宽 不对称圆角同基础版内容区layoutWeight(1) padding同基础版数据接收Prop funFact同基础版展开状态始终展开State isExpanded过渡动画无animateTo(300ms, EaseInOut)条件渲染无if (isExpanded) / height(0)展开指示器无▲ / ▼ 箭头FunFactCard 的设计体现了从简单到复杂渐进增强的组件开发理念。基础版本满足了基本的展示需求而进阶的可折叠版本通过animateToState 条件渲染三个核心技术的组合在极少的代码增量下实现了流畅的交互体验。这种渐进式的设计思路值得在所有组件开发中借鉴。 相关链接项目源码Atomgit仓库上一篇HarmonyOS应用奇妙科学乐园开发第45篇:Stack层叠布局——Hero图片区与渐变遮罩实现下一篇HarmonyOS应用奇妙科学乐园开发第47篇:动画演示组件AnimationDemo——科普互动设计
返回列表