微信小程序中懸浮窗功能的實現(主要探討和解決在原生組件上的拖動)

問題場景

所謂懸浮窗就是圖中微信圖標的按鈕,採用fixed定位,可拖動和點擊。html

這算是一個比較常見的實現場景了。android

爲何要用cover-view作懸浮窗?原生組件出來背鍋了~

最初我作懸浮窗用的不是cover-view,而是view。ios

這是簡化的代碼結構:小程序

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呢?app

由於頁面上有個textarea組件,這個組件是原生組件,當懸浮窗移動到這個textarea組件上時,將沒法繼續拖動和點擊。函數

若是懸浮窗一開始就定位在textarea上,那麼就更慘了,一開始就不能點擊和拖動了。測試

這個緣由是由於微信小程序的原生組件層級高於非原生組件,不是你修改幾下樣式就能解決的問題。this

這裏就不講什麼原生組件了,若是想進一步瞭解,能夠參考我以前寫的一篇博客:微信小程序在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月都沒有結果。

若是不是微信小程序的官方態度有問題,那麼只能說明這個問題的解決確實有難度或者優先級並不高,不管是哪種,暫時都仍是得用補丁方案。

這個方案並無那麼完美,他在一些邊界的銜接上面可能仍是會存在一些小問題,但它至少可用,而且應該是大多數用戶能夠接受的。

原文出處:https://www.cnblogs.com/vvjiang/p/11286880.html

相關文章
相關標籤/搜索