新方法解决IOS软键盘出现后固定底部的fixed bottom失效

2,830 阅读12分钟

前言

上篇文章 讲到如何解决IOS软键盘出现后,设置了position:fixed;top:0;失效的解决方案。这次,解决一个比上次说的更加常见的设计,就是在页面底部会有固定的底部栏,上面有各种操作按钮等,这种布局,目前是十分常见的,比上篇说的吸顶设计更为常见。

所以本次,主要讲解决position:fixed; bottom:0; 在IOS页面中,软键盘出现后失效的问题。

对于看过了我上篇文章的同学来讲,其实要解决这里的问题能迎刃而解,基本思路是一样的,无非就是把吸顶改成吸底而已,只要把这部分的代码简单改一下就ok了。

具体的一步步思考,一个个的思路诞生,其实是跟上篇文章一样的,所以一些走了弯路的方案,如方案一、二、七等这些,由于效果不好,就不在这里重复分析写一遍了。如果你想了解前因后果,请务必阅读上一篇文章——6种解决IOS软键盘出现后position:fixed吸顶失效的新思路方案

如果你仅仅是想找一个解决方案解决你的问题,不想知道前因后果等思路,那么就可直接阅读此篇文章了,这里直接给出实现较好的解决方案,对标上篇文章的方案三和方案六。

核心计算

首先了解下,所谓的position: fixed; bottom: 0;失效,是因为在IOS中软键盘出现后,是直接覆盖在webview上面的,并不是像安卓那样进行webview的挤压。因此实际上定位是没有失效的,只是视觉上看着像是不起作用而已。

文章的各种基础知识都是建立在大家已经阅读了 不如刷新认识移动端软键盘出现带来的网页问题 本篇文章,所以一些基础知识不再展开讲。

那么既然被遮挡住了,我们的目的就是要在软键盘出现后,实时变化bottom定位,让其显露出来,并定位到软键盘的上方,紧挨着。

看图分析下

image.png

图中红色块为承载网页的webview,绿色区域为软键盘,黑色区域为手机视图,而虚线块就是固定在底部的元素,图中的位置是软键盘出现后预期想要出现的位置。

可以看到软键盘出现了,webview向上平移了一部分距离。那么此时你想要把固定在底部的元素重定位到软键盘上面,根据图中标识的不难看出,定位元素的bottom值通过下述公式计算可得

bottom = 软键盘高度 - webview偏移量

而软键盘的高度计算公式为

软键盘高度 = window.innerHeight - window.visualViewport.height

剩下的就差关于webview偏移量的计算了,关于这部分,上篇文章也围绕着它想出了各种方案,从各种方案分析中得出,我们最终解决问题采用的是结合css布局方案,因此计算webview偏移量也变得十分简单:

webview偏移量 = window.pageYOffset

因此最终的计算公式变成

bottom =  window.innerHeight - window.visualViewport.height - window.pageYOffset

方案(布局+交互)

经过 上篇文章 的各种分析,从实现手段和效果两方面综合考虑来讲,决定采用上篇文章提及的到方案三和方案六,在这里我们只需要把定位元素从top定位改成bottom

需要重定位的场景仍然是有三处:

  • 软键盘出现后
  • 收起软键盘后恢复定位
  • 从页面中一个引起软键盘出现的元素聚焦到另一个同样引起软键盘出现的元素(如输入框)
  1. 所谓布局,即简单利用css布局,让计算webview平移变得简单,让原本window.pageYOffset可能包含平移距离和webview内容滚动距离,直接变成代表平移距离。
  2. 所谓交互,即利用交互手段的限制,来避免频繁的监听滚动不断变化定位(因为这种不断变化效果看起来很糟糕),这种交互就是在软键盘出现后,只要发生滑动手势,即主动收起软键盘

虽然目前给大家的感觉是方案跟解决fixed top方案是一样的,只是计算公式有区别,但是很多细节上,还是要注意的,缺少了这些细节代码,跑起来还是存在缺陷的。请看官好好看看js部分的注释说明。在代码旁边写分析注释更直观。

基础版

这里的基础版,实际上是对标上篇文章的方案三,即简单的结合css布局和交互手段。

js代码部分已经注释了详细的分析说明文字

html部分

<html>
    <body>
        <div class="page">
            <div class="wrap">
                <!--这里有很多页面中的输入框内容等-->
            </div>
            <div class="fixed">固定底部元素</div>
        </div>
    </body>
</html>

css部分

html, body, .page {
   height: 100%;
}
.page {
    overflow: auto;
}

.fixed {
    position: fixed;
    bottom: 0;
}

js部分(具体解释看注释)

var page = document.querySelector('.page')
var currentInput = null // 当前聚焦的输入框
var fixedEle = document.querySelector('.fixed')
var originHeight = window.innerHeight

// 滚动手势时收起软键盘
function stopMove () {
    if (currentInput) {
        currentInput.blur()
    }
}
/**
 * 调整定位元素计算
 */
function setFixedBottom () {
    fixedEle.style.bottom = originHeight - window.visualViewport.height - window.pageYOffset + 'px'
}
// webview发生平移,则及时更新fixed元素的定位
function handleScroll () {
    setFixedBottom()
}

