一、引言
在鸿蒙(HarmonyOS)应用开发中,浮动操作按钮(FAB) 和 抽屉菜单 是Material Design的核心组件,用于提升操作效率和界面空间利用率。FAB是位于屏幕右下角的圆形按钮,用于触发主要功能;抽屉菜单是从屏幕边缘滑出的侧边栏,用于展示次要功能。两者结合可实现高效操作流与沉浸式体验。本文将系统讲解鸿蒙FAB与抽屉菜单的实现原理、代码封装及交互动效设计,提供可直接集成的完整代码。
二、技术背景
1. 鸿蒙UI框架核心组件
|
|
|
|
|
|
|
|
|
|
DrawerLayout+ NavigationView
|
|
|
|
|
|
|
|
|
|
2. 关键技术特性
三、应用场景
|
|
|
|
|
|
主界面右下角FAB点击打开分享菜单(微信/微博/邮件)
|
|
|
|
FAB悬浮在商品图片上,点击展开操作菜单(收藏/购物车/客服)
|
|
|
|
文档编辑界面FAB触发多功能菜单(插入表格/图片/链接)
|
|
|
|
FAB控制地图视角(2D/3D切换),抽屉菜单展示图层选项
|
|
四、核心原理与流程图
1. FAB变形原理
graph TD
A[初始状态] -->|点击| B[FAB变形为关闭图标]
B -->|动画完成| C[展开抽屉菜单]
C -->|点击外部/关闭按钮| D[收起菜单]
D -->|动画完成| E[FAB恢复原始状态]
2. 抽屉菜单交互流程
graph TD
A[用户操作] --> B{操作类型}
B -->|点击FAB| C[展开菜单]
B -->|边缘滑动| D[展开菜单]
B -->|点击遮罩层| E[收起菜单]
B -->|点击菜单项| F[执行对应操作]
C --> G[显示遮罩层]
D --> G
E --> H[隐藏遮罩层]
F --> H
G --> I[菜单滑入动画]
H --> J[菜单滑出动画]
五、核心特性
-
-
-
-
-
六、环境准备
1. 开发环境
-
-
HarmonyOS SDK:API 9+(支持ArkUI声明式开发)
-
设备:真机/模拟器(Phone/Tablet/Foldable)
2. 项目配置
{
"module": {
"abilities": [
{
"skills": [
{
"entities": ["entity.system.navigation"],
"actions": ["action.system.navigator"]
}
]
}
]
}
}
七、详细代码实现
以下分基础FAB、抽屉菜单、联动交互三个场景实现完整功能。
场景1:基础FAB封装(FabComponent)
1. 组件代码(FabComponent.ets)
// FAB组件
@Component
export struct FabComponent {
@Link isExpanded: boolean // 展开状态
@Prop icon: Resource = $r('app.media.ic_add') // 默认图标
@Prop expandedIcon: Resource = $r('app.media.ic_close') // 展开状态图标
@Prop position: { x: number, y: number } = { x: 0, y: 0 } // 位置偏移
@Prop size: number = 56 // 直径(px)
@Prop color: string = '#FF4081' // 背景色
private animator: AnimatorProperty = new AnimatorProperty()
build() {
Button() {
Image(this.isExpanded ? this.expandedIcon : this.icon)
.width(24)
.height(24)
.objectFit(ImageFit.Contain)
}
.width(this.size)
.height(this.size)
.borderRadius(this.size / 2)
.backgroundColor(this.color)
.shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 4 })
.position({ x: this.position.x, y: this.position.y })
.onClick(() => {
this.isExpanded = !this.isExpanded
this.playTransformAnimation()
})
.onReady(() => {
// 初始位置自适应
const safeArea = display.getDefaultDisplaySync().safeArea
this.position = {
x: display.getDefaultDisplaySync().width - this.size - 20,
y: safeArea.bottom - this.size - 20
}
})
}
// 播放变形动画
private playTransformAnimation() {
animateTo({
duration: 300,
curve: Curve.EaseOut,
iterations: 1,
playMode: PlayMode.Normal
}, () => {
// 实际项目中可添加缩放/旋转效果
})
}
}
场景2:抽屉菜单封装(DrawerMenu)
1. 组件代码(DrawerMenu.ets)
// 菜单项模型
interface MenuItem {
id: string;
icon?: Resource;
text: string;
group?: string;
subItems?: MenuItem[];
action?: () => void;
}
@Component
export struct DrawerMenu {
@Link isOpen: boolean // 菜单打开状态
@Prop menuItems: MenuItem[] = [] // 菜单数据
@Prop headerView?: () => void // 自定义头部视图
@State activeGroup: string = '' // 当前激活的分组
build() {
if (!this.isOpen) return
// 遮罩层
Stack() {
// 菜单内容
Column() {
// 头部视图
if (this.headerView) {
this.headerView()
} else {
this.defaultHeader()
}
// 菜单项
Scroll() {
Column() {
ForEach(this.groupedMenuItems, (group: {title: string, items: MenuItem[]}) => {
this.buildMenuGroup(group.title, group.items)
}, group => group.title)
}
}
.flexGrow(1)
}
.width('80%')
.height('100%')
.backgroundColor('#FFFFFF')
.translate({ x: this.isOpen ? '0%' : '-100%' })
.animation({ duration: 300, curve: Curve.EaseOut })
}
.width('100%')
.height('100%')
.backgroundColor('#80000000') // 半透明遮罩
.onClick(() => this.isOpen = false) // 点击遮罩关闭
}
// 默认头部视图
@Builder defaultHeader() {
Column() {
Image($r('app.media.ic_user'))
.width(80)
.height(80)
.borderRadius(40)
.margin({ top: 40 })
Text('用户名')
.fontSize(20)
.margin({ top: 10 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding(20)
}
// 构建菜单分组
private get groupedMenuItems() {
const groups: Record<string, MenuItem[]> = {}
this.menuItems.forEach(item => {
const group = item.group || 'default'
if (!groups[group]) groups[group] = []
groups[group].push(item)
})
return Object.keys(groups).map(title => ({
title,
items: groups[title]
}))
}
// 构建单个菜单组
@Builder buildMenuGroup(title: string, items: MenuItem[]) {
if (title !== 'default') {
Text(title)
.fontSize(14)
.fontColor('#999999')
.margin({ top: 20, left: 20, bottom: 10 })
}
ForEach(items, item => {
if (item.subItems) {
this.buildSubMenu(item)
} else {
this.buildMenuItem(item)
}
}, item => item.id)
}
// 构建普通菜单项
@Builder buildMenuItem(item: MenuItem) {
Row() {
if (item.icon) {
Image(item.icon)
.width(24)
.height(24)
.margin({ right: 16 })
}
Text(item.text)
.fontSize(16)
}
.padding(16)
.width('100%')
.onClick(() => {
item.action?.()
this.isOpen = false
})
}
// 构建带子菜单的项
@Builder buildSubMenu(item: MenuItem) {
Column() {
// 父菜单项
Row() {
if (item.icon) {
Image(item.icon)
.width(24)
.height(24)
.margin({ right: 16 })
}
Text(item.text)
.fontSize(16)
Image($r('app.media.ic_arrow_right'))
.width(16)
.height(16)
.margin({ left: 'auto' })
}
.padding(16)
.width('100%')
.onClick(() => this.activeGroup = this.activeGroup === item.id ? '' : item.id)
// 子菜单项
if (this.activeGroup === item.id) {
Column() {
ForEach(item.subItems!, subItem => this.buildMenuItem(subItem), subItem => subItem.id)
}
.padding({ left: 40 })
.backgroundColor('#F5F5F5')
}
}
}
}
场景3:FAB与抽屉菜单联动(FabDrawerDemo)
1. 页面代码(FabDrawerDemo.ets)
import { FabComponent } from '../components/FabComponent'
import { DrawerMenu } from '../components/DrawerMenu'
@Entry
@Component
struct FabDrawerDemo {
@State isMenuOpen: boolean = false
@State fabPosition: { x: number, y: number } = { x: 0, y: 0 }
// 菜单数据
private menuItems: MenuItem[] = [
{ id: 'home', icon: $r('app.media.ic_home'), text: '首页', action: () => {} },
{ id: 'search', icon: $r('app.media.ic_search'), text: '搜索', action: () => {} },
{
id: 'share',
icon: $r('app.media.ic_share'),
text: '分享',
group: 'social',
subItems: [
{ id: 'wechat', icon: $r('app.media.ic_wechat'), text: '微信', action: () => {} },
{ id: 'weibo', icon: $r('app.media.ic_weibo'), text: '微博', action: () => {} },
{ id: 'email', icon: $r('app.media.ic_email'), text: '邮件', action: () => {} }
]
},
{ id: 'settings', icon: $r('app.media.ic_settings'), text: '设置', action: () => {} }
]
build() {
Stack() {
// 主内容区
Column() {
Text('主界面内容')
.fontSize(24)
.margin(20)
// ...其他内容
}
.width('100%')
.height('100%')
.backgroundColor('#F0F0F0')
// FAB按钮
FabComponent({
isExpanded: $isMenuOpen,
icon: $r('app.media.ic_add'),
expandedIcon: $r('app.media.ic_close'),
position: this.fabPosition,
size: 64,
color: '#FF4081'
})
// 抽屉菜单
DrawerMenu({
isOpen: $isMenuOpen,
menuItems: this.menuItems,
headerView: this.buildCustomHeader
})
}
.width('100%')
.height('100%')
.onReady(() => {
// 初始化FAB位置
const display = display.getDefaultDisplaySync()
const safeArea = display.safeArea
this.fabPosition = {
x: display.width - 84,
y: safeArea.bottom - 84
}
})
}
// 自定义头部视图
@Builder buildCustomHeader() {
Column() {
Image($r('app.media.profile'))
.width(80)
.height(80)
.borderRadius(40)
.margin({ top: 40 })
Text('张三')
.fontSize(20)
.margin({ top: 10 })
Text('高级会员')
.fontSize(14)
.fontColor('#FFD700')
.margin({ top: 5 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding(20)
}
}
八、运行结果与测试步骤
1. 预期效果
-
FAB交互:点击时平滑变形为关闭图标,再次点击恢复
-
-
-
2. 测试步骤
-
-
安装DevEco Studio 3.1+,创建"Empty Ability"项目(语言选择TS)
-
添加图标资源到
resources/base/media/目录
-
-
九、部署场景
|
|
|
|
|
|
|
|
|
|
|
|
|
|
增大触摸区域(最小48×48dp),支持遥控器方向键导航
|
|
|
|
十、疑难解答
|
|
|
|
|
|
|
使用display.getDefaultDisplaySync().safeArea计算位置
|
|
|
|
添加.renderGroup(true)启用合成层
|
|
|
|
使用系统资源@color/text_color_primary
|
|
|
|
使用Column嵌套并设置padding({left: 40})
|
|
|
|
使用gesturePriority设置菜单手势优先级高于页面滚动
|
十一、未来展望与技术趋势
1. 趋势
2. 挑战
-
多端一致性:手机/车机/手表等不同形态设备的交互适配
-
-
十二、总结
-
FAB变形动画:通过
AnimatorProperty实现平滑状态切换
-
抽屉菜单结构:使用
Stack+Column实现滑入滑出效果
-
-
-
通过本文封装的组件,开发者可快速实现符合鸿蒙设计规范的专业级FAB与抽屉菜单,提升应用操作效率和用户体验。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
评论(0)