如何用 js 獲取虛擬鍵盤高度?(適用全部平臺)

前言

這是一個存在好久的歷史問題了,對於這樣一個具備廣泛性的問題瀏覽器恰恰沒有給出解決方案,what?沒有方案還聊個什麼?html

別急,別急,接下來咱們一塊兒來扒一扒關於軟鍵盤高度和 input 的問題html5

咱們先來看一個短片認識一下這個問題react

js 獲取軟鍵盤高度,解決第三方輸入法遮擋底部input及android鍵盤迴落後留白問題

問題描述:當操做者進行輸入操做的時候,彈起的軟鍵盤把本來的輸入框遮擋了,致使操做者看不到操做結果android

以往的解決方案

以往的解決方案:ios

  1. 修改網站的頁面佈局,好比本例中 twitter 儘可能把 input 放置在中部以上的位置,從佈局上儘可能避免此類問題git

  2. 在一些指定設備和瀏覽器中異步獲取 window.innerHeight 進行先後對比而得出鍵盤高度github

再來看一下另外一種常見輸入框的頁面佈局:web

js 獲取軟鍵盤高度,解決第三方輸入法遮擋底部input及android鍵盤迴落後留白問題

在這個場景裏,輸入框定位在頁面的最底部,當軟鍵盤彈起時整個視圖窗口頁面向上捲動,到達最底部時中止。恰巧當咱們用 h5 來模擬這個效果的時候恰好勉強作到。瀏覽器

這是由於當你首次 fouse 到輸入框的時候軟鍵盤彈出,瀏覽器會使頁面會向上滾動,以確保 input 是可見的,該特性和 document.body.scrollIntoViewIfNeeded 方法是一致的,可是當你 body 的可滾動高度超過窗口高度時還會產生另外一個問題:固定元素將隨頁面滾動 以下圖app

js 獲取軟鍵盤高度,解決第三方輸入法遮擋底部input及android鍵盤迴落後留白問題

所以瀏覽器關心的只是 input 是否被覆蓋?其實是 input 中的光標位置!那麼這就解釋了爲何輸入框在底部的時候恰好勉強完成了,由於 input 在頁面的底部時,軟鍵盤彈出勢必會遮擋住 input,於是瀏覽器會向上滾動至輸入框可見的位置。

可是以下圖的效果這樣就沒法作到了,由於在輸入框的下面還有一行工具欄,也就是說輸入框並不是在最底部的位置,那麼瀏覽器在滾動到可視位置時只會確保到 input 可見,而對於工具欄是否可見則並不在瀏覽器的考慮範圍內。

js 獲取軟鍵盤高度,解決第三方輸入法遮擋底部input及android鍵盤迴落後留白問題

IOING 的解決方案分析

綜合來看上面兩種佈局方案的問題,都不能完美解決輸入被鍵盤遮擋和底部 footer 不能被頂起的問題,那是否是就沒得法子了?

固然號稱可讓 HTML5 表現更接近 Native 的 IOING 引擎必定是有解決方案的

咱們先來看一段 input 在 IOING 中的表現

解決第三方輸入法遮擋底部input及android鍵盤迴落後留白問題

解決第三方輸入法遮擋底部input及android鍵盤迴落後留白問題

咱們能夠看到在輸入過程當中頁面經過滾動來始終保持光標位於可視區域的中心位置,所以在這裏咱們須要提一個知識點:獲取輸入光標的實時位置,固然這也是一個曲折的過程,在這裏我就不擴算話題了,繼續來說原話題

前面說了三個主要的傳統解決方案:

  1. 第一個是經過把 input 佈局儘可能放在頁面頂部,顯然這個不是咱們想要的,否決掉

  2. 把 input 放在最底部,用來完成 footer 固定的效果,可是要侷限頁面高度不超過窗口高度,咱們能夠經過自制滾動控件來解除這個限制,那如今須要解決的技術點就變爲實現一個模擬滾動控件

  3. 經過比對軟鍵盤彈出先後的 window.innerHeight 的高度差來獲得鍵盤高度,從而根據這個高度來實現底部定位和輸入劇中,可是該方法侷限於不一樣設備平臺的支持

    綜上所述咱們總結一下咱們要解決的思路和步驟

    先來看一下下面的圖片

解決第三方輸入法遮擋底部input及android鍵盤迴落後留白問題

當鍵盤彈出時,鍵盤高度 = 不可見窗口高度
這個等式是有條件的,只有當 input 在對底部時該等式才成立 (這是上面講過的 scrollIntoViewIfNeeded 的緣由)

思考:若是咱們能讓該等式成立,且可以獲取不可見位置高度,是否就能得出鍵盤高度了呢

咱們整理好思路一步一步來實現

1.須要將內容放置在虛擬滾動中,在 IOING 像下面這樣就能夠建立一個虛擬滾動區域了

<scroll>
<scrolling>
    頁面內容
</scrolling>
</scroll>

傳統頁面可使用 WebKit 私有屬性「-webkit-overflow-scrolling: touch」 來容許獨立的滾動區域和觸摸回彈,或者使用 iScroll.js 等第三方庫來完成,可是須要注意對 iScroll 使用不當可能會形成性能問題

2.獲取光標位於屏幕中的位置

3.當光標 fouce 時,鍵盤彈起,若 input 被遮擋頁面會進行滾動,但滾動量不肯定,所以咱們能夠強制滾動到底端,即鍵盤徹底彈出後主動使窗口向上滾動窗口高度的距離,而實際上窗口只能向上滾動到最底部位置後就不能再向上滾動了,此時獲取頁面的 top.scrollY 即爲實際鍵盤高度

得出公式:

可視區域的中心位置 = 鍵盤高度 + (窗口高 - 鍵盤高度)/2
應滾動距離 = 可視區域的中心位置 - 光標offsetTop - (光標被遮擋 ?鍵盤高度 :0)

固然實際操做須要更多的細節,po 出 IOING 中該部分邏輯實現的源代碼:

// IOING 中部分源代碼
// dom 爲 input 元素
// scroll 爲滾動容器的 Scroll 對象

function scrollTo (y, _y, t, s, r) {
    r = r == undefined ? 1 : r
    y = y == undefined ? top.scrollY : y
    if ( r == 1 ? y > _y : y < _y) return
    s = s == undefined ? Math.abs((_y - y) / t * 17.6) : s
    rAF(function () {
        top.scrollTo(0, y += r*s)
        scrollTo(y, _y, t, s, r)
    })
}

function visibility () {
    if ( this.moving || this.wheeling ) {
        var top = dom.offset().top
        var height = dom.offsetHeight
        var viewTop = keyboardHeight + scrollOffsetTop
        var viewBottom = factWindowHeight - scrollOffsetBottom

        if ( top + height <= viewTop || top >= viewBottom ) {
            dom.blur()
        }
    }
}

function refreshCursor () {
    rAF(function () {
        dom.getSelectionRangeInsert('')
    })
}

function getScroll () {
    var scroller = reactScroller || dom.closest('scroll')

    scroll = scroller ? scroller.scrollEvent : null

    if ( type == 1 ) {
        minScrollY = scroll.minScrollY
    }
}

function getViewOffset () {
    // android : (top.scrollY == 0 ? keyboardHeight : 0)
    viewOffset = viewCenter - rangeOffset.top - (top.scrollY == 0 ? keyboardHeight : 0) + (that.module.config.sandbox ? keyboardHeight : 0)
    
    return viewOffset
}

function keyboardUp (e) {
    getScroll(1)

    if ( !scroll ) return

    // refresh cursor {{

        if ( device.os.ios && device.os.iosVersion < 12 ) {
            scroll.on('scroll scrollend', refreshCursor)
        }

    // }}
    
    if ( normal ) return

    function upend (e) {

        window.keyboard.height = keyboardHeight = top.scrollY || factWindowHeight - top.innerHeight

        // change minScrollY

        scroll.minScrollY = minScrollY + keyboardHeight
        scroll.options.minScrollY = scroll.minScrollY

        // 光標位置
        
        rangeOffset = dom.getSelectionRangeOffset()

        // 可見視圖的中心

        viewWrapper = factWindowHeight - keyboardHeight - scrollOffsetTop - scrollOffsetBottom
        viewCenter = keyboardHeight + viewWrapper / 2

        scroll.scrollBy(0, getViewOffset(), 600, null, false)

        // 滾動到不可見區域時 blur
        
        scroll.on('scroll', visibility)

        window.trigger('keyboardup', { 
            height : keyboardHeight 
        })

        if ( reactResize ) {
            scrollTo(null, 0, 300, null, -1)
        }
    }

    setTimeout(function () {

        top.one('scrollend', upend)

        // no scroll
        
        setTimeout(function () {
            if ( keyboardHeight == 0 ) upend() 
        }, 300)

        // ``` old
        
        var offset = 0

        if ( device.os.mobileSafari && device.os.iosVersion < 12 ) {
            offset = 24 * viewportScale
        }

        // scroll to bottom

        scrollTo(null, viewportHeight - offset, 300, null, 1)

    }, 300)
}

function keyboardDown () {
    getScroll()

    if ( !scroll ) return

    // ``` old : refresh cursor {{

        if ( device.os.ios && device.os.iosVersion < 11 ) {
            scroll.off('scroll scrollend', refreshCursor)
        }

    // }}

    if ( normal ) return
    if ( keyboardHeight == 0 ) return false

    top.scrollTo(0, 0)
    scroll.wrapper.scrollTop = 0
    
    // change minScrollY

    scroll.minScrollY = minScrollY
    scroll.options.minScrollY = minScrollY
    scroll.off('scroll', visibility)
    scroll._refresh()

    window.keyboard.height = keyboardHeight = 0
}

function selectionRange (e) {
    getScroll()

    if ( !scroll ) return

    // 非箭頭按鍵取消
    
    if ( e.type == 'keyup' && ![8, 13, 37, 38, 39, 40].consistOf(e.keyCode) ) return

    // 重置光標位置

    if ( reactOffset ) {
        rangeOffset = dom.getSelectionRangeOffset()
    } else if ( reactPosition ) {
        rangeOffset = dom.getSelectionRangePosition()
    }

    if ( reactOrigin && rangeOffset ) {
        rangeOffset.each(function (i, v) {
            scope.setValueOfHref(reactOrigin + '.' + i, v)
        })
    }

    if ( normal ) return

    // 光標居中

    if ( e.type == 'input' && e.timeStamp - timeStamp < 2000 ) return
    if ( !scroll || !viewCenter ) return
    if ( !reactOffset ) {
        rangeOffset = dom.getSelectionRangeOffset()
    }

    timeStamp = e.timeStamp

    scroll.scrollBy(0, getViewOffset(), 400, null, false)
}

dom.on('click', checkChange)
dom.on('focus', keyboardUp)
dom.on('blur', keyboardDown)
dom.on('focus keyup input paste mouseup', selectionRange)
})

其它的小細節和注意事項:

  1. safari 會受到瀏覽器底部導航欄的影響,會產生20多像素偏差,須要針對考慮

  2. safari 中的 input 光標在執行 transform 3d變換的時候會出現光標停滯的現象,須要執行光標刷新操做

  3. 當 input 被操做者主動滑出可視區域外時應處罰鍵盤收起操做,不然在輸入時 scrollIntoViewIfNeeded 效應將致使窗口滾動出現空白的問題

最後總結:

獲取鍵盤高度只是咱們的表象,真正解決 html5 帶來的各類問題纔是咱們的研究課題,也只有掃清這些佈局殺手 h5 才能在追趕 Native 的道路上更近一步!


結尾

最後的最後我來 po 一下在 IOING 中完成這一步咱們須要作什麼?

<input placeholder=寫點啥>

就是這麼簡單,IOING 中 input 默認就能擁有自動居中特性

若是你要取消這個特性,就像下面這樣寫

<input nomal placeholder=寫點啥>

固然也能夠設置居中相對底部/相對於頂部的偏移位置

<input scroll-offset-top=50 placeholder=寫點啥>
<input scroll-offset-bottom=50 placeholder=寫點啥>

在輸入過程當中可以實時輸出光標位置,且將位置信息賦值給數據源對象

<textarea react-position="test.range" resize="none"></textarea>
<p>當前光標位置:left: {test.range.left}, top: {test.range.top}</p>
<!-- test.range 爲一個數據源對象 -->
<!-- react-position 指令將把該輸入框的光標狀態傳遞給 test.range 對象  -->

用js 獲取鍵盤高度的方法

//鍵盤彈起時爲鍵盤高度,未彈起時爲0
console.log(window.keyboard.height)
// 經過鍵盤彈起事件獲取
window.on('keyboardup', function (e) {
    console.log(e.height)
})
// 鍵盤收起事件
window.on('keyboarddown', function (e) {
    console.log(e.height) // 0
})

詳細文檔傳送門:http://ioing.com/#docs-dom-input
GitHub 傳送門:https://github.com/ioing/IOING

相關文章
相關標籤/搜索