在小程序里实现一个知乎浮动按钮
前言
逛知乎App,点进一个回答,有一个很好用的 浮动
的按钮,可以点击到下一个回答。这个按钮有什么特点呢?
- 点击触发下一篇 width y轴过渡动画
- 可拖动,左右轴吸附,拖动有安全范围,能记忆位置
- 可变形,有
tooltip
那么我们如何在小程序里实现这种 App
类似的效果呢?
思路分解
- 下一篇过渡动画与点击可以依靠
css
+js
解决 - 拖动,吸附,安全范围
movable-area
+movable-view
+js
解决 - 变形加
tooltip
,css
+ 预置弹出解决
这样分析下来,其实就第二点难度稍微大一些。
可拖动
h5
中直接就可以使用 css
+ touch events
,来实现拖动的效果。小程序也已经有了现成的 movable-area
和 movable-view
组件来实现这样的功能。
其中 movable-area
为内部 movable-view
的可移动区域。
movable-view
必须在 movable-area
组件中,且必须是直接子节点,否则不能移动。
这里有官方的文档和demo , 通过预览还是很容易理解这2个组件的运作方式的。
其中对于 movable-view
来说,在不考虑 out-of-bounds
的情况下,view
是只能在 area
的范围内移动的。
movable-area
,movable-view
都必须设置width
和height
属性,不设置默认为10px
。
设置安全范围
movable-view
默认为绝对定位,top
和left
属性为0px。
而 movable-area
默认为相对定位,这意味着我们完全可以这么写:
<view
class="pointer-events-none fixed z-50 left-4 right-4 top-4 bottom-4">
<movable-area class="h-full w-full">
<movable-view
direction="all"
@change="fabSync"
@touchend="touchend"
:x="btn.x"
:y="btn.y"
class="w-10 h-10"
>
</movable-view>
</movable-area>
</view>
上述这段 wxml
中,我们在页面中套上了一个 fixed
的遮罩层,通过设置 z-index
, 将它的层级提高,再通过 pointer-events-none
,来保证交互事件的穿透。最后通过内部的 h-full
和 w-full
,把 movable-area
的大小撑开。
最后再通过对外层 fixed
遮罩层的大小做限制 (left-4 right-4 top-4 bottom-4
)。来达到对浮动按钮拖动区域进行限制的目的。
如图所示:
左右侧吸附
movable-view
天生是带动画效果(animation
)的,但是这个动画的触发机制,是和 input
的 focus
获取焦点很像。也就是说 movable-view
组件的x
,y
属性本身必须发生变化,才能触发动画。
举个例子, input
的 focus
属性 默认为 false
, 在设置为 true
时,会获取焦点。但是当 focus
本身为 true
时,再去 setData
为 true
是没有作用的,因为控件本身并没有监测到属性值的 变化。
同样,movable-view
的 x(y)
也需要监测这样的 变化
同时出于x
轴吸附动画的考量,我们不得不进行位置信息的同步。
不然就会出现,拖动按钮松开后,触发吸附行为时,按钮先闪现到初始位置(比如坐标 (0,0)
) ,然后再播放动画的情况。
防抖的同步
我们知道,对 setData
的滥用,很容易造成性能瓶颈。针对高频触发的 touchmove
, bindchange
or onPageScroll
这类,我们往往会加一层 防抖(debounce)
处理,来减小频繁更新的影响。
此时就可以给同步方法套一层 壳
:
debounce(function (x, y, source) {
this.btn.x = x
this.btn.y = y
}, 100),
来保证高频的拖动时,减少 setData
的调用。
边界吸附
这个经过前面的分析,这个行为已经被简化成了一个属性变化的问题了。
吸附这个行为往往发生在 touchend
后的几百毫秒内。所以我们可以在 touchend
中创建一个宏(macro)任务。同时保证 这个宏任务 要在 防抖同步坐标
这个过程完成后,进行触发。
这意味着我们 setTimeout
的 delay
必须大于 debounce
的 wait
时间!
比如之前我们的 debounce
设置为 100ms
,现在 setTimeout
的 delay
设置为 250ms
。这样做是为了保证偏移动画的位置准确性。
向左走,向右走
接下来,我们把安全区域分为左右 2 边。用户在左边松手,吸附在左轴,右边亦然。
movable-view
的 x(y)
是从 view
左上角开始算起的,这意味着我们需要把 view
本身的大小也考虑进去,才能做出精确的吸附行为。不然就会出现,用户把按钮拖到了,屏幕中线靠右的位置,却还是往左边吸附的奇葩行为。
此时我们就要去计算,这个中间线坐标究竟在哪?
以 px
单位为例,这个 x
坐标往往是在
const mid = (windowWidth - viewWidth - leftEdgeWidth - rightEdgeWidth) / 2
// `y` 轴同理
也就是说,用户拖动行为结束并同步坐标后,假如此时的 x
> mid
,则吸附在右侧,反之则吸附在左侧。
记忆位置
记忆位置这个可以在吸附行为完成后,单独创建一个任务,和 localstorage
进行同步,此处不再叙述。
大致实现
setTimeout(() => {
if (
this.btn.x >
(windowWidth - btnWidth - edgeWidth * 2) / 2
) {
this.btn.x = rightEdge
} else {
this.btn.x = 0 // leftEdge
}
setTimeout(() => {
ls.sync('float-btn-position',this.btn)
}, 0)
}, 250)
效果
见作者博客小程序 破冰客 文章详情页面
设置point-events: none; 难道不会导致拖拽失效吗?
源码打不开了 大佬能再发一份吗?
安全范围还是可以考虑再改改,考虑一下底部 Home Indicator 对浮动按钮的影响
博客小程序是用 towxml 做的吗?
——我是一条小尾巴🗯️
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);