文章目录Stack 的基本逻辑文件卡片的结构悬停显示的实现完整代码Stack 子组件的定位悬停动画的关键小结文件管理是 HarmonyOS PC 端的核心场景之一。文件卡片上往往要放操作按钮——重命名、移动、删除——这些按钮平时不显示鼠标悬停时才出现浮在缩略图上方。用Row或Column实现不了这种覆盖效果只能用Stack。这篇把Stack的用法讲清楚顺便把文件卡片的悬浮操作全套做出来。Stack 的基本逻辑Stack是叠放容器。子组件按声明顺序从下到上叠加先声明的在底层后声明的覆盖在上面。Stack(){Image(...)// 底层文件缩略图Column(){...}// 上层操作按钮遮罩悬停时显示}Stack的alignContent参数控制所有子组件的默认对齐位置Stack({alignContent:Alignment.BottomEnd}){// 子组件默认对齐到右下角}但如果每个子组件的位置不一样可以单独设.align(Alignment.xxx)。文件卡片的结构一个 PC 端文件卡片通常包括┌─────────────────────────┐ │ │ │ [文件缩略图/类型图标] │ ← 占据卡片主要区域 │ │ │ [操作遮罩层悬停显示] │ ← Stack 叠加在上 │ [■ 重命名] [■ 移动] [■删] │ └─────────────────────────┘ │ 文件名.docx │ │ 修改于 2024-01-15 2.4MB │ └─────────────────────────┘关键点操作遮罩层和缩略图区是 Stack 里的两层不是 Column 里的两行。悬停显示的实现ArkUI 目前截至 HarmonyOS 5.x没有原生的:hover伪类。PC 端常用onHover事件配合State来模拟悬停效果StateisHovered:booleanfalseStack(){Image(...)// 悬停遮罩isHovered 为 true 时显示if(this.isHovered){Column(){// 操作按钮}.width(100%).height(100%).backgroundColor(#80000000).borderRadius(8)}}.onHover((isHover){this.isHoveredisHover})完整代码enumFileType{Doc,Xlsx,Pdf,Img,Zip,Code}classFileItem{id:numbername:stringtype:FileType size:stringmodifiedAt:stringisFavorite:booleanconstructor(id:number,name:string,type:FileType,size:string,modifiedAt:string,isFavorite:boolean){this.ididthis.namenamethis.typetypethis.sizesizethis.modifiedAtmodifiedAtthis.isFavoriteisFavorite}}EntryComponentstruct PcFileCardPage{Statefiles:FileItem[][newFileItem(1,HarmonyOS设计规范.docx,FileType.Doc,2.4MB,今天 14:23,true),newFileItem(2,Q1项目预算.xlsx,FileType.Xlsx,856KB,昨天 10:15,false),newFileItem(3,产品需求文档v2.3.pdf,FileType.Pdf,4.1MB,2024-01-10,true),newFileItem(4,界面截图合集.png,FileType.Img,12.8MB,2024-01-08,false),newFileItem(5,项目源码备份.zip,FileType.Zip,38.5MB,2024-01-05,false),newFileItem(6,PcHomePage.ets,FileType.Code,18KB,2024-01-12,true),newFileItem(7,会议纪要0115.docx,FileType.Doc,124KB,2024-01-15,false),newFileItem(8,数据统计报表.xlsx,FileType.Xlsx,1.2MB,2024-01-14,false),]StatehoveredId:number-1getFileIcon(type:FileType):string{switch(type){caseFileType.Doc:returncaseFileType.Xlsx:returncaseFileType.Pdf:returncaseFileType.Img:return️caseFileType.Zip:returncaseFileType.Code:returndefault:return}}getFileColor(type:FileType):string{switch(type){caseFileType.Doc:return#EFF6FFcaseFileType.Xlsx:return#F0FDF4caseFileType.Pdf:return#FEF2F2caseFileType.Img:return#FEF3C7caseFileType.Zip:return#F5F3FFcaseFileType.Code:return#F0F9FFdefault:return#F3F4F6}}getFileBadgeColor(type:FileType):string{switch(type){caseFileType.Doc:return#3B82F6caseFileType.Xlsx:return#10B981caseFileType.Pdf:return#EF4444caseFileType.Img:return#F59E0BcaseFileType.Zip:return#8B5CF6caseFileType.Code:return#06B6D4default:return#6B7280}}getFileTypeLabel(type:FileType):string{switch(type){caseFileType.Doc:returnDOCcaseFileType.Xlsx:returnXLSXcaseFileType.Pdf:returnPDFcaseFileType.Img:returnIMGcaseFileType.Zip:returnZIPcaseFileType.Code:returnCODEdefault:returnFILE}}toggleFavorite(id:number){this.filesthis.files.map((file:FileItem){if(file.idid){returnnewFileItem(file.id,file.name,file.type,file.size,file.modifiedAt,!file.isFavorite)}returnfile})}BuilderfileCard(file:FileItem){Column({space:0}){// 缩略图区域Stack实现悬停操作层Stack({alignContent:Alignment.Center}){// 底层文件图标背景Column(){Text(this.getFileIcon(file.type)).fontSize(40)Text(this.getFileTypeLabel(file.type)).fontSize(10).fontColor(this.getFileBadgeColor(file.type)).fontWeight(FontWeight.Bold).padding({left:6,right:6,top:2,bottom:2}).backgroundColor(Color.White).borderRadius(4).margin({top:8})}.width(100%).height(120).backgroundColor(this.getFileColor(file.type)).borderRadius({topLeft:12,topRight:12}).justifyContent(FlexAlign.Center)// 上层悬停操作遮罩if(this.hoveredIdfile.id){Column({space:12}){Row({space:8}){// 重命名Column({space:4}){Text(✏️).fontSize(18)Text(重命名).fontSize(11).fontColor(Color.White)}.padding({left:12,right:12,top:8,bottom:8}).backgroundColor(#40FFFFFF).borderRadius(8).onClick((){})// 移动Column({space:4}){Text().fontSize(18)Text(移动).fontSize(11).fontColor(Color.White)}.padding({left:12,right:12,top:8,bottom:8}).backgroundColor(#40FFFFFF).borderRadius(8).onClick((){})// 删除Column({space:4}){Text(️).fontSize(18)Text(删除).fontSize(11).fontColor(#FCA5A5)}.padding({left:12,right:12,top:8,bottom:8}).backgroundColor(#40FFFFFF).borderRadius(8).onClick((){})}}.width(100%).height(120).backgroundColor(#80000000).borderRadius({topLeft:12,topRight:12}).justifyContent(FlexAlign.Center)// 右上角收藏星标Stack子项单独定位Text(file.isFavorite?★:☆).fontSize(16).fontColor(file.isFavorite?#F59E0B:Color.White).padding(6).onClick((){this.toggleFavorite(file.id)}).align(Alignment.TopEnd).margin({top:8,right:8}).position({x:100%,y:0}).offset({x:-32,y:0})}}.width(100%).onHover((isHover){this.hoveredIdisHover?file.id:-1})// 文件信息区Column({space:4}){Text(file.name).fontSize(13).fontColor(#1F2937).fontWeight(FontWeight.Medium).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis}).width(100%)Row(){Text(file.modifiedAt).fontSize(11).fontColor(#9CA3AF).layoutWeight(1)Text(file.size).fontSize(11).fontColor(#9CA3AF)}.width(100%)}.padding({left:12,right:12,top:10,bottom:12}).alignItems(HorizontalAlign.Start)}.backgroundColor(Color.White).borderRadius(12).shadow({radius:this.hoveredIdfile.id?16:4,color:this.hoveredIdfile.id?#1A3B82F6:#0F000000,offsetY:this.hoveredIdfile.id?4:2}).scale({x:this.hoveredIdfile.id?1.02:1.0,y:this.hoveredIdfile.id?1.02:1.0}).animation({duration:200,curve:Curve.EaseOut})}build(){Column({space:0}){// 顶部工具栏Row({space:16}){Text(我的文件).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#111827).layoutWeight(1)Row({space:8}){Button( 上传).fontSize(13).height(36).backgroundColor(#3B82F6).borderRadius(8)Button(新建文件夹).fontSize(13).height(36).backgroundColor(#F3F4F6).fontColor(#374151).borderRadius(8)}}.padding({left:32,right:32,top:24,bottom:20}).backgroundColor(Color.White).width(100%)Divider().strokeWidth(1).color(#F3F4F6)// 文件网格Scroll(){Flex({wrap:FlexWrap.Wrap,alignContent:FlexAlign.Start}){ForEach(this.files,(file:FileItem){Column(){this.fileCard(file)}.width(calc(25% - 12px)).margin({right:16,bottom:16})})}.width(100%).padding({left:32,right:16,top:24,bottom:32})}.layoutWeight(1).backgroundColor(#F9FAFB)}.width(100%).height(100%).constraintSize({minWidth:800})}}Stack 子组件的定位Stack里每个子组件默认居中对齐。如果你需要某个子组件固定在特定角落有两种方式alignContent全局控制Stack({ alignContent: Alignment.TopEnd })让所有子组件默认右上角对齐。单独.align()覆盖某个子组件用.align(Alignment.BottomStart)覆盖全局设置。代码里收藏星标用了.position().offset()精确定位到右上角而不是依赖alignContent因为整个遮罩层已经alignContent: Alignment.Center了需要单独给星标定位。悬停动画的关键卡片悬停时微缩放 1.02 加深阴影视觉上有浮起的感觉.scale({x:this.hoveredIdfile.id?1.02:1.0}).shadow({radius:this.hoveredIdfile.id?16:4}).animation({duration:200,curve:Curve.EaseOut})animation让状态变化有过渡不是瞬间跳变。duration: 200是比较舒适的悬停反馈时长太短感觉生硬太长感觉迟钝。小结Stack解决的是同一位置放多层内容的问题。文件卡片悬停操作、角标、进度遮罩……这类叠在上面的东西Stack是标准解法。记住两点后声明的子组件在上层子组件位置用alignContent.align()控制。