movable-area + movable-view 是唯一靠谱的跨平台方案,因原生封装保障性能与兼容性;需设明确宽高、fixed 定位、direction="all"、动态计算坐标及安全区限位,并配合吸附动画与 nvue 实现 App 全局悬浮。

movable-area + movable-view 是唯一靠谱的跨平台方案
别用 position: fixed + @touchmove 手动算坐标,那在小程序里根本跑不起来——事件穿透、卡顿、安卓拖出屏幕、iOS 状态栏遮挡全是常态。官方 movable-area 和 movable-view 是原生组件封装,各端都走底层 touch 处理,性能和兼容性有保障。
关键点:movable-area 必须设明确宽高(不能只写 100%),且要用 fixed 定位;movable-view 的 direction 设为 "all",damping 建议 30–50(值越大越“沉”,手感更稳)。
-
movable-area的style要动态绑定:width: screenWidth + 'px'、height: screenHeight + 'px',否则 H5 下可能失效,小程序下直接拖不动 - 初始
x、y别写死像素,按屏幕宽度/高度比例算,比如靠右:x: screenWidth - ballSize - 微信小程序中,
movable-area必须包裹在cover-view内才不会被下拉刷新或导航栏截断——但cover-view又不支持v-if,所以得用hidden控制显隐
吸附逻辑不是“松手就跳”,而是带缓动的平滑归位
直接 this.x = 0 或 this.x = screenWidth - ballSize 会显得生硬。真正在用的方案是:松手后判断当前位置离左/右边缘的距离,若小于阈值(比如 80rpx),就用 uni.createAnimation() 或 CSS transition 驱动归位动画。
注意两个细节:
- 阈值不能固定写死,要随球尺寸动态算:比如
threshold = ballSize * 0.7,否则小球贴边太“怂”,大球又容易误吸 - 归位目标不是绝对左/右边界,而是留出安全间距:
leftTarget = statusBarHeight + safeArea.left(iOS)、rightTarget = screenWidth - safeArea.right - ballSize(Android/iOS 底部安全区也要扣掉) - 动画必须用
requestAnimationFrame或setTimeout包一层再触发,否则@touchend里直接改x会被 movable-view 内部状态覆盖
菜单展开时,吸附逻辑要重新校准
悬浮球点击展开扇形菜单后,整体宽度变大(比如从 60rpx → 200rpx),但 movable-view 的 x 没变——结果就是菜单一半悬空、一半被切掉。这不是 UI 问题,是吸附计算没同步更新。
解决方案分两步:
- 菜单展开时,立刻重算当前
movable-view的有效活动范围:maxX = screenWidth - expandedWidth,并限制x不超界(否则拖拽会卡住) - 吸附判断也得用新宽度:
if (x maxX - threshold) { targetX = maxX } - 千万别在
@touchend里直接改x后立刻调uni.createAnimation——movable-view会拦截首次赋值。得用nextTick或setTimeout(() => {}, 0)延迟一帧再执行动画
nvue 页面才是 App 端真正全局悬浮的唯一解
H5 和小程序能靠 movable-area 撑住,但 App 端不行:普通 vue 页面的 movable-area 会被页面容器裁剪,切页就消失。只有 nvue 页面能挂到原生窗口层,实现真正的“全局”。
实操上必须满足这几点:
- 新建独立
ball.nvue,不进pages.json,也不挂路由;在App.vue的onLaunch里用uni.navigateTo({ url: '/pages/ball/ball' })启动,并立即uni.hideTabBar() - nvue 里不能用
v-if控制显隐,改用:style="{ opacity: show ? 1 : 0, transform: show ? 'scale(1)' : 'scale(0.8)' }"+transition - 拖拽限位必须用
uni.getSystemInfoSync()实时读取safeArea、statusBarHeight、screenHeight,iOS 和 Android 的底部安全区数值不同,硬编码必翻车 - Android 上
touchmove默认触发滚动,必须event.preventDefault();iOS 上则要关掉animation和box-shadow,否则动画掉帧严重
最易忽略的是:nvue 页面无法直接访问 Vue 实例或 Vuex,所有状态通信必须走 uni.$emit/uni.$on,且业务页面要在 onUnload 主动 uni.$off,否则重复绑定导致点击一次触发多次。


















