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


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 相关文章推荐
jquery 插件 任意位置浮动固定层
Dec 25 Javascript
JavaScript设置FieldSet展开与收缩
May 15 Javascript
JS下高效拼装字符串的几种方法比较与测试代码
Apr 15 Javascript
iframe子页面与父页面在同域或不同域下的js通信
May 07 Javascript
浅谈EasyUI中Treegrid节点的删除
Mar 01 Javascript
学习使用bootstrap基本控件(table、form、button)
Apr 12 Javascript
js中的闭包学习心得
Feb 06 Javascript
webstorm中配置Eslint的两种方式及差异比较详解
Oct 19 Javascript
JS中使用new Option()实现时间联动效果
Dec 10 Javascript
Vue插槽原理与用法详解
Mar 05 Javascript
微信小程序顶部导航栏可滑动并选中放大
Dec 05 Javascript
VueQuillEditor富文本上传图片(非base64)
Jun 03 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 字符串函数收集
2010/03/29 PHP
PHP FOR MYSQL 代码生成助手(根据Mysql里的字段自动生成类文件的)
2011/07/23 PHP
php更新修改excel中的内容实例代码
2014/02/26 PHP
将CMYK颜色值和RGB颜色相互转换的PHP代码
2014/07/28 PHP
Web开发者必备的12款超赞jQuery插件
2010/12/03 Javascript
JS onmousemove鼠标移动坐标接龙DIV效果实例
2013/12/16 Javascript
让table变成exls的示例代码
2014/03/24 Javascript
基于JS如何实现类似QQ好友头像hover时显示资料卡的效果(推荐)
2016/06/09 Javascript
jQuery 跨域访问解决原理案例详解
2016/07/09 Javascript
jQuery 获取页面li数组并删除不在数组中的key
2016/08/02 Javascript
原生js实现倒计时--2018
2017/02/21 Javascript
Angular之指令Directive用法详解
2017/03/01 Javascript
jQuery插件FusionWidgets实现的Cylinder图效果示例【附demo源码】
2017/03/23 jQuery
浅析Angular2子模块以及异步加载
2017/04/24 Javascript
详解webpack-dev-server的简单使用
2018/04/02 Javascript
vue 不使用select实现下拉框功能(推荐)
2018/05/17 Javascript
nodejs微信开发之授权登录+获取用户信息
2019/03/17 NodeJs
JS扁平化输出数组的2种方法解析
2019/09/17 Javascript
Vue 实现复制功能,不需要任何结构内容直接复制方式
2019/11/09 Javascript
基于vue 动态菜单 刷新空白问题的解决
2020/08/06 Javascript
vue实现禁止浏览器记住密码功能的示例代码
2021/02/03 Vue.js
python实现超简单端口转发的方法
2015/03/13 Python
使用Python编写一个最基础的代码解释器的要点解析
2016/07/12 Python
python模拟事件触发机制详解
2018/01/19 Python
Python爬虫设置代理IP的方法(爬虫技巧)
2018/03/04 Python
Python Unittest根据不同测试环境跳过用例的方法
2018/12/16 Python
python判断文件是否存在,不存在就创建一个的实例
2019/02/18 Python
python+selenium 鼠标事件操作方法
2019/08/24 Python
python enumerate内置函数用法总结
2020/01/07 Python
tensorflow实现读取模型中保存的值 tf.train.NewCheckpointReader
2020/02/10 Python
爱尔兰电子产品购物网站:Komplett.ie
2018/04/04 全球购物
来自南加州灵感的工作和娱乐服装:TravisMathew
2019/05/01 全球购物
文化宣传方案
2014/03/13 职场文书
幼儿园小班评语
2014/04/18 职场文书
大学生求职计划书
2014/04/30 职场文书
给公司的建议书范文
2014/05/13 职场文书