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

来源:https://www.cnblogs.com/vvjiang/archive/2019/08/02/11286880.html
-Advertisement-
Play Games

問題場景 所謂懸浮窗就是圖中微信圖標的按鈕,採用fixed定位,可拖動和點擊。 這算是一個比較常見的實現場景了。 為什麼要用cover view做懸浮窗?原生組件出來背鍋了~ 最初我做懸浮窗用的不是cover view,而是view。 這是簡化的代碼結構: index.wxml: 一大段test,占 ...


問題場景

所謂懸浮窗就是圖中微信圖標的按鈕,採用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月都沒有結果。

如果不是微信小程式的官方態度有問題,那麼只能說明這個問題的解決確實有難度或者優先順序並不高,無論是哪一種,暫時都還是得用補丁方案。

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 因為項目需求,最近開始轉到微信公眾號開發,接觸到了Vue框架,這個效果的實現雖說是基於Vue框架下實現的,但是同樣也可以借鑒到其他地方,原理都是一樣的。 進入正題,先看下效果圖: 其實js做這個效果還是挺簡單的,因為在css中我們可以設置一個元素的position: fixed;,這樣它就可以固定在 ...
  • 本例結合我的前兩篇博客,使用jquery moblie框架搭建了一個簡單的手機版博客園;項目地址為http://blog.mdzz.tv:1011,這裡我僅爬取了博客園首頁的鏈接,點擊每一項即跳轉到博客園相應的頁面,源碼已放在github上了,地址https://github.com/guasses... ...
  • 本實例原始信息: 作者: "anix" 演示地址: "Echarts模擬遷徙" 源碼地址: "GitHub ananix qianxi" 前言 “百度地圖春節人口遷徙大數據”(簡稱“百度遷徙”)是百度在春運期間推出的技術品牌項目,為業界首個以“人群遷徙”為主題的大數據可視化項目。 準備 開發工具 由 ...
  • 1.HTML部分 accept屬性可以設置要上傳文件的格式 2.js部分 介面部分 代碼部分 ...
  • 截取字元串中的數字 1、使用parseInt() 2、使用正則 截取字元串 1、使用split() 2、使用join() 3、使用substring() 4、使用indexOf() 5、使用substr() ...
  • 事件處理程式指的是當 HTML 中發生某些事件時所調用的方法,是 jQuery 中的核心函數。 ...
  • 一、ES6的Set、Map數據結構 Map、Set都是ES6新的數據結構,都是新的內置構造函數,也就是說typeof的結果,多了兩個: Set 是不能重覆的數組 Map 是可以任何東西當做鍵的對象 ES6 提供了新的數據結構 Set。它類似於數組,但是成員的值都是唯一的,沒有重覆的值。 let s ...
  • 一、函數作用域查找1、定義說明1)、函數當前作用域查找不到,可以訪問外層函數作用域的活動對象(參數、局部變數、定義在外層函數體里的函數)2)、外層的外層函數。。。一直到全局 第一條說明:定義在外層函數體里的函數,包括當前函數,當前函數調用自己的時候,就是遞歸調用。 2、原理執行環境、作用域鏈、作用域 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...