该功能仿照米家APP设计,在页面右上角设置悬浮加号按钮,点击后弹出包含多个功能选项的菜单,基于uniapp开发,支持iOS、Android等多端适配,通过CSS动画实现菜单展开收起效果,结合事件绑定处理选项点击逻辑,样式采用圆角、阴影等细节设计,提升用户体验,适用于需快速触发多功能的场景。
Uniapp实现仿米家右上角悬浮菜单交互效果
在移动端应用设计中,简洁高效的交互体验对提升用户留存率至关重要,小米米家APP的右上角悬浮菜单(点击加号按钮后菜单项以圆形扩散动画展开)因其直观的操作逻辑和流畅的视觉效果,已成为众多产品效仿的经典案例,本文将基于Uniapp框架,从组件拆解、动画实现到跨平台适配,完整拆解这一交互效果的实现方案。
效果概述与实现思路
目标效果
点击页面右上角固定位置的加号按钮,触发菜单展开动画:菜单项以按钮中心为圆心呈圆形扩散排列,支持点击菜单项执行对应操作;点击遮罩层或再次点击加号按钮可收起菜单,同时伴随反向动画效果。
实现思路
- 组件化拆分:将悬浮菜单拆分为"固定按钮"和"动态菜单"两个独立组件,通过状态变量控制显隐逻辑
- 布局方案:采用fixed定位实现按钮固定,菜单项使用绝对定位配合三角函数计算扩散位置
- 交互逻辑:通过v-show指令控制菜单显示,结合事件冒泡处理遮罩层点击和菜单项点击
- 动画实现:使用CSS transition实现位移动画,结合opacity渐变实现淡入淡出效果
- 跨平台适配:针对不同平台特性使用条件编译,优化点击区域和动画表现
详细实现步骤
创建基础页面结构
在Uniapp页面中实现悬浮菜单的基础布局,以`index.vue`为例:
<template>
<view class="container">
<!-- 页面主体内容 -->
<view class="content">
<text>这是页面主体内容区域</text>
</view>
<!-- 右上角悬浮按钮 -->
<view class="fab-btn" @click="toggleMenu">
<text class="plus-icon">+</text>
</view>
<!-- 弹出菜单容器 -->
<view class="menu-wrapper" v-show="isMenuShow" @click="closeMenu">
<!-- 遮罩层 -->
<view class="menu-mask"></view>
<!-- 菜单项列表 -->
<view class="menu-list">
<view
class="menu-item"
v-for="(item, index) in menuItems"
:key="index"
:style="getItemStyle(index)"
@click.stop="handleItemClick(item)"
>
<image class="item-icon" :src="item.icon" mode="aspectFit"></image>
<text class="item-text">{{item.text}}</text>
</view>
</view>
</view>
</view>
</template>
定义状态与数据
在`script`部分定义菜单数据和控制变量:
<script>
export default {
data() {
return {
isMenuShow: false, // 菜单显示状态
menuItems: [
{ icon: '/static/icons/scan.png', text: '扫码', action: 'scan' },
{ icon: '/static/icons/album.png', text: '相册', action: 'album' },
{ icon: '/static/icons/file.png', text: '文件', action: 'file' },
{ icon: '/static/icons/share.png', text: '分享', action: 'share' }
]
}
},
methods: {
// 切换菜单显示/隐藏
toggleMenu() {
this.isMenuShow = !this.isMenuShow;
if (this.isMenuShow) {
this.$nextTick(() => {
this.animateMenuItems(); // 初始化菜单项动画
});
}
},
// 关闭菜单(点击遮罩层)
closeMenu() {
this.isMenuShow = false;
},
// 处理菜单项点击
handleItemClick(item) {
console.log('执行操作:', item.action);
// 实际业务逻辑(如页面跳转、API调用等)
this.closeMenu();
},
// 计算菜单项位置(圆形扩散)
getItemStyle(index) {
const totalItems = this.menuItems.length;
const angleStep = (2 * Math.PI) / totalItems;
const radius = 100; // 扩散半径(rpx)
const angle = angleStep * index;
// 计算x,y坐标(转换为rpx单位)
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
return {
transform: `translate(${x}rpx, ${y}rpx)`,
opacity: 0,
transition: 'all 0.3s ease'
};
},
// 初始化菜单项动画
animateMenuItems() {
this.$nextTick(() => {
const menuItems = document.querySelectorAll('.menu-item');
menuItems.forEach((item, index) => {
setTimeout(() => {
item.style.opacity = 1;
}, index * 50); // 错开动画时间
});
});
}
}
}
</script>
样式设计与动画实现
在`style`部分实现完整样式和动画效果:
<style>
.container {
position: relative;
width: 100%;
min-height: 100vh;
background-color: #f8f8f8;
}
.content {
padding: 40rpx;
text-align: center;
}
/* 悬浮按钮样式 */
.fab-btn {
position: fixed;
top: 40rpx;
right: 40rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #ff6700, #ff9800);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 16rpx rgba(255, 103, 0, 0.3);
z-index: 1000;
transition: transform 0.3s ease;
}
.fab-btn:active {
transform: scale(0.95);
}
.plus-icon {
color: #ffffff;
font-size: 60rpx;
font-weight: bold;
}
/* 菜单容器 */
.menu-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
}
/* 遮罩层 */
.menu-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
transition: opacity 0.3s ease;
}
/* 菜单项列表 */
.menu-list {
position: absolute;
top: 40rpx;
right: 40rpx;
width: 200rpx;
height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
}
/* 单个菜单项 */
.menu