微信小程序中懸浮窗功能的實(shí)現(xiàn)代碼
問題場景
所謂懸浮窗就是圖中微信圖標(biāo)的按鈕,采用fixed定位,可拖動和點(diǎn)擊。
這算是一個比較常見的實(shí)現(xiàn)場景了。

為什么要用cover-view做懸浮窗?原生組件出來背鍋了~
最初我做懸浮窗用的不是cover-view,而是view。
這是簡化的代碼結(jié)構(gòu):
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({
/**
* 頁面的初始數(shù)據(jù)
*/
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, //默認(rèn)顯示位置 left距離
top: 250 //默認(rèn)顯示位置 top距離
})
}
},
/**
* 返回首頁
*/
goToHome: () => {
wx.reLaunch({
url: '/pages/index/index',
})
}
})
為什么要用cover-view呢?
因?yàn)轫撁嫔嫌袀€textarea組件,這個組件是原生組件,當(dāng)懸浮窗移動到這個textarea組件上時,將無法繼續(xù)拖動和點(diǎn)擊。
如果懸浮窗一開始就定位在textarea上,那么就更慘了,一開始就不能點(diǎn)擊和拖動了。
這個原因時因?yàn)槲⑿判〕绦虻脑M件層級高于非原生組件,不是你修改幾下樣式就能解決的問題。
這里就不講什么原生組件了,如果想進(jìn)一步了解,可以參考我之前寫的一篇博客:微信小程序在ios下Echarts圖表不能滑動的解決方案。
如果你的頁面上面沒有原生組件,那么像上面的代碼一樣用view做懸浮窗即可。
如果有,那么就可以跟著我繼續(xù)踩坑,使用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,因?yàn)閏over-view只支持嵌套 cover-view、cover-image,不過可在 cover-view 中使用 button。
這樣雖然解決了可在原生組件上自由拖動點(diǎn)擊的問題,但是在安卓上出現(xiàn)了一個很奇怪的現(xiàn)象,以至于我認(rèn)為已經(jīng)無法用抖動可以來形容了:

