微信小程序中悬浮窗功能的实现代码


Posted in Javascript onAugust 02, 2019

问题场景

所谓悬浮窗就是图中微信图标的按钮,采用fixed定位,可拖动和点击。

这算是一个比较常见的实现场景了。

微信小程序中悬浮窗功能的实现代码

为什么要用cover-view做悬浮窗?原生组件出来背锅了~

最初我做悬浮窗用的不是cover-view,而是view。

这是简化的代码结构:

index.wxml:

<view class="move-view" style=" top:{{top}}px;left:{{left}}px;" bindtap="goToHome" catchtouchmove="setTouchMove">
 <image class="img" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=4294841024,3545417298&fm=179&app=42&f=PNG?w=56&h=56">
 </image>
</view>
<textarea placeholder='我是textarea组件,用来输入一些信息'></textarea>
<view>
 一大段test,占个位,表示下存在感
</view>

index.js:

Page({

 /**
 * 页面的初始数据
 */
 data: {
 left: 20,
 top: 250,
 isIos: true
 },
 /**
 * 拖拽移动
 */
 setTouchMove: function (e) {
 if (e.touches[0].clientX > 0 && e.touches[0].clientY > 0) {
  this.setData({
  left: e.touches[0].clientX - 30,
  top: e.touches[0].clientY - 30
  })
 } else {
  this.setData({
  left: 20, //默认显示位置 left距离
  top: 250 //默认显示位置 top距离
  })
 }
 },
 /**
 * 返回首页
 */
 goToHome: () => {
 wx.reLaunch({
  url: '/pages/index/index',
 })
 }
})

为什么要用cover-view呢?

因为页面上有个textarea组件,这个组件是原生组件,当悬浮窗移动到这个textarea组件上时,将无法继续拖动和点击。

如果悬浮窗一开始就定位在textarea上,那么就更惨了,一开始就不能点击和拖动了。

这个原因时因为微信小程序的原生组件层级高于非原生组件,不是你修改几下样式就能解决的问题。

这里就不讲什么原生组件了,如果想进一步了解,可以参考我之前写的一篇博客:微信小程序在ios下Echarts图表不能滑动的解决方案。

如果你的页面上面没有原生组件,那么像上面的代码一样用view做悬浮窗即可。

如果有,那么就可以跟着我继续踩坑,使用cover-view这个原生组件层级的组件来做悬浮窗。

安卓下的cover-view拖动起来,抖得不像帕金森,像是魔鬼的步伐

以下是我们修改为cover-view之后的代码:

<cover-view class="move-view" style=" top:{{top}}px;left:{{left}}px;" bindtap="goToHome" catchtouchmove="setTouchMove">
 <cover-image class="img" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=4294841024,3545417298&fm=179&app=42&f=PNG?w=56&h=56">
 </cover-image>
</cover-view>
<textarea placeholder='我是textarea组件,用来输入一些信息'></textarea>
<view>
 一大段test,占个位,表示下存在感
</view>

注意这里,我们的image也改为了cover-image,因为cover-view只支持嵌套 cover-view、cover-image,不过可在 cover-view 中使用 button。

这样虽然解决了可在原生组件上自由拖动点击的问题,但是在安卓上出现了一个很奇怪的现象,以至于我认为已经无法用抖动可以来形容了:

微信小程序中悬浮窗功能的实现代码

上图是就是我滑动这个悬浮窗之后的效果,我只是很缓慢地在移动手指,但是这个悬浮窗的表现简直就像一个受惊的兔子。

当我第一眼看见这个效果的时候一脸懵逼,我都不知道说什么好。

虽然在ios上cover-view移动起来表现良好,但是在安卓上拖动起来的表现简直没法看。

勉强能看的补丁方案

安卓上这么挫,还不如原来的呢。

所以来个补丁方案好了,在ios下用cover-view完美拖动,在安卓上用view先跑着。

<cover-view wx-if="{{isIos}}" class="move-view" style=" top:{{top}}px;left:{{left}}px;" bindtap="goToHome" catchtouchmove="setTouchMove">
 <cover-image class="img" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=4294841024,3545417298&fm=179&app=42&f=PNG?w=56&h=56">
 </cover-image>
</cover-view>
<view wx-if="{{!isIos}}" class="move-view" style=" top:{{top}}px;left:{{left}}px;" bindtap="goToHome" catchtouchmove="setTouchMove">
 <image class="img" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=4294841024,3545417298&fm=179&app=42&f=PNG?w=56&h=56">
 </image>
