如何巧妙應對iOS鍵盤難題?

前端南玖發表於2024-04-11

前言

寫過移動端的同學或多或少都遇到過軟鍵盤帶來的各種各樣的問題,最典型的就是輸入框被軟鍵盤遮擋、fixed元素失效等問題,並且這些問題在iOS上的表現讓人難以接受。

webview的差異

在移動端上,我們的H5頁面一般是執行在宿主APP提供的webview中,簡單點理解,你其實可以把它當作瀏覽器,就是用來展現頁面內容的。目前移動端主流系統分為AndroidiOS,然而兩者提供的webview容器也存在著諸多差異,今天我們就只探討兩者軟鍵盤帶來的影響。

首先,我們先來寫個簡單的頁面佈局:頭部fixed+中間自適應+底部fixed

k2.png

Android

事實上Android的表現並不會有太大問題,它只不過是在鍵盤彈起來之後把webview的高度減小了,變成了:原來的webview高度減去鍵盤的高度

k2.png

這樣的表現正是我們期待的,完全沒有影響整個頁面的佈局

iOS

軟鍵盤

在iOS 8.2 之後,iOS 唯一指定瀏覽器核心、Webkit 鼻祖 Safari 將 fixed 元素的佈局基準區域從鍵盤上方的可見區域改成了鍵盤背後的整個視窗,也就是說此時的webview高度並不會發生變化,鍵盤是直接蓋在webview上方的。

這樣是為了在鍵盤彈起來之後,不用重新渲染頁面,他們是方便了,但遭殃的是我們前端開發人員...

比如上面這個頁面,我們看看iOS的表現是怎樣的:

k3.png

可以看到,iOS為了不讓webview壓縮,並且為了不讓軟鍵盤遮擋輸入框,他們自作聰明地把webview整體往上移動,最大移動距離為軟鍵盤的高度。

這樣就導致我們的頭部以及頁面上半部分內容移動到了可視區之外,這個表現是難以接受的,至少頭部應該還要在可視區。(這就會讓我們誤以為fixed失效,實際上它相對於webview的位置並沒有變,只不過是webview發生了移動)

這個移動似乎沒有邏輯,不信大家可以試試把輸入框放到頁面的各個位置,我發現只有輸入框在最頂部,webview才不會發生上移,其它位置都或多或少的會產生移動。

還有一個問題就是,此時的webview是可以滑動的,那麼就會出現有使用者會將輸入框滑動到鍵盤下方,想想這個體驗也是難以接受的...

k3-1.gif

並且你會發現,在頁面的上方與下方都多出了一個不論是 Viewport 還是 VisualViewport 都無法到達的白色襯底區域,我們可以嘗試把頁面所有元素背景都改成黑色再來看,會更加明顯

k3-2.gif

看到這些奇奇怪怪的問題你心裡作何感想??

所有問題產生的根本原因是:iOS為了不用在鍵盤彈起之後重新渲染頁面,他們並沒有去壓縮webview容器的高度,而是對webview整體進行平移處理

軟鍵盤監聽

對於Android,我們通常可以透過監聽resize事件來實現,但對於iOS,我們從上面瞭解到鍵盤彈起,iOS的webview高度並不會發生變化,所以也就觸發不了resize事件。

在iOS中,可以透過focusin & focusout事件來進行監聽

export const watchKeyBoard = (callback: (isShow: boolean) => void) => {
  //  IOS
  if (isIOSByUA()) {
    document.body.addEventListener('focusin', () => {
      //軟鍵盤彈出的事件處理
      callback(true)
    })
    document.body.addEventListener('focusout', () => {
      //軟鍵盤收起的事件處理
      callback(false)
    })
  } else {
    //  Android
    const originalHeight =
      document.documentElement.clientHeight || document.body.clientHeight
    window.addEventListener('resize', () => {
      const resizeHeight =
        document.documentElement.clientHeight || document.body.clientHeight
      if (resizeHeight - 0 < originalHeight - 0) {
        // 鍵盤彈起事件
        callback(true)
      } else {
        // 鍵盤收起事件
        callback(false)
      }
    })
  }
}

解決方案

瞭解完產生問題的原因,我們就可以來嘗試著解決問題,但想要純前端去解決這個問題,或多或少都會存在一些體驗問題,也許你可以去推動你們的客戶端同學來協助處理這個問題,只要讓iOS的webview在鍵盤彈起時的表現與Android一致,就不會存在這些奇怪的問題了,但似乎他們處理起來也非常棘手...

模仿Android的處理

雖然我們改不了webview的高度,但我們可以改我們佈局的高度,我們只需要將頁面高度改為頁面可視區的高度即可,如果頁面內容有滾動互動的話,需要額外處理,要與webview的滾動隔離開。

VisualViewport

先來了解下這個API,它可以用來獲取對應 window 的視覺視口

  • VisualViewport.offsetLeft :返回視覺視口的左邊框到佈局視口的左邊框的 CSS 畫素距離。
  • VisualViewport.offsetTop:返回視覺視口的上邊框到佈局視口的上邊框的 CSS 畫素距離。
  • VisualViewport.pageLeft:返回相對於初始的 viewport 屬性的 X 軸座標所對應的 CSS 畫素數。
  • VisualViewport.pageTop:返回相對於初始的 viewport 屬性的 Y 軸座標所對應的 CSS 畫素數。
  • VisualViewport.width:返回視覺視口的寬度所對應的 CSS 畫素數。
  • VisualViewport.height:返回視覺視口的高度所對應的 CSS 畫素數。
  • VisualViewport.scale:返回當前視覺視口所應用的縮放比例。

這裡我們需要的就是這個VisualViewport.height,用來獲取可視區的高度。

但需要注意的是,這個API最低只支援iOS13,ios13以下的使用window.innerHeight兜底

頁面佈局

整體佈局採用flex佈局,頭部和底部也就不需要fixed來定位了,中間自適應撐滿剩餘高度,超長滾動

k4.png

鍵盤開啟計算高度重新佈局

我們需要在鍵盤彈起後,計算可視區的高度,並將最外層容器高度賦值為可視區高度

watchKeyBoard((status) => {
  setTimeout(() => {
    console.log(
      'status',
      status ? '鍵盤開啟' : '鍵盤關閉',
    )
    const container = document.getElementById('container')
    if (status) {
      container.style.height = `${
      window.visualViewport.height || window.innerHeight
    }px`
      window.scrollTo(0, 0)
    } else {
      container.style.height = `100vh`
      document.removeEventListener('touchmove', this.stopMove)
    }
  }, 100)
})

這樣頁面展示算是正常了

k5.png

但是隨之而來的是滾動問題😓

k6.gif

處理滾動

我們需要禁用全域性的滾動,但對一些需要滾動的區域需要放開,比如中間的列表部分

if (utils.isIOSByUA()) {
  watchKeyBoard((status) => {
    setTimeout(() => {
      console.log(
        'status',
        status ? '鍵盤開啟' : '鍵盤關閉',
        window.innerHeight,
      )
      const container = document.getElementById('container')
      if (status) {
        container.style.height = `${
        window.visualViewport.height || window.innerHeight
      }px`
        window.scrollTo(0, 0)
        document.addEventListener('touchmove', this.stopMove, {
          passive: false,
        })
        document.addEventListener('touchend', this.scroll)
      } else {
        container.style.height = `100vh`
        document.removeEventListener('touchmove', this.stopMove)
        document.removeEventListener('touchend', this.scroll)
      }
    }, 100)
  })
}
stopMove(e) {
	// 排除可以滾動的區域
  if (['content', 'keyboard_center'].includes(e.target?.className)) return
  e.preventDefault()
}
scroll() {
  window.scrollTo(0, 0)
}

完整體驗如下

k7.gif

比起它原本帶來的遮擋、滾動、fixed失效等體驗,現在的體驗算是可以接受的(這裡所有的操作我們只需要在iOS上執行即可)

相關文章