/**
 * 聚焦输入框时
 */
function handleFocusin (e) {
    var el = e || window.event
    currentInput = el.target

    setFixedBottom() // 从一个输入框聚焦到另一个输入框时,因为上一个聚焦的输入框因为失焦导致top置为0了,如果新聚焦的输入框不会触发webview平移,则沿用当时的位移就好了
    // 添加滚动监听,为了软键盘出现 以及 从一个聚焦输入框聚焦到另外一个输入框时, 重新定位fixed元素
    window.addEventListener('scroll', handleScroll)
    // 监听移动手势: 在软键盘出现后,如果想要滚动,则收起软键盘,解绑webview的滚动监听事件
    window.addEventListener('touchmove', stopMove)

    // 这里主要是应付每次第一次聚焦输入框出现软键盘时时没有发生webview平移时(如输入框本身就在页面很上面),手动调整定位,因为没有发生平移,就不能依赖监听scroll事件进行调整定位。
    // 可能有人想着干脆去掉监听scroll,把定位调整的都通过这个定时器解决,
    // 但是这么做的话,当从一个输入框聚集到另一个输入框,由于这个变化过程比较快,这个定时器反应比较慢,所以就会出现定位延后调整的视觉感受,有种滞后感,体验不好
    // 利用scroll调整就比较丝滑。虽然最终还是会执行到这个定时器,但是此时执行的结果都已经调整好了,所以不会有任何影响。
    setTimeout(() => {
        setFixedBottom()
    }, 400)
}

/**
 * 输入框失焦
 * 为什么需要监听失焦事件,是因为当用户主动收起软键盘时(如点击软键盘上的收起或完成按钮),如果刚好webview原本就没有发生平移的(如聚焦的输入框在页面很上面,软键盘出现后也不会发生webview平移),
 * 那么就不能通过监听srcoll事件来及时更新定位,恢复最底部。因此要监听失焦,手动赋值bottom: 0
 */
function handleFocusout (e) {
    currentInput = null
    window.removeEventListener('touchmove', stopMove)
    window.removeEventListener('scroll', handleScroll)
    fixedEle.style.bottom = 0
}

page.addEventListener('focusin', handleFocusin)
page.addEventListener('focusout', handleFocusout)

该方案需要判断是在IOS中才可用。安卓不需要这些脚本。

核心的变动是定位的计算,但是方案实现细节方面还是跟处理固定顶部有些差异,特别是软键盘出现后但webview不平移的情况。

优点

  • 实现起来较为简单
  • 没有了软键盘出现的再滚动,就不会需要不断变化fixed元素定位导致的不及时问题

缺点

  • IOS13+ 支持 ,因为用到了window.visualViewport.height
  • 有布局限制(但是其实这个布局限制对于移动端开发来讲,影响不是很大,我觉得算不上弊端?)
  • 软键盘出现后一滚动就会收起(但是其实移动端可浏览视口本身就小了,滚动是为了浏览更多内容,所以收起来,其实也算是一个影响不大的交互,更甚者有些交互设计专门要求这样实现),当然,如果H5里的内容确实需要有拖动的交互,该方案就不行了,如滑动方块确认这种。

极致版

这里的极致版,就是对标上篇文章的方案六。即利用“增高”手段,让用户在软键盘出现后仍然能滚动查看内容,直到到达最顶或最底,再收回软键盘。

该方案做到更加极致。还原滚动查看能力给用户,且能在H5滚动的时候就看到被平移出去的内容,这样就更大幅度减少触发主动收起软键盘的概率了,做到极致! 为达到目的,我要在这基础上,再做一些改变。

增高

首先,在滚动body里的滚动条时,就希望能滚动看到网页webview被平移出去的内容,为了达到这个目的,我们需要动态增加body里滚动容器的内容,以足以滚动查看到这些内容。 增高的原则是,被平移出去了多少,就在顶部插入多高的内容,被软键盘挡住了多少,就在底部插入多高的内容。

  1. 获取平移出去的高度,就是window.pageYOffset可获取
  2. 获取软键盘遮挡的内容高度,就是用软键盘高度 - window.pageYOffset,即window.innerHeight - window.visualViewport.height - window.pageYOffset

处理webview的平移

做了增高后,就能直接通过滚动H5里的滚动条就能够查看到所有网页内容了,而无需等到webview平移才能查看到。

但是,当我们触发webview平移时,就会发现,因为增高了的缘故,能看到这些增高填充的无意义内容。

那怎么应付这种情况呢?

当body里的内容滚动到顶/底部时,如果再作滚动手势,则收起软键盘,取消增高的内容,一切恢复到原样

以下为代码主要部分

html部分

<html>
    <body>
        <div class="page">
            <!--这个是内容占位,用于顶部内容区增高-->
            <div class="prefix-placeholder"></div>
            <div class="wrap">
                <!--这里有很多页面中的输入框内容等-->
            </div>
            <div class="fixed">固定底部元素</div>
            <!--这个是内容占位,用于底部内容区增高-->
            <div class="suffix-placeholder"></div>
        </div>
    </body>