</view>
<textarea placeholder='我是textarea组件,用来输入一些信息'></textarea>
<view>
 一大段test,占个位,表示下存在感
</view>

当然少不了要在js里面加上这句代码:

onLoad: function (options) { 

wx.getSystemInfo({
 success: (res) => {
  if (res.platform == "android") {
  this.setData({
   isIos: false
  })
  }
 }
 })
}

不要忘记isIos默认为true哦。

反正ios环境下可以完美使用了,至于安卓下拖到textarea组件上没法再拖的问题,调整下悬浮框的初始位置就好了。

而且只要不是刻意移动到textarea组件上,拖动着悬浮框经过textarea组件也是没有问题的嘛。

像我这么聪明的用户还懂得滑动下面的页面来使悬浮窗移动到非原生组件的地方,这样就又可以拖动了嘛。

你又以为你的测试一定能发现这个问题?发现了又怎样,我已经尽力了,还给你整出这么多理论依据,足够你把锅牢牢地按在微信小程序官方的头上。

使用movable-view:仿佛发现了新大陆,结果发现这个还是个弟弟

甩锅是一定要甩锅的,但是段位要高。

所以要遍查官方文档,探讨一切可能性,以免甩锅的时候被打脸。

我们仔细观察小程序官方文档,发现还是有个专门用来拖动的组件叫movable-view

这个组件和cover-view摆放在一起仿佛很厉害的样子,紧接着我们在原生组件使用限制文档中发现了它并不是原生组件。

也就是说这个东西的层级一定还是低于咱们的textarea组件的。

虽然已经很确定这个东西没什么用了,但是最后还是试探一把,结果发现是个真弟弟,这里就不给出代码了。

我写这个弟弟方案放在这里的目的主要是为了不要浪费你的验证时间。

理论上行得通的方案:将拖动事件的捕获放在父级

现在我们确认的最优甩锅方案里,已经实现了功能和甩锅两不误。

那么作为一名有追求的技术人员,还是需要去探讨以下这个问题到底有没有完美的解决方案。

因为我最开始是把这个悬浮窗做成了一个组件,那么作为组件来讲,这个东西就只能做到这个地步了。

不过如果你是像我现在的例子一样直接做在了页面里,那么实现起来也不是说没有办法的。

我们将拖动的事件放在父级上就可以了,请看接下来的代码:

index.wxml:

<view bindtouchmove="setTouchMove">
 <view class="move-view" style=" top:{{top}}px;left:{{left}}px;" bindtap="goToHome">
  <image class="img" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=4294841024,3545417298&fm=179&app=42&f=PNG?w=56&h=56">
  </image>
 </view>
 <textarea placeholder='我是textarea组件,用来输入一些信息'></textarea>
 <view>
  一大段test,占个位,表示下存在感
 </view>
</view>

index.js:

Page({

 /**
 * 页面的初始数据
 */
 data: {
 left: 20,
 top: 250
 },

 /**
 * 拖拽移动
 */
 setTouchMove: function (e) {
 const MOVE_VIEW_RADIUS = 30 // 悬浮窗半径

 const touchPosX = e.touches[0].clientX
 const touchPosY = e.touches[0].clientY

 const moveViewCenterPosX = this.data.left + MOVE_VIEW_RADIUS
 const moveViewCenterPosY = this.data.top + MOVE_VIEW_RADIUS

 // 确保手指在悬浮窗上才可以移动
 if (Math.abs(moveViewCenterPosX - touchPosX) < MOVE_VIEW_RADIUS + 60 && Math.abs(moveViewCenterPosY - touchPosY) < MOVE_VIEW_RADIUS + 60) {
  if (touchPosX > 0 && touchPosY > 0) {
  this.setData({
   left: touchPosX - MOVE_VIEW_RADIUS,
   top: touchPosY - MOVE_VIEW_RADIUS
  })
  } else {
  this.setData({
   left: 20, // 默认显示位置 left距离
   top: 250 // 默认显示位置 top距离
  })
  }
 }
 },
 /**
 * 返回首页
 */
 goToHome: () => {
 wx.reLaunch({
  url: '/pages/index/index',
 })
 }
})

关键代码就是这块了:

// 确保手指在悬浮窗上才可以移动
if (Math.abs(moveViewCenterPosX - touchPosX) < MOVE_VIEW_RADIUS + 60 && Math.abs(moveViewCenterPosY - touchPosY) < MOVE_VIEW_RADIUS + 60) {

}

