鸿蒙应用开发实战【17】— bindPopup 气泡弹出与筛选面板实现
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
号码助手首页有一个「全部/已选N ⌄」筛选按钮,点击后会弹出一个气泡面板,展示分类、卡号、状态三维筛选条件。这个功能使用 ArkUI 的bindPopup实现——它能将一个自定义的@Builder内容以气泡形式浮层显示在触发元素附近,不会遮挡整个屏幕。
本篇涵盖:bindPopup 基本用法、PopupOptions 参数详解、Placement 弹出位置、自定义 @Builder builder 内容、onStateChange 状态回调、Flex + FlexWrap 实现 chip 布局、三维筛选系统完整实现。
一、bindPopup 组件预览
图1:bindPopup 锚点定位原理 + Placement 九宫格方位枚举
二、bindPopup 概述
2.1 bindPopup vs bindSheet 的选择
| 特性 | bindPopup | bindSheet |
|---|---|---|
| 显示位置 | 触发元素附近(气泡) | 屏幕底部 |
| 遮挡内容 | 部分遮挡(小区域) | 遮挡大部分屏幕 |
| 适合内容 | 少量选项(筛选、快捷操作) | 大量内容(列表选择) |
| 关闭方式 | 点击外部自动关闭 | 下滑或点击完成关闭 |
| 号码助手使用 | 首页筛选面板 | AddAppPage 卡号选择 |
2.2 基本语法
// 绑定在触发元素上Button('筛选').bindPopup(this.showPopup,{builder:this.PopupContent,// @Builder 方法placement:Placement.Bottom,// 弹出位置popupColor:'#FFFFFF',// 气泡背景色onStateChange:(e)=>{if(!e.isVisible){this.showPopup=false}}}).onClick(()=>{this.showPopup=!this.showPopup})三、bindPopup 参数详解
3.1 完整参数
interfacePopupOptions{builder?:CustomBuilder// 自定义内容(@Builder 方法)placement?:Placement// 弹出方向popupColor?:ResourceColor// 气泡背景色autoCancel?:boolean// 点击外部是否自动关闭(默认 true)offset?:{x:number,y:number}// 弹出偏移onStateChange?:(e:{isVisible:boolean})=>void// 状态变化回调arrowOffset?:number// 箭头偏移showInSubWindow?:boolean// 是否在子窗口显示maskColor?:ResourceColor// 遮罩颜色enableArrow?:boolean// 是否显示箭头(默认 true)}3.2 Placement 弹出位置
// 可用的 Placement 枚举值Placement.Top// 触发元素上方Placement.Bottom// 触发元素下方(号码助手使用)✅Placement.Left// 触发元素左侧Placement.Right// 触发元素右侧Placement.TopLeft// 上方偏左Placement.TopRight// 上方偏右Placement.BottomLeft// 下方偏左Placement.BottomRight// 下方偏右// ❌ 注意:Placement.BottomEnd 不存在!// 正确使用 Placement.Bottom + 通过 offset 微调位置四、首页筛选面板完整实现
4.1 触发按钮
// HomePage.ets — 筛选按钮Row({space:3}){Text(this.activeFilterCount()>0?`已选${this.activeFilterCount()}`:'全部').fontSize(12).fontWeight(AppFonts.WEIGHT_SEMIBOLD).fontColor(this.activeFilterCount()>0?'#3E68E0':AppColors.TEXT_2)Text('⌄').fontSize(10).fontColor(this.activeFilterCount()>0?'#3E68E0':AppColors.TEXT_3)}.padding({left:11,right:11,top:6,bottom:6}).backgroundColor(this.activeFilterCount()>0?AppColors.PRIMARY_BG:AppColors.CARD_B).border({width:1,color:this.activeFilterCount()>0?'#4D4F7CFF':AppColors.LINE,radius:14})// ← bindPopup 绑定.bindPopup(this.showFilter,{builder:this.FilterPopoverBuilder,placement:Placement.Bottom,popupColor:'#FFFFFF',onStateChange:(e)=>{if(!e.isVisible){this.showFilter=false}// 点外部关闭时同步状态}}).onClick(()=>{this.showFilter=!this.showFilter})4.2 @Builder 筛选面板内容
@BuilderprivateFilterPopoverBuilder(){Column(){// ── 分类筛选 ──Column(){Text('分类').fontSize(11).fontColor(AppColors.TEXT_3).fontWeight(AppFonts.WEIGHT_SEMIBOLD).alignSelf(ItemAlign.Start).margin({bottom:8})Flex({wrap:FlexWrap.Wrap}){ForEach(CATEGORIES,(cat:string,idx:number)=>{Text(cat).fontSize(12).fontWeight(AppFonts.WEIGHT_MEDIUM).fontColor(this.filterCatIdx===idx?'#FFFFFF':AppColors.TEXT_2).padding({left:12,right:12,top:6,bottom:6}).backgroundColor(this.filterCatIdx===idx?AppColors.PRIMARY:AppColors.CARD_B).border({width:1,color:this.filterCatIdx===idx?'#00000000':AppColors.LINE,radius:14}).margin({right:6,bottom:6}).onClick(()=>{this.filterCatIdx=idx})},(cat:string)=>cat)}}.margin({bottom:14}).alignItems(HorizontalAlign.Start)// ── 卡号筛选 ──Column(){Text('卡号').fontSize(11).fontColor(AppColors.TEXT_3).fontWeight(AppFonts.WEIGHT_SEMIBOLD).alignSelf(ItemAlign.Start).margin({bottom:8})Flex({wrap:FlexWrap.Wrap}){// 「全部」选项Text('全部').fontSize(12).fontWeight(AppFonts.WEIGHT_MEDIUM).fontColor(this.filterCardIdx===0?'#FFFFFF':AppColors.TEXT_2).padding({left:12,right:12,top:6,bottom:6}).backgroundColor(this.filterCardIdx===0?AppColors.PRIMARY:AppColors.CARD_B).border({width:1,color:this.filterCardIdx===0?'#00000000':AppColors.LINE,radius:14}).margin({right:6,bottom:6}).onClick(()=>{this.filterCardIdx=0})// 各卡号选项ForEach(this.cards,(card:CardEntity,idx:number)=>{Text(card.label).fontSize(12).fontWeight(AppFonts.WEIGHT_MEDIUM).fontColor(this.filterCardIdx===idx+1?'#FFFFFF':AppColors.TEXT_2).padding({left:12,right:12,top:6,bottom:6}).backgroundColor(this.filterCardIdx===idx+1?AppColors.PRIMARY:AppColors.CARD_B).border({width:1,color:this.filterCardIdx===idx+1?'#00000000':AppColors.LINE,radius:14}).margin({right:6,bottom:6}).onClick(()=>{this.filterCardIdx=idx+1})},(card:CardEntity)=>`fc_${card.id??0}`)}}.margin({bottom:14}).alignItems(HorizontalAlign.Start)// ── 状态筛选 ──Column(){Text('状态').fontSize(11).fontColor(AppColors.TEXT_3).fontWeight(AppFonts.WEIGHT_SEMIBOLD).alignSelf(ItemAlign.Start).margin({bottom:8})Flex({wrap:FlexWrap.Wrap}){ForEach(STATUS_FILTERS,(s:string,idx:number)=>{Text(s).fontSize(12).fontWeight(AppFonts.WEIGHT_MEDIUM).fontColor(this.filterStatIdx===idx?'#FFFFFF':AppColors.TEXT_2).padding({left:12,right:12,top:6,bottom:6}).backgroundColor(this.filterStatIdx===idx?AppColors.PRIMARY:AppColors.CARD_B).border({width:1,color:this.filterStatIdx===idx?'#00000000':AppColors.LINE,radius:14}).margin({right:6,bottom:6}).onClick(()=>{this.filterStatIdx=idx})},(s:string)=>s)}}.margin({bottom:12}).alignItems(HorizontalAlign.Start)// ── 底部按钮 ──Row({space:8}){Button('重置',{type:ButtonType.Normal}).layoutWeight(1).height(36).fontSize(12).fontColor(AppColors.TEXT).backgroundColor(AppColors.CARD_B).border({width:1,color:AppColors.LINE,radius:10}).onClick(()=>this.resetFilters())Button('完成',{type:ButtonType.Normal}).layoutWeight(1).height(36).fontSize(12).fontColor('#FFFFFF').borderRadius(10).linearGradient({angle:135,colors:[[AppColors.PRIMARY,0],[AppColors.PRIMARY_2,1]]}).backgroundColor(Color.Transparent).onClick(()=>{this.showFilter=false})}}.width(220).padding(14)}五、Flex + FlexWrap 实现 Chip 布局
5.1 FlexWrap 自动换行
// Chip 标签自动换行布局(筛选选项数量不固定时很有用)Flex({wrap:FlexWrap.Wrap}){Text('分类1').margin({right:6,bottom:6})Text('分类2').margin({right:6,bottom:6})Text('分类3').margin({right:6,bottom:6})// 超过一行时自动换行}// 关键参数:// FlexWrap.Wrap:允许换行(默认 NoWrap 不换行)// margin.right:chip 之间的水平间距// margin.bottom:行与行之间的垂直间距5.2 与 Row 的区别
// Row:固定单行,不自动换行(内容超出时裁剪)Row({space:6}){Text('分类1')Text('分类2')Text('分类3')}// Flex + wrap:多行自动换行(适合数量不固定的标签)Flex({wrap:FlexWrap.Wrap}){Text('分类1').margin(...)Text('分类2').margin(...)Text('分类3').margin(...)}六、筛选状态管理
6.1 筛选条件计数
privateactiveFilterCount():number{letn=0if(this.filterCatIdx!==0)n++// 非「全部」时 +1if(this.filterCardIdx!==0)n++if(this.filterStatIdx!==0)n++returnn}6.2 筛选重置
privateresetFilters():void{this.filterCatIdx=0this.filterCardIdx=0this.filterStatIdx=0this.showFilter=false// 重置后关闭面板}6.3 三维过滤逻辑
privategetFilteredRows():AppRow[]{returnthis.rows.filter((r:AppRow)=>{constcatOk=this.filterCatIdx===0||r.binding.category===CATEGORIES[this.filterCatIdx]constcardOk=this.filterCardIdx===0||r.cardLabel===this.cards[this.filterCardIdx-1]?.labelconststatOk=this.filterStatIdx===0||r.binding.status===STATUS_FILTERS[this.filterStatIdx]returncatOk&&cardOk&&statOk})}七、本篇小结
| 知识点 | 核心要点 |
|---|---|
| bindPopup | 气泡弹出,适合小型筛选面板 |
| Placement.Bottom | 在触发元素下方弹出 |
| onStateChange | 点击外部关闭时同步 @State |
| Flex + FlexWrap.Wrap | 标签自动换行(chip 布局) |
| 筛选三维度 | 分类/卡号/状态独立过滤,AND 关系 |
参考资料
- 鸿蒙应用开发实战【05】— 首页开发与状态管理
- 鸿蒙应用开发实战【16】— bindSheet 底部弹出面板
- bindPopup API 参考
- Placement 枚举
- Flex 容器
- FlexWrap 说明
- @Builder 装饰器
- Button 组件
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- bindPopup 文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-popup
- Flex 容器文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-flex