上圖是就是我滑動這個懸浮窗之后的效果,我只是很緩慢地在移動手指,但是這個懸浮窗的表現(xiàn)簡直就像一個受驚的兔子。
當(dāng)我第一眼看見這個效果的時候一臉懵逼,我都不知道說什么好。
雖然在ios上cover-view移動起來表現(xiàn)良好,但是在安卓上拖動起來的表現(xiàn)簡直沒法看。
勉強(qiáng)能看的補(bǔ)丁方案
安卓上這么挫,還不如原來的呢。
所以來個補(bǔ)丁方案好了,在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>
當(dāng)然少不了要在js里面加上這句代碼:
onLoad: function (options) {
wx.getSystemInfo({
success: (res) => {
if (res.platform == "android") {
this.setData({
isIos: false
})
}
}
})
}
不要忘記isIos默認(rèn)為true哦。
反正ios環(huán)境下可以完美使用了,至于安卓下拖到textarea組件上沒法再拖的問題,調(diào)整下懸浮框的初始位置就好了。
而且只要不是刻意移動到textarea組件上,拖動著懸浮框經(jīng)過textarea組件也是沒有問題的嘛。
像我這么聰明的用戶還懂得滑動下面的頁面來使懸浮窗移動到非原生組件的地方,這樣就又可以拖動了嘛。
你又以為你的測試一定能發(fā)現(xiàn)這個問題?發(fā)現(xiàn)了又怎樣,我已經(jīng)盡力了,還給你整出這么多理論依據(jù),足夠你把鍋牢牢地按在微信小程序官方的頭上。
使用movable-view:仿佛發(fā)現(xiàn)了新大陸,結(jié)果發(fā)現(xiàn)這個還是個弟弟
甩鍋是一定要甩鍋的,但是段位要高。
所以要遍查官方文檔,探討一切可能性,以免甩鍋的時候被打臉。
我們仔細(xì)觀察小程序官方文檔,發(fā)現(xiàn)還是有個專門用來拖動的組件叫movable-view。
這個組件和cover-view擺放在一起仿佛很厲害的樣子,緊接著我們在原生組件使用限制文檔中發(fā)現(xiàn)了它并不是原生組件。
也就是說這個東西的層級一定還是低于咱們的textarea組件的。
雖然已經(jīng)很確定這個東西沒什么用了,但是最后還是試探一把,結(jié)果發(fā)現(xiàn)是個真弟弟,這里就不給出代碼了。
我寫這個弟弟方案放在這里的目的主要是為了不要浪費(fèi)你的驗(yàn)證時間。
理論上行得通的方案:將拖動事件的捕獲放在父級
現(xiàn)在我們確認(rèn)的最優(yōu)甩鍋方案里,已經(jīng)實(shí)現(xiàn)了功能和甩鍋兩不誤。
那么作為一名有追求的技術(shù)人員,還是需要去探討以下這個問題到底有沒有完美的解決方案。
因?yàn)槲易铋_始是把這個懸浮窗做成了一個組件,那么作為組件來講,這個東西就只能做到這個地步了。
不過如果你是像我現(xiàn)在的例子一樣直接做在了頁面里,那么實(shí)現(xiàn)起來也不是說沒有辦法的。
我們將拖動的事件放在父級上就可以了,請看接下來的代碼:
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({
/**
* 頁面的初始數(shù)據(jù)
*/
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, // 默認(rèn)顯示位置 left距離
top: 250 // 默認(rèn)顯示位置 top距離
})
}
}
},
/**
* 返回首頁
*/
goToHome: () => {
wx.reLaunch({
url: '/pages/index/index',
})
}
})
關(guān)鍵代碼就是這塊了:
// 確保手指在懸浮窗上才可以移動
if (Math.abs(moveViewCenterPosX - touchPosX) < MOVE_VIEW_RADIUS + 60 && Math.abs(moveViewCenterPosY - touchPosY) < MOVE_VIEW_RADIUS + 60) {
}
只要確保手指在懸浮窗的范圍內(nèi)就可以觸發(fā)移動了,這里的60是為了確保你的手指太大,或者移動得比較快時超出了懸浮窗區(qū)域依然可以觸發(fā)拖動,這個可以自己設(shè)定數(shù)值。
這個方案在理論上很合理,并且還加上了60這個緩沖區(qū)域,但是實(shí)際在拖動的時候你仍然會面臨下面三個問題:
1.如果懸浮窗下方有滾動區(qū)域,那么拖動的時候就會滾動頁面,效果會顯得比較奇怪。
2.實(shí)際移動沒法移動太順暢,只能拖著懸浮窗亦步亦趨,要不然很容易超過60這個緩沖區(qū)域,導(dǎo)致拖動不繼續(xù)觸發(fā)。
2.如果將緩沖區(qū)域設(shè)置過大,那么又會出現(xiàn)一種比較奇怪的場景:明明不準(zhǔn)備拖動懸浮窗,只是準(zhǔn)備滑動頁面,懸浮窗卻跳到自己手指這里了。
進(jìn)階解決方案:禁止冒泡的拖動 + 理論方案
這個解決方案基于我們的最初方案,并且使用我們的理論方案作為補(bǔ)充。
先上代碼:
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({
/**
* 頁面的初始數(shù)據(jù)
*/
data: {
left: 20,
top: 250
},
/**
* 拖拽移動(補(bǔ)丁)
*/
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, // 默認(rèn)顯示位置 left距離
top: 250 // 默認(rèn)顯示位置 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, //默認(rèn)顯示位置 left距離
top: 250 //默認(rèn)顯示位置 top距離
})
}
},
/**
* 返回首頁
*/
goToHome: () => {
wx.reLaunch({
url: '/pages/index/index',
})
}
})
這個方案的核心點(diǎn)在于:catchtouchmove="handleTouchMove" 。
當(dāng)我們正常拖動懸浮窗時,通過catchtouchmove,我們可以捕獲在懸浮窗上的滑動事件,并且不冒泡到父元素,那么我們綁在父層級的滑動事件就不會觸發(fā)。
而當(dāng)我們拖動在原生組件之上的懸浮窗時,因?yàn)辄c(diǎn)不到這個懸浮窗,就不會觸發(fā)handleTouchMove函數(shù),只會觸發(fā)綁定在父元素上的handleSetMoveViewPos函數(shù)。
另外如果你細(xì)心的話,就會發(fā)現(xiàn)在handleSetMoveViewPos函數(shù)這里我縮小了那個60的緩沖區(qū)域?yàn)?0,這樣做的目的是因?yàn)橛|發(fā)這個函數(shù)只會在原生組件上,所以多番權(quán)衡距離之后,盡量避免近距離滑動操作就觸發(fā)拖動懸浮框。
通過我們的方案,我們可以在非原生組件上自由拖動,在原生組件上比較順暢地拖動。
本來我是準(zhǔn)備將這個方案作為最終方案的,但是ios下,懸浮窗在原生組件上時,在父元素上的滑動事件竟然不觸發(fā)。
棋差一招,棋差一招啊!
最終解決方案:更多的補(bǔ)丁,更多的快樂
這個最終解決方案,當(dāng)然是把我們之前所有的補(bǔ)丁方案全部結(jié)合起來。
代碼如下:
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({
/**
* 頁面的初始數(shù)據(jù)
*/
data: {
left: 20,
top: 250,
isIos: true
},
/**
* 生命周期函數(shù)--監(jiān)聽頁面加載
*/
onLoad: function (options) {
wx.getSystemInfo({
success: (res) => {
if (res.platform == "android") {
this.setData({
isIos: false
})
}
}
})
},
/**
* 拖拽移動(補(bǔ)丁)
*/
handleSetMoveViewPos: function (e) {
// 在ios下永遠(yuǎn)都不會走這個方案,以免引起無用的計(jì)算
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, // 默認(rèn)顯示位置 left距離
top: 250 // 默認(rèn)顯示位置 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, //默認(rèn)顯示位置 left距離
top: 250 //默認(rèn)顯示位置 top距離
})
}
},
/**
* 返回首頁
*/
goToHome: () => {
wx.reLaunch({
url: '/pages/index/index',
})
}
})
這個最終解決方案在ios下直接使用cover-view來做懸浮窗,而在android的非原生組件上移動時,使用view來做懸浮窗,不冒泡滑動事件,在原生組件上移動時捕獲冒泡的滑動事件來繼續(xù)移動操作。
總結(jié)
雖然問題解決了,但是這仍然只是一個補(bǔ)丁方案。
最好的方式依然是微信小程序官方能修復(fù)cover-view在安卓移動時的BUG,但是我發(fā)現(xiàn)最早有人反饋這個問題是在2018年11月,到了現(xiàn)在2019年8月都沒有結(jié)果。
如果不是微信小程序的官方態(tài)度有問題,那么只能說明這個問題的解決確實(shí)有難度或者優(yōu)先級并不高,無論是哪一種,暫時都還是得用補(bǔ)丁方案。
這個方案并沒有那么完美,他在一些邊界的銜接上面可能還是會存在一些小問題,但它至少可用,并且應(yīng)該是大多數(shù)用戶可以接受的。
以上所述是小編給大家介紹的微信小程序中懸浮窗功能的實(shí)現(xiàn) ,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
如果你覺得本文對你有幫助,歡迎轉(zhuǎn)載,煩請注明出處,謝謝!
相關(guān)文章
詳解小程序如何避免多次點(diǎn)擊,重復(fù)觸發(fā)事件
這篇文章主要介紹了詳解小程序如何避免多次點(diǎn)擊,重復(fù)觸發(fā)事件,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-04-04
基于layer.js實(shí)現(xiàn)收貨地址彈框選擇然后返回相應(yīng)的地址信息
這篇文章主要介紹了基于layer.js實(shí)現(xiàn)收貨地址彈框選擇然后返回相應(yīng)的地址信息,需要的朋友可以參考下2017-05-05
JS優(yōu)雅的使用function實(shí)現(xiàn)一個class
這篇文章主要為大家介紹了JS優(yōu)雅的使用function實(shí)現(xiàn)一個class示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
JavaScript中Require調(diào)用js的實(shí)例分享
下面小編就為大家?guī)硪黄狫avaScript中Require調(diào)用js的實(shí)例分享。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10
ng-options和ng-checked在表單中的高級運(yùn)用(推薦)
AngularJS是當(dāng)前非常的流行的前端框架,它的語法糖非常多,也極大的方便了前端開發(fā)者。這篇文章主要介紹了ng-options和ng-checked在表單中的高級運(yùn)用,需要的朋友可以參考下2017-01-01
詳細(xì)分析Javascript中創(chuàng)建對象的四種方式
這篇文章詳細(xì)介紹了Javascript中創(chuàng)建對象的幾種方式與每種方式的優(yōu)缺點(diǎn),其中包括工廠模式、構(gòu)造函數(shù)模式、原型模式和組合使用構(gòu)造函數(shù)模式和原型模式,有需要的小伙伴們一起來學(xué)習(xí)學(xué)習(xí)吧。2016-08-08
淺談類似于(function(){}).call()的js語句
這篇文章主要介紹了淺談類似于(function(){}).call()的js語句,的相關(guān)資料,需要的朋友可以參考下2015-03-03

