问题场景

所谓悬浮窗就是图中微信图标的按钮,采用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. JavaWeb入门_模仿天猫整站Tmall_JavaEE实践项目

    Tmall_JavaEE 技术栈 Servlet + Jsp + Tomcat , 是Java Web入门非常好的练手项目 效果展示: 模仿天猫前台 模仿天猫后台 项目简介 关联项目 github - ...

  2. Mysql索引优化之索引的分类

    Mysql的历史 简单回顾一下Mysql的历史,Mysql 是一个关系型数据库管理系统,由瑞典 Mysql AB 公司开发,目前属于 Oracle 公司.关系型数据库​将数据保存在不同的表中,而不是将 ...

  3. react 项目全家桶构件流程

    资源:create-react-app.react.react-dom.redux.react-redux.redux-thunk.react-router-dom.antd-mobile/antd. ...

  4. Matplotlib快速入门

    Matplotlib 可能还有小伙伴不知道Matplotlib是什么,下面是维基百科的介绍. Matplotlib 是Python编程语言的一个绘图库及其数值数学扩展 NumPy.它为利用通用的图形用 ...

  5. cat more less 命令混用

    在Linux系统中有三种命令可以用来查阅全部的文件,分别是cat.more和less命令.它们查阅文件的使用方法也比较简单都是 命令 文件名 ,但是三者又有着区别. 1.cat命令可以一次显示整个文件 ...

  6. Mybatisの常见面试题

    Mybatis -面试问题 最近准备系统的学一下Mybatis,之前只有粗略的看了下,选了十个常见的面试题 1. #{}和${}的区别是什么? #{}是预编译处理,${}是字符串替换. Mybatis ...

  7. easyui datagrid 单元格 编辑时 事件 修改另一单元格

    //datagrid 列数据 $('#acc').datagrid({ columns : [ [ { field : 'fee_lend', title : '收费A', width : 100, ...

  8. Linux虚拟机怎么添加磁盘?

    一.VMware workstation菜单栏

  9. java三大集合遍历

    1. 场景描述 今天需要用到map集合遍历,一下子忘记咋写了,以前一般用map.get()直接获取值,很少遍历map,刚好总结下java中常用的几个集合-map,set,list遍历. 2. 解决方案 ...

  10. CDQZ集训DAY6 日记

    又炸了. 早上起来其他竞赛生也走了,食堂做饭做的挺潦草,但为什么四川烧麦的馅是米啊??!! 起来看题总觉得都似曾相识.第一题打完40分暴力后想拿莫队搞到70分,但发现能想到的莫队维护都是nsqrt(n ...