在小程序里实现一个知乎浮动按钮
发布于 3 年前 作者 guiying40 670 次浏览 来自 分享

在小程序里实现一个知乎浮动按钮

前言

逛知乎App,点进一个回答,有一个很好用的 浮动的按钮,可以点击到下一个回答。这个按钮有什么特点呢?

  1. 点击触发下一篇 width y轴过渡动画
  2. 可拖动,左右轴吸附,拖动有安全范围,能记忆位置
  3. 可变形,有 tooltip

那么我们如何在小程序里实现这种 App 类似的效果呢?

思路分解

  1. 下一篇过渡动画与点击可以依靠 css + js 解决
  2. 拖动,吸附,安全范围 movable-area + movable-view + js 解决
  3. 变形加 tooltip , css + 预置弹出解决

这样分析下来,其实就第二点难度稍微大一些。

可拖动

h5 中直接就可以使用 css + touch events,来实现拖动的效果。小程序也已经有了现成的 movable-areamovable-view 组件来实现这样的功能。

其中 movable-area 为内部 movable-view 的可移动区域。
movable-view 必须在 movable-area 组件中,且必须是直接子节点,否则不能移动。

这里有官方的文档和demo , 通过__预览__还是很容易理解这2个组件的运作方式的。

其中对于 movable-view 来说,在不考虑 out-of-bounds 的情况下,view 是只能在 area 的范围内移动的。

movable-area,movable-view 都必须设置 widthheight属性,不设置默认为10px

设置安全范围

movable-view 默认为绝对定位,topleft属性为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-fullw-full,把 movable-area 的大小撑开。

最后再通过对外层 fixed 遮罩层的大小做限制 (left-4 right-4 top-4 bottom-4)。来达到对浮动按钮拖动区域进行限制的目的。

如图所示:

左右侧吸附

movable-view 天生是带动画效果(animation)的,但是这个动画的触发机制,是和 inputfocus 获取焦点很像。也就是说 movable-view 组件的x,y属性本身必须发生变化,才能触发动画。

举个例子, inputfocus属性 默认为 false, 在设置为 true 时,会获取焦点。但是当 focus 本身为 true 时,再去 setDatatrue 是没有作用的,因为控件本身并没有监测到属性值的 变化

同样,movable-viewx(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)任务。同时保证 这个宏任务 要在 防抖同步坐标 这个过程完成后,进行触发。

这意味着我们 setTimeoutdelay 必须大于 debouncewait 时间!

比如之前我们的 debounce 设置为 100ms ,现在 setTimeoutdelay 设置为 250ms。这样做是为了保证偏移动画的位置准确性。

向左走,向右走

接下来,我们把安全区域分为左右 2 边。用户在左边松手,吸附在左轴,右边亦然。

movable-viewx(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)

效果

见作者博客小程序 破冰客 文章详情页面

3 回复

错误,退出公公号

安全范围还是可以考虑再改改,考虑一下底部 Home Indicator 对浮动按钮的影响

博客小程序是用 towxml 做的吗?

——我是一条小尾巴🗯️

回到顶部