只要确保手指在悬浮窗的范围内就可以触发移动了,这里的60是为了确保你的手指太大,或者移动得比较快时超出了悬浮窗区域依然可以触发拖动,这个可以自己设定数值。

这个方案在理论上很合理,并且还加上了60这个缓冲区域,但是实际在拖动的时候你仍然会面临下面三个问题:

1.如果悬浮窗下方有滚动区域,那么拖动的时候就会滚动页面,效果会显得比较奇怪。
2.实际移动没法移动太顺畅,只能拖着悬浮窗亦步亦趋,要不然很容易超过60这个缓冲区域,导致拖动不继续触发。
2.如果将缓冲区域设置过大,那么又会出现一种比较奇怪的场景:明明不准备拖动悬浮窗,只是准备滑动页面,悬浮窗却跳到自己手指这里了。

进阶解决方案:禁止冒泡的拖动 + 理论方案

这个解决方案基于我们的最初方案,并且使用我们的理论方案作为补充。

先上代码:

index.wxml:
<view bindtouchmove="handleSetMoveViewPos">
 <view class="move-view" style=" top:{{top}}px;left:{{left}}px;" bindtap="goToHome" catchtouchmove="handleTouchMove">
  <image class="img" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=4294841024,3545417298&fm=179&app=42&f=PNG?w=56&h=56">
  </image>
 </view>
 <textarea placeholder='我是textarea组件,用来输入一些信息'></textarea>
 <view>
  一大段test,占个位,表示下存在感
 </view>
</view>

index.js:
Page({
 /**
 * 页面的初始数据
 */
 data: {
 left: 20,
 top: 250
 },
 /**
 * 拖拽移动(补丁)
 */
 handleSetMoveViewPos: function (e) {
 const MOVE_VIEW_RADIUS = 30 // 悬浮窗半径

 const touchPosX = e.touches[0].clientX
 const touchPosY = e.touches[0].clientY

 const moveViewCenterPosX = this.data.left + MOVE_VIEW_RADIUS
 const moveViewCenterPosY = this.data.top + MOVE_VIEW_RADIUS

 // 确保手指在悬浮窗上才可以移动
 if (Math.abs(moveViewCenterPosX - touchPosX) < MOVE_VIEW_RADIUS+30 && Math.abs(moveViewCenterPosY - touchPosY) < MOVE_VIEW_RADIUS+30 ) {
  if (touchPosX > 0 && touchPosY > 0) {
  this.setData({
   left: touchPosX - MOVE_VIEW_RADIUS,
   top: touchPosY - MOVE_VIEW_RADIUS
  })
  } else {
  this.setData({
   left: 20, // 默认显示位置 left距离
   top: 250 // 默认显示位置 top距离
  })
  }
 }
 },
 /**
 * 拖拽移动
 */
 handleTouchMove: function (e) {
 const MOVE_VIEW_RADIUS = 30 // 悬浮窗半径

 const touchPosX = e.touches[0].clientX
 const touchPosY = e.touches[0].clientY

 if (touchPosX > 0 && touchPosY > 0) {
  this.setData({
  left: touchPosX - MOVE_VIEW_RADIUS,
  top: touchPosY - MOVE_VIEW_RADIUS
  })
 } else {
  this.setData({
  left: 20, //默认显示位置 left距离
  top: 250 //默认显示位置 top距离
  })
 }
 },
 /**
 * 返回首页
 */
 goToHome: () => {
 wx.reLaunch({
  url: '/pages/index/index',
 })
 }
})

这个方案的核心点在于:catchtouchmove="handleTouchMove"

当我们正常拖动悬浮窗时,通过catchtouchmove,我们可以捕获在悬浮窗上的滑动事件,并且不冒泡到父元素,那么我们绑在父层级的滑动事件就不会触发。

而当我们拖动在原生组件之上的悬浮窗时,因为点不到这个悬浮窗,就不会触发handleTouchMove函数,只会触发绑定在父元素上的handleSetMoveViewPos函数。

另外如果你细心的话,就会发现在handleSetMoveViewPos函数这里我缩小了那个60的缓冲区域为30,这样做的目的是因为触发这个函数只会在原生组件上,所以多番权衡距离之后,尽量避免近距离滑动操作就触发拖动悬浮框。

通过我们的方案,我们可以在非原生组件上自由拖动,在原生组件上比较顺畅地拖动。

本来我是准备将这个方案作为最终方案的,但是ios下,悬浮窗在原生组件上时,在父元素上的滑动事件竟然不触发。

棋差一招,棋差一招啊!

最终解决方案:更多的补丁,更多的快乐

这个最终解决方案,当然是把我们之前所有的补丁方案全部结合起来。

代码如下:

index.wxml:
<view bindtouchmove="handleSetMoveViewPos">
 <view wx-if="{{!isIos}}" class="move-view" style=" top:{{top}}px;left:{{left}}px;" bindtap="goToHome" catchtouchmove="handleTouchMove">
  <image class="img" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=4294841024,3545417298&fm=179&app=42&f=PNG?w=56&h=56">
  </image>
 </view>
 <cover-view wx-if="{{isIos}}" class="move-view" style=" top:{{top}}px;left:{{left}}px;" bindtap="goToHome" catchtouchmove="handleTouchMove">
  <cover-image class="img" src="https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=4294841024,3545417298&fm=179&app=42&f=PNG?w=56&h=56">
  </cover-image>
 </cover-view>
 <textarea placeholder='我是textarea组件,用来输入一些信息'></textarea>
 <view>
  一大段test,占个位,表示下存在感
 </view>
</view>

index.js:
Page({

 /**
 * 页面的初始数据
 */
 data: {
 left: 20,
 top: 250,
 isIos: true
 },

 /**
 * 生命周期函数--监听页面加载
 */
 onLoad: function (options) {
 wx.getSystemInfo({
  success: (res) => {
  if (res.platform == "android") {
   this.setData({
   isIos: false
   })
  }
  }
 })
 },

 /**
 * 拖拽移动(补丁)
 */
 handleSetMoveViewPos: function (e) {
 // 在ios下永远都不会走这个方案,以免引起无用的计算
 if (!ios) {
  const MOVE_VIEW_RADIUS = 30 // 悬浮窗半径

  const touchPosX = e.touches[0].clientX
  const touchPosY = e.touches[0].clientY

  const moveViewCenterPosX = this.data.left + MOVE_VIEW_RADIUS
  const moveViewCenterPosY = this.data.top + MOVE_VIEW_RADIUS

  // 确保手指在悬浮窗上才可以移动
  if (Math.abs(moveViewCenterPosX - touchPosX) < MOVE_VIEW_RADIUS && Math.abs(moveViewCenterPosY - touchPosY) < MOVE_VIEW_RADIUS) {
  if (touchPosX > 0 && touchPosY > 0) {
   this.setData({
   left: touchPosX - MOVE_VIEW_RADIUS,
   top: touchPosY - MOVE_VIEW_RADIUS
   })
  } else {
   this.setData({
   left: 20, // 默认显示位置 left距离
   top: 250 // 默认显示位置 top距离
   })
  }
  }
 }
 },
 /**
 * 拖拽移动
 */
 handleTouchMove: function (e) {
 const MOVE_VIEW_RADIUS = 30 // 悬浮窗半径

 const touchPosX = e.touches[0].clientX
 const touchPosY = e.touches[0].clientY

 if (touchPosX > 0 && touchPosY > 0) {
  this.setData({
  left: touchPosX - MOVE_VIEW_RADIUS,
  top: touchPosY - MOVE_VIEW_RADIUS
  })
 } else {
  this.setData({
  left: 20, //默认显示位置 left距离
  top: 250 //默认显示位置 top距离
  })
 }
 },
 /**
 * 返回首页
 */
 goToHome: () => {
 wx.reLaunch({
  url: '/pages/index/index',
 })
 }
})

这个最终解决方案在ios下直接使用cover-view来做悬浮窗,而在android的非原生组件上移动时,使用view来做悬浮窗,不冒泡滑动事件,在原生组件上移动时捕获冒泡的滑动事件来继续移动操作。

总结

虽然问题解决了,但是这仍然只是一个补丁方案。

最好的方式依然是微信小程序官方能修复cover-view在安卓移动时的BUG,但是我发现最早有人反馈这个问题是在2018年11月,到了现在2019年8月都没有结果。

如果不是微信小程序的官方态度有问题,那么只能说明这个问题的解决确实有难度或者优先级并不高,无论是哪一种,暂时都还是得用补丁方案。

这个方案并没有那么完美,他在一些边界的衔接上面可能还是会存在一些小问题,但它至少可用,并且应该是大多数用户可以接受的。

以上所述是小编给大家介绍的微信小程序中悬浮窗功能的实现 ,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
如何使Chrome控制台支持多行js模式——意外发现
Jun 13 Javascript
jquery中获得元素尺寸和坐标的方法整理
May 18 Javascript
一道优雅面试题分析js中fn()和return fn()的区别
Jul 05 Javascript
JS及PHP代码编写八大排序算法
Jul 12 Javascript
EasyUi 打开对话框后控件赋值及赋值后不显示的问题解决办法
Jan 19 Javascript
jQuery实现的简单悬浮层功能完整实例
Jan 23 Javascript
原生JS实现层叠轮播图
May 17 Javascript
微信小程序checkbox组件使用详解
Jan 31 Javascript
在Vue中获取组件声明时的name属性方法
Sep 12 Javascript
AngularJS 监听变量变化的实现方法
Oct 09 Javascript
说说如何使用Vuex进行状态管理(小结)
Apr 14 Javascript
详解JavaScript中的链式调用
Nov 27 Javascript
微信小程序的授权实现过程解析
Aug 02 #Javascript
jQuery实现input[type=file]多图预览上传删除等功能
Aug 02 #jQuery
用 js 写一个 js 解释器过程详解
Aug 02 #Javascript
vue实现登录页面的验证码以及验证过程解析(面向新手)
Aug 02 #Javascript
详解element-ui中el-select的默认选择项问题
Aug 02 #Javascript
jQuery实现checkbox全选、反选及删除等操作的方法详解
Aug 02 #jQuery
150行Node.js实现的dns代理工具
Aug 02 #Javascript
You might like
关于拼配咖啡,你要知道
2021/03/03 咖啡文化
用PHP读取IMAP邮件
2006/10/09 PHP
浅谈web上存漏洞及原理分析、防范方法(安全文件上存方法)
2013/06/29 PHP
PHP队列用法实例
2014/11/05 PHP
通过修改配置真正解决php文件上传大小限制问题(nginx+php)
2015/09/23 PHP
php中青蛙跳台阶的问题解决方法
2018/10/14 PHP
一个原生的用户等级的进度条
2010/07/03 Javascript
javascript innerHTML使用分析
2010/12/03 Javascript
JS获取节点的兄弟,父级,子级元素的方法
2014/01/09 Javascript
js获取指定的cookie的具体实现
2014/02/20 Javascript
浅谈jQuery事件绑定原理
2015/01/02 Javascript
嵌入式iframe子页面与父页面js通信的方法
2015/01/20 Javascript
BOOTSTRAP时间控件显示在模态框下面的bug修复
2015/02/05 Javascript
JS简单编号生成器实现方法(附demo源码下载)
2016/04/05 Javascript
AngularJS入门教程之双向绑定详解
2016/08/18 Javascript
使用JS 插件qrcode.js生成二维码功能
2017/02/20 Javascript
validationEngine 表单验证插件使用实例代码
2017/06/15 Javascript
js排序与重组的实例讲解
2017/08/28 Javascript
angularJS自定义directive之带参方法传递详解
2018/10/09 Javascript
通过JS运行机制的角度说说作用域
2019/03/12 Javascript
vue router 跳转时打开新页面的示例方法
2019/07/28 Javascript
[02:06]DOTA2英雄基础教程 暗影萨满
2013/12/16 DOTA
[27:28]Ti4 冒泡赛第二天 iG vs NEWBEE 1
2014/07/15 DOTA
Python的Flask框架的简介和安装方法
2015/11/13 Python
最近Python有点火? 给你7个学习它的理由!
2017/06/26 Python
对python dataframe逻辑取值的方法详解
2019/01/30 Python
Python实现随机取一个矩阵数组的某几行
2019/11/26 Python
Python 寻找局部最高点的实现
2019/12/05 Python
对Keras中predict()方法和predict_classes()方法的区别说明
2020/06/09 Python
深入浅析pycharm中 Make available to all projects的含义
2020/09/15 Python
css3实现书本翻页效果的示例代码
2021/03/08 HTML / CSS
Sixt美国租车:高端豪华车型自驾体验
2017/09/02 全球购物
得到Class的三个过程是什么
2012/08/10 面试题
公司面试感谢信
2014/02/01 职场文书
校庆标语集锦
2014/06/25 职场文书
小学校园广播稿(3篇)
2014/09/19 职场文书