</html>

css部分

html, body, .page {
   height: 100%;
}
.page {
    overflow: auto;
}

.fixed {
    position: fixed;
    bottom: 0;
}

js部分(具体解释看注释,在代码旁边写分析注释更直观)

var page = document.querySelector('.page')
var prePlace = document.querySelector('.prefix-placeholder')
var sufPlace = document.querySelector('.suffix-placeholder')
var fixedEle = document.querySelector('.fixed')
var currentInput = null // 当前聚焦的输入框
var keyboardHeight = 0 // 软键盘高度
// 初始化的时候就要记录了,因为这里有个很奇怪的现象,ios软键盘出现后,window.innerHeight会随着webview自身的滚动而变化,用document.documentElement.offsetHeight和window.visualViewport.height 也是,不知道是什么影响了。
var height = window.innerHeight
var startMove = false // 记录是否发生了滑动手势

function handleTouchmove () {
    startMove = true
    page.addEventListener('touchend', handleTouchend)
}

function handleTouchend () {
    startMove = false
    page.removeEventListener('touchend', handleTouchend)
}

// webview发生平移,则及时更新fixed元素的定位
function handleWdinowScroll () {
    // 当平移时H5的内容已经滚动到顶部或底部,且 是因为发生滑动手势引起的scroll事件,此时就输入框失焦收起软键盘
    // 聚焦到输入框也会引起scroll事件,所以要加startMove区分开是滑动手势引起的
    if ((page.scrollTop === 0 || page.scrollTop + page.clientHeight >= page.scrollHeight) && startMove) {
        return triggerBlur()
    }
    // 这里计算一次是为了这里可能比handleFocus先执行
    const max = keyboardHeight || height - window.visualViewport.height
    // IOS回弹效果时不要改变定位
    if (window.pageYOffset <= max && window.pageYOffset >= 0) {
        // 聚焦输入框引起的平移
        setFixedBottom()
    }
}

function handleAddHeight () {
    // 计算过就不用再计算了,一般软键盘的高度是固定的,这样做还有一个顾虑:有些情况下,滚动的时候window.innerHeight和visualViewport.height还会变化,不能如实反馈,因此减少多次计算。
    keyboardHeight || (keyboardHeight = height - window.visualViewport.height)
    // 底部增高
    sufPlace.style.height = keyboardHeight - window.pageYOffset + 'px'
    // 顶部增高
    prePlace.style.height = window.pageYOffset + 'px'
    // 因为增高的缘故,需要及时更新滚动位置,保持原本理应展示的位置
    page.scrollTop += window.pageYOffset
}

/**
 * 调整定位元素计算
 */
 function setFixedBottom () {
    fixedEle.style.bottom = keyboardHeight - window.pageYOffset + 'px'
}

// 当页面里的输入框聚焦时(随后会出现软键盘)
function handleFocusin (e) {
    var el = e || window.event
    currentInput = el.target
    // 延迟是为了确保软键盘出来后才开始计算,因为软键盘出来有个过程动画
    setTimeout(() => {
        handleAddHeight()
        setFixedBottom()
    }, 400)
    // 从一个输入框聚焦到另一个输入框时,因为上一个聚焦的输入框因为失焦导致top置为0了,如果新聚焦的输入框不会触发webview平移,则沿用当时的位移就好了
    setFixedBottom()
    // 添加滚动监听,为了软键盘出现 以及 从一个聚焦输入框聚焦到另外一个输入框时, 重新定位fixed元素(其实这里不用滚动事件监听变化也可以用setTimeout来更新定位)
    window.addEventListener('scroll', handleWdinowScroll)
    page.addEventListener('touchmove', handleTouchmove)
}

// 主动失焦
function triggerBlur () {
    currentInput && currentInput.blur()
}

// 失焦时,重置一些数据
function handleBlur () {
    currentInput = null
    // 增高的都恢复到原样,即没有高度
    sufPlace.style.height = 0
    prePlace.style.height = 0
    fixedEle.style.bottom = 0
    startMove = false
    window.removeEventListener('scroll', handleWdinowScroll)
    page.removeEventListener('touchmove', handleTouchmove)
}

page.addEventListener('focusin', handleFocusin)
page.addEventListener('focusout', handleBlur)

优点

  • 定位效果展示良好
  • 满足绝大部分场景了,safari支持也良好
  • 效果上来讲,很好

缺点

  • IOS13+ 支持 ,因为用到了window.visualViewport.height
  • 有布局要求和最后的特定交互限制(个人觉得不是问题)

总结

整体上跟处理固定顶部的方案差不多,但是实现细节上,还是有不同的需要关注的场景,但是核心思想是一样的。那么自然,如果你的需求还有其他位置的定位,其实是一样道理的,万变不离其宗。希望我这些方案思路能给你带来方向。

至此关于IOS fixed定位“失效”的痛点分析与解决就结束了,下一篇将解决更加让开发者头痛的难点——软键盘出现后,输入框被软键盘挡住了!

欢迎关注哦~

未经授权,请勿私自转载

本文正在参加「金石计划 . 瓜分6万现金大奖」