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


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 相关文章推荐
JavaScript使用技巧精萃[代码非常实用]
Nov 21 Javascript
[原创]IE view-source 无法查看看源码 JavaScript看网页源码
Jul 19 Javascript
javascript对数组的常用操作代码 数组方法总汇
Jan 27 Javascript
javascript工具库代码
Mar 29 Javascript
jquery无刷新验证邮箱地址实现实例
Feb 19 Javascript
浅析javascript操作 cookie对象
Dec 26 Javascript
jQuery获取标签文本内容和html内容的方法
Mar 27 Javascript
使用vue.js开发时一些注意事项
Apr 27 Javascript
vue 实现左右拖拽元素并且不超过他的父元素的宽度
Nov 30 Javascript
JS中比Switch...Case更优雅的多条件判断写法
Sep 05 Javascript
Vue.js仿Select下拉框效果
Feb 18 Javascript
在react中使用vue的状态管理的方法示例
May 02 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
一个php作的文本留言本的例子(四)
2006/10/09 PHP
php include的妙用,实现路径加密
2008/07/29 PHP
php中获取远程客户端的真实ip地址的方法
2011/08/03 PHP
php中\r \r\n \t的区别示例介绍
2014/02/08 PHP
雄兵连第三季海报曝光,艾妮熙德成主角,蔷薇新造型
2021/03/09 国漫
javaScript 关闭浏览器 (不弹出提示框)
2010/01/31 Javascript
在js文件中如何获取basePath处理js路径问题
2013/07/10 Javascript
JS小功能(setInterval实现图片效果显示时间)实例代码
2013/11/28 Javascript
jquery ajax传递中文参数乱码问题及解决方法说明
2014/02/07 Javascript
JavaScript charCodeAt方法入门实例(用于取得指定位置字符的Unicode编码)
2014/10/17 Javascript
JavaScript获取表格(table)当前行的值、删除行、增加行
2015/07/03 Javascript
JS封装的自动创建表格的实现代码
2016/06/15 Javascript
一道优雅面试题分析js中fn()和return fn()的区别
2016/07/05 Javascript
从零学习node.js之搭建http服务器(二)
2017/02/21 Javascript
在 Angular 中实现搜索关键字高亮示例
2017/03/21 Javascript
nodejs实现邮件发送服务实例分享
2017/03/29 NodeJs
原生JS实现移动端web轮播图详解(结合Tween算法造轮子)
2017/09/10 Javascript
js实现手机web图片左右滑动效果
2017/12/29 Javascript
vue打包使用Nginx代理解决跨域问题
2018/08/27 Javascript
layui的表单验证支持ajax判断用户名是否重复的实例
2019/09/06 Javascript
NodeJS http模块用法示例【创建web服务器/客户端】
2019/11/05 NodeJs
[58:11]守擂赛第二周擂主赛 DeMonsTer vs Leopard
2020/04/28 DOTA
python实现数通设备端口监控示例
2014/04/02 Python
Python3.5 处理文本txt,删除不需要的行方法
2018/12/10 Python
解决python3 HTMLTestRunner测试报告中文乱码的问题
2018/12/17 Python
为何人工智能(AI)首选Python?读完这篇文章你就知道了(推荐)
2019/04/06 Python
opencv设置采集视频分辨率方式
2019/12/10 Python
意大利在线药房:Farmacia Loreto Gallo
2019/08/09 全球购物
小学后勤管理制度
2014/01/14 职场文书
本科毕业生求职自荐信
2014/04/09 职场文书
作文评语集锦大全
2014/04/23 职场文书
法定代表人授权委托书范文
2014/09/22 职场文书
公务员群众路线心得体会
2014/11/03 职场文书
2015年化妆品销售工作总结
2015/05/11 职场文书
办公用品管理制度
2015/08/04 职场文书
《童年》读后感(三篇)
2019/08/27 职场文书