问题场景

所谓悬浮窗就是图中微信图标的按钮,采用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月都没有结果。

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

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

微信小程序中悬浮窗功能的实现(主要探讨和解决在原生组件上的拖动)的更多相关文章

  1. 微信小程序中的组件使用1

    不管是vue还是react中,都在强调组件思想,同样,在微信小程序中也是使用组件思想来实现页面复用的,下面就简单介绍一下微信小程序中的组件思想. 组件定义与使用 要使用组件,首先需要有组件页面和使用组 ...

  2. 全栈开发工程师微信小程序-中(下)

    全栈开发工程师微信小程序-中(下) 微信小程序视图层 wxml用于描述页面的结构,wxss用于描述页面的样式,组件用于视图的基本组成单元. // 绑定数据 index.wxml <view> ...

  3. 全栈开发工程师微信小程序-中(中)

    全栈开发工程师微信小程序-中(中) 开放能力 open-data 用于展示微信开放的数据 type 开放数据类型 open-gid 当 type="groupName" 时生效, ...

  4. 全栈开发工程师微信小程序-中

    全栈开发工程师微信小程序-中 多媒体及其他的组件 navigator 页面链接 target 在哪个目标上发生跳转,默认当前小程序,可选值self/miniProgram url 当前小程序内的跳转链 ...

  5. 微信小程序添加悬浮在线客服会话按钮

    微信为小程序提供客服消息能力,小程序用户可以方便快捷地与小程序服务提供方进行沟通,并且已经做成了组件的形式,直接就可以调用.客服会话按钮,用于在页面上显示一个客服会话按钮,用户点击该按钮后会进入客服会 ...

  6. ES6中Class的用法及在微信小程序中的应用实例

    1.ES6的基本用法 ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板.通过class关键字,可以定义类.基本上,ES6 的class可以看作只是一个语法糖,它的绝 ...

  7. 微信小程序仿朋友圈功能开发(发布、点赞、评论等功能)

    微信小程序仿朋友圈功能开发(发布.点赞.评论等功能) 1.项目分析 项目整体分为三个部分 发布 展示 详情页 graph LR 朋友圈发布 --内容发布--> 内容展示 内容展示 --点击展示卡 ...

  8. 微信小程序中的自定义组件

    微信小程序中的组件 前言 之前做小程序开发的时候,对于开发来说比较头疼的莫过于自定义组件了,当时官方对这方面的文档也只是寥寥几句,一笔带过而已,所以写起来真的是非常非常痛苦!! 好在微信小程序的库从 ...

  9. 微信小程序中路由跳转

    一.是什么 微信小程序拥有web网页和Application共同的特征,我们的页面都不是孤立存在的,而是通过和其他页面进行交互,来共同完成系统的功能 在微信小程序中,每个页面可以看成是一个pageMo ...

随机推荐

  1. 【设计模式】结构型04桥接模式(Bridge Pattern)

    学习地址:http://www.runoob.com/design-pattern/bridge-pattern.html 桥接模式(Bridge Pattern) 桥接模式(Bridge patte ...

  2. 模块化 require.js 入门教学(前端必看系列)

    在工作的时候总是会用到模块化开发,那接下来我就顺着这个问题来说一下什么是模块化 前端模块化 !!! JS 模块化提供给我们三种规范 分别就是 No.1 commonjs  这个其实也就代表了node. ...

  3. PATB 1028. 人口普查(20)

    1028. 人口普查(20) 注意特判合理人数为0,否则格式错误.很暴力的sort排序找出最大最小. 时间限制 200 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Stan ...

  4. 17 | 精益求精:聊聊提高GUI测试稳定性的关键技术

  5. 为什么建议大家使用 Linux 开发

    Linux 能用吗? 我身边还有些朋友对 linux 的印象似乎还停留在黑乎乎的命令行界面上.当我告诉他或者建议他使用 linux 时,会一脸惊讶的问我,那个怎么用(来开发或者日常使用)? Linux ...

  6. MySQL数据库修改字段的长度

    数据库版本:5.7.22 使用DDL语句:alter table 表名 modify 字段名 字符类型(长度) 例如: alter table db2.admin modify password );

  7. fis3前端工程构建配置入门教程

    一.前言 fis3是百度推出的一款前端工程构建工具,类似的还有webpack,gulp等工具:无论大家有没有使用过,从事前端行业应该都略知一二了,所以对于此类工具用干嘛的我这里就不做重复了. 其实对于 ...

  8. JAVA String类型的一些小操作

    String类型是否包含某个String类型的函数:源字符串.contains(包含字符串)  返回值为:boolean类型(true或false) String类型把某个字符替换成另一个字符:源字符 ...

  9. Oracle数据库常用的脚本命令(一)

    --连接数据库的命令connect,用于切换连接用户,简写形式conn--语法格式:conn 用户名/密码conn yanln/yanln --显示当前登录的用户show user --执行操作系统的 ...

  10. Akka-CQRS(16)- gRPC用JWT进行权限管理

    前面谈过gRPC的SSL/TLS安全机制,发现设置过程比较复杂:比如证书签名:需要服务端.客户端两头都设置等.想想实际上用JWT会更加便捷,而且更安全和功能强大,因为除JWT的加密签名之外还可以把私密 ...