7种解决IOS软键盘出现后position:fixed吸顶失效的新思路方案

5,768 阅读24分钟

我正在参加「掘金·启航计划」 image.png

未经允许,请勿私自转载

前言

这个是IOS典型问题之一,你会发现搜索fixed失效,能找到一堆网络博客,很可惜,即使这么多的资料,也未能找到我想要的。

为什么呢?因为很多找到的资料,虽然标题、关键字意思上看起来是一样的,都是说解决IOS fixed失效,然而发现很多都不是一个问题,但却被描述成一样,这就是很多人为什么明明跟着博客作者一样的写法却无法生效的原因,因为压根不是一个问题,我简单总结下这些问题都有哪些:

  • 软键盘出来后,fixed底部不生效
  • 软键盘出来后又收起来,fixed底部的按钮点击不起作用不生效(我晕,是指点击不生效)
  • 软键盘出来后又收起来,底部有一片空白(我不知道为什么竟然会被描述成fixed不生效 - -)
  • 没有提及到软键盘的事,但是直接说fixed不生效,得换布局方式(这种类型的帖子还真不少,翻找国外资料,估计是很久前的IOS问题了,后来好像被修复了,现在我未能复现)
  • 等等

反正很多不同原因导致的失效,都被他们描述成“fixed不生效”,所以混淆信息很多,大家要学会分析原因,才能找到符合自己的答案。

而为这里我要说的不生效是指什么呢?指的是在IOS中,设定了position: fixed; top: 0;的元素,在软键盘出现后及后续滚动中,会被滚动上去以至于被可视视口隐藏掉

关键点是:

  • IOS中
  • fixed网页顶部的元素
  • 软键盘出现后才会被滚动上去

解决方案

现有解决方案

说句实在话的,搜索这个问题找到的帖子相对较少,大部分还是集中在前言中我所罗列的那几个问题,尤其是固定在底部的,你说要找固定到顶部的,是真的少,也许在移动端的设计中,这种设计还是比较少数的?

很遗憾我未能找到很好的解决方案,从找到的帖子中也仅仅能得到别人总结的两点信息

  1. 利用app的顶部标题栏来实现(无疑就不是H5领域了,变成原生移动端开发了,且这也只适合混合app的使用的方案,纯粹网页无法实现)
  2. 顺其自然,既然移动端本身不推崇这种设计,那么就算非得到这种地步了,就让他上去吧,无伤大雅。

我的方案

既然网上没有现成的方案可以解决,那就只能自己想办法,想方案出来咯。经过自身的思考与实践,暂且总结出6个方案吧,大家就凑合着使用吧。

在大家看方案前,务必请阅读【此篇文章】,理解好IOS在软键盘出现前后的webview变化情况,这有助于你理解这里说的方案的实现与思路。

不过这里也说下上述文章的一个关键点:

软键盘出现后,IOS上webview不会被挤压,但是为了让底部信息能展示出来,不让软键盘遮挡,就让webview整体发生平移,这个平移会同样触发windowscroll事件以及影响window.scrollY

接下来随着我的思考思路一起一步步探究不同的实现方式。

方案一

正是因为webview平移关系导致fixed到顶部的元素随着webview一起被平移出可视区域了。那么很自然的想法就是只要时刻监听平移出去的距离然后不断给fixed元素的top值赋值等同距离不就可以一直看到顶部固定元素吗?

没错,想法方向是正确的,但是真正实现起来会遇到各种细节难点。

整体思路:软键盘出来后,获取webview的移动距离,让fixed元素的top值等于移动的距离,后续滚动,webview移动了多少,就不断变化top值。说白了,就是跟着滚动而不断变化固定元素的定位距离,让视觉上看起来是在顶部的。

但是要注意区分,如果滚动的是网页内容自身,则不需要改变top值,本身这时候只要webview没有被顶上去,fixed元素肯定是固定在页面顶部即可视视口顶部的。

而不论是webview平移了还是网页内容自身滚动,都会引起pageYOffset的变化以及windowscroll事件。

所以问题的关键就变成,区分这两者了。

阅读了上述推荐阅读的文章后,要清楚两个关键点:

  • webview平移的最大距离为软键盘的高度
  • 当webview上有滚动,你欲滚动webview上的内容(非h5容器里的滚动内容),会先移动webview自身,移动完才到滚动webview上的内容

结合上述两点,可以总结出,当向下滚动时,如果pageYOffset的值不超过软键盘高度,则表明是在平移webview;向上滚动时,如果从一开始向上滚动时与向上滚动结束时(不论时连续的还是断断续续的,但是必须是一直都是向上滚动)的距离相差不超过软键盘高度,则表明webview在平移。

而软键盘高度可以通过window.innerHeight - window.visiualViewport.height得出。

实现

一切关键字信息都准备好了。接下来就是组织这些信息来形成方案了。

哦这里我不写代码出来了,想直接用文字描述方案, - - 跟下述方案相比,这个方案略显复杂但是实现效果是一样的,但是希望能给大家带来新的思路,请谅解

  1. 软键盘弹出时,将fixed元素的top等于window.pageYOffset,注意软键盘弹出有个动画,需要延迟赋值。
  2. 监听滚动手势是属于向上滚动还是向下滚动。
  3. 定义一个变量,记录开始向上滚动当时的scrollY,只要发生过一次向下滚动,则这个标志重置为0
  4. 如向下滚动,则标志重置为0,然后根据判断向下滚动距离不超过window.innerHeight - window.visiualViewport.height,若不超过,则将scrollY赋值给fixed元素的top
  5. 如向上滚动,则开始记录标志,之后每次向上滚动都拿当时的pageYOffset跟标志比较(不论是连续的一次性向上滚动还是断断续续的进行向上滚动,只要中间没发生过一次向下滚动,则不会重新记录标志),如果相减值小于window.innerHeight - window.visiualViewport.height,则将window.innerHeight - window.visiualViewport.height - 标志 - pageYOffset 赋值给fixed元素的top
  6. 其他场景,都得将top设为0,特别是软键盘收起时

缺点

  • 利用了window.visiualViewport对象,而这个新特性,只有IOS 13才开始支持,即意味着这个方案在很低版本系统的机子中,还是有问题,但是这部分机子市场占比是非常小的了。
  • 非常依赖pageYOffset属性,但是在Safari中计算可能不准确
    • 中途visiualViewport可能会发生不可预测的变化,推测是这个浏览器url栏和工具栏不断会发生变化的原因导
    • 再加上滚动距离还多出了底部工具栏的距离,这个距离还没法得到。
  • 方案是监听滚动时才做反应重新赋值固定元素的top值,这个是有延迟的,即获取到了变化后的pageYOffset才响应变化,所以在滚动过程中即使看到最终是固定头部了,但是中间会看到固定元素移动得不流畅,就算加了动画过渡也无法跟正常的固定效果相提并论
  • 因为scroll是高频事件,在快速滚动过程种,会有一定消抖,导致获取pageYOffset存在不精准的问题,最终导致定位也会存在不精准

其实这个方案的缺点最关键的主要是两点:

  1. 要区分是webview平移导致的pageYOffset变化还是本身webview内容滚动导致,麻烦。
  2. 监听scroll变化的频次太高,且被节流了,响应时机被延迟,计算不准

后续的各个方案都是目标为了解决这两个问题,只要解决这两个问题,其他都是小问题。把上述两点问题记作【问题1】、【问题2】,后续各个方案有描述到。

方案二(利用布局)

对页面进行高度限制,不然其超出webview高度,产生webview的滚动条。这样我们就不像方案一那样需要判断window.pageYOffset是属于哪种滚动引起的,这里就限定死了,有pageYOffset就是滚动webview自身导致的。这样就可以直接赋值pageYOffset的值了,且还不用考虑方向,方便很多。

我这里建议是用一个div.page来覆盖整个网页,而不是直接在body上动手,即把div.page当作body,后面的内容都在这个div.page上写。

html结构:

<html>
   <body>
       <div class="page">
           <div class="fixed">这是fixed顶部的内容栏</div>
       </div>
   </body>
</html>

css部分:

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

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

为什么不建议在body上当成滚动容器呢?这里有实践中发现的几点知识:
1、就算html,body设置了100%,但是如果挨着body的子元素设置了margin-top,还是会影响到父元素body的margin。
2、就算html,body设置了100%,body设置了overflow:auto; 也不能如期的让body内容滚动,body底下内容过长时,引起页面的滚动是window的滚动,而不是body,即webview的滚动条(移动端中)。得设置html的overflow为auto/scroll/overlay/hidden;但是如果你不是用body作为滚动容器,而是再起一个div来作为滚动容器,设置该div height:100%;overflow:auto;这样的话,html,body就不需要设置overflow了。
3、用body作为滚动容器,利用css设置滚动条样式,好像不完全起效,是有问题的,如::-webkit-scrollbar,::-webkit-scrollbar-thumb
4、经实验发现,要想body上的overflow起效,html必须得设置overflow为auto/scroll/overlay/hidden;才行

js部分:

var page = document.querySelector('.page') // 这个是包含输入框的父元素
var keyboardHeight = null // 这个变量是用来记录软键盘的高度的,即可知道webview自身的移动最大距离了
// 初始化的时候就要记录了,因为这里有个很奇怪的现象,ios软键盘出现后,window.innerHeight会随着webview自身的滚动而变化,用document.documentElement.offsetHeight和window.visualViewport.height 也是,不知道是什么影响了。
var height = window.innerHeight

page.addEventListener('focusin', function (e) {
   // 等软键盘完全出现后
   setTimeout(() => {
   	keyboardHeight = height - window.visualViewport.height
   	info.style.top = `${window.pageYOffset}px`

   	window.addEventListener('scroll', function (e) {
   	    // fixed.innerHTML = `${innerHeight}、${document.documentElement.offsetHeight}、${window.visualViewport.height}`
   		// 要防止ios回弹效果导致的pageYOffset变化,这种情况下是不需要变动fixed元素的top的
   		if (window.pageYOffset < keyboardHeight && window.pageYOffset > 0) {
   			info.style.top = `${window.pageYOffset}px`
   		}
   	})
   }, 500);
})

// 软键盘收齐后就需要让定位元素回归 且 取消绑定监听
page.addEventListener('focusout', function (e) {
   info.style.top = '0'
   // 取消绑定window的scroll事件
})

这里虽说使用了window.visiualViewport来做一些计算处理,但是目的是为了防止回弹效果造成的误判,因此如果你选择采用某些方法阻止IOS的回弹,那么这里就不需要用到window.visiualViewport了,就没有这个兼容性问题

缺点

  • 利用了window.visiualViewport对象,而这个新特性,只有IOS 13才开始支持,即意味着这个方案在很低版本系统的机子中,还是有问题,但是这部分机子市场占比是非常小的了。
  • 非常依赖pageYOffset属性,但是在Safari中计算可能不准确
    • 中途visiualViewport可能会发生不可预测的变化,推测是这个浏览器url栏和工具栏不断会发生变化的原因导
    • 再加上滚动距离还多出了底部工具栏的距离,这个距离还没法得到。
  • 方案是监听滚动时才做反应重新赋值固定元素的top值,这个是有延迟的,即获取到了变化后的pageYOffset才响应变化,所以在滚动过程中即使看到最终是固定头部了,但是中间会看到固定元素移动得不流畅,就算加了动画过渡也无法跟正常的固定效果相提并论
  • 因为scroll是高频事件,在快速滚动过程种,会有一定消抖,导致获取pageYOffset存在不精准的问题,最终导致定位也会存在不精准
  • 跟上述方案相比,这里需要结合html结构以及样式上配合
  • 体验不太好,滚动完h5容器顶部或底部后,需要等h5容器滚动条消失后再滚动才是让webview平移,容易让用户一开始觉得是滚不动了,这样误以为底下的内容无法查看。

优点

  • 比方案一js部分编写和理解要简单很多
  • 通过css布局的调整,充分利用了一个知识点:【IOS软键盘出现后,当webview自身内容没有可滚动查看更多内容时,但是网页自身的元素有滚动条时,在移动端上它们的共同区域中做出滚动手势,则优先滚动网页自身H5元素的滚动条,后才发生webview的平移】。根据上述知识点,此方案更大的优势在于能够很大程度减少webview平移触发的概率,继而减少fixed元素被遮挡而要调整的概率。所以大场景下显示ok,小概率就需要用到上述js进行top的调整。

ps: 上述的知识点在 上篇文档 有介绍,没看的强烈推荐先了解基本的情况。

这个方案很好的解决了【问题1】,至于【问题2】则是减低发生概率,不算解决但是算优化了。

方案三(布局+交互)

上个方案是减少触发webview平移的频率,减少问题的发生,这个方案更简单粗暴点,在软键盘出现后如果想要滚动页面,则输入框失焦收起软键盘,直接让平移webview发生这种可能性为0。

在软键盘出现后,就直接赋值pageYOffset给fixed固定元素的定位,在软键盘收起后,也直接定位成top为0。关键的是,要监听禁页面的滑动手势,只要发生滚动手势,就输入框失焦,让软键盘消失。这样就没有webview平移上去了fixed元素超出可视窗口的问题了。

需要注意,还有一个细节需要处理好的,就是当聚焦了某个输入框后,软键盘出现了,然后再点击聚焦另外一个输入框,此时webview可能还是会发生平移,这时候也是要及时更新定位的。

该方案是在方案二的基础上添加了上述的交互手段限制。滚动失焦收起软键盘解决了【问题2】,css布局解决了【问题1】

可能有些人不理解,在软键盘出现后,都禁止页面滚动了,为啥还要解决【问题1】,禁止滚动了不就表示pageYOffset的变化只能代表webview的平移吗?是的没错,在软键盘出现后的情况下确实是这样的,但是在软键盘出现前,webview自身内容可能已经发生了滚动,软键盘出现后,pageYOffset就变成原本的pageYOffset + webview平移的据里了。显然对fixed元素的top的值赋值还是要区分开来,只挑平移的据里赋值,到头来还是需要解决问题1。

具体实现见下方代码,注释也解释清楚了。

var page = document.querySelector('.page')
var scrollTop = null // 计算div.page应该滚动的距离
var currentInput = null // 当前聚焦的输入框
var inputs = document.querySelectorAll('input')
var fixedEle = document.querySelector('.fixed')

// 滚动时收起软键盘
function stopMove () {
    if (currentInput) {
        currentInput.blur()
        currentInput = null
        window.removeEventListener('touchmove', stopMove)
        window.removeEventListener('scroll', handleScroll)
        fixedEle.style.top = 0
    }
}
// webview发生平移,则及时更新fixed元素的定位
function handleScroll () {
    fixedEle.style.top = `${window.pageYOffset}px`
}

function handleFocusin (e) {
    var el = e || window.event
    // 在本身有输入框处于聚焦状态软键盘出现时,点击聚焦另外的输入框
    if (currentInput) {
        currentInput = el.target
        return
    }
    // 添加滚动监听,为了软键盘出现 以及 从一个聚焦输入框聚焦到另外一个输入框时, 重新定位fixed元素(其实这里不用滚动事件监听变化也可以用setTimeout来更新定位)
    window.addEventListener('scroll', handleScroll)
    currentInput = el.target
    // 监听移动手势:
    // 1. 在软键盘出现后,如果想要滚动,则收起软键盘,解绑webview的滚动监听事件
    // 2. 在软键盘出现后,用户主动收起软键盘(如点击软键盘的收起/完成等按钮),此时用户没有做移动手势,那么就会在收起软键盘后只要做了移动手势,就仍然触发绑定事件,达到解绑滚动监听事件的目的,阻止监听到webview回弹效果导致的固定顶部元素发生位移。
    window.addEventListener('touchmove', stopMove)
}

page.addEventListener('focusin', handleFocusin)

优点

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

缺点

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

方案四(借助参考物)

上述的方案三,需要调整好布局,如果你出于某些原因,不能采用上述布局,那倒还有一个方法,稍微改改,加个同样固定到顶部的元素作为参考物,利用getBoundingClientRect().top来获取到软键盘出现后,参考物随着webview平移出去距离视口的位移。该位移正是webview平移的距离。

html部分很简单,就加个固定顶部的参考物即可。

<html>
    <body>
        <div class="page">
            <!--这个是参考物-->
            <em id="flag" style="position: fixed;top: 0;"></em>
            <div class="fixed">要展示的固定顶部的内容</div>
            <div class="wrap">
               <!--这里面是包含输入框等网页的正式内容-->
            </div>
        </div>
    </body>
</html>

脚本部分如下,基本上跟上述方案三大同小异:

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

// 滚动时收起软键盘
function stopMove () {
    if (currentInput) {
        currentInput.blur()
    }
}
// webview发生平移,则及时更新fixed元素的定位
function handleScroll () {
    var top = -flag.getBoundingClientRect().top
    fixedEle.style.top = `${top}px`
}

function handleFocusin (e) {
    var el = e || window.event
    currentInput = el.target
    handleScroll() // 从一个输入框聚焦到另一个输入框时,因为上一个聚焦的输入框因为失焦导致top置为0了,如果新聚焦的输入框不会触发webview平移,则沿用当时的位移就好了
    // 添加滚动监听,为了软键盘出现 以及 从一个聚焦输入框聚焦到另外一个输入框时, 重新定位fixed元素(其实这里不用滚动事件监听变化也可以用setTimeout来更新定位)
    window.addEventListener('scroll', handleScroll)
    // 监听移动手势: 在软键盘出现后,如果想要滚动,则收起软键盘,解绑webview的滚动监听事件
    window.addEventListener('touchmove', stopMove)
}

function handleFocusout () {
    currentInput = null
    window.removeEventListener('touchmove', stopMove)
    window.removeEventListener('scroll', handleScroll)
    fixedEle.style.top = 0
}

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

优点

  • 实现起来较为简单
  • 没有了软键盘出现的再滚动,就不会需要不断变化fixed元素定位导致的不及时问题
  • 相比方案三,没有布局限制,仅需要加个参考物即可

缺点

  • 软键盘出现后一滚动就会收起
  • 相比方案三来讲,其实效果是差不多的,但是在细节表现方面,还是略差于方案三,因此我更推荐使用方案三,此处我罗列出来,更多的是为了表达还有这么一种思考方向。

方案五(交互+布局,优化)

该方案在方案三的基础上,追求极致一点的做法。方案三是直接在软键盘出现后统一监听滑动手势让输入框失焦收起软键盘,

而这里的方案则是把这种监听滑动的时机缩小,降低影响范围。

思路跟方案三一样,但调整下监听手势滑动的时机:

当H5内容滚动到极限的地方了(顶部/底部),再继续作滚动手势,就自然会平移webview,所以只需要在此时添加监听事件收起软键盘即可。

var page = document.querySelector('.page')
var currentInput = null // 当前聚焦的输入框
var fixedEle = document.querySelector('.fixed')
var startMove = false // 记录是否发生了滑动手势

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

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

// H5容器发生滚动到顶部或底部时,就收起软键盘。这个属于优化,看个人喜好要不要加
// 不加的话就会体验不太好,滚动完h5容器顶部或底部后,需要等h5容器滚动条消失后再滚动才是让webview平移,容易让用户一开始觉得是滚不动了,这样误以为底下的内容无法查看。
function handlePageScroll () {
    if (page.scrollTop < 0 || page.scrollTop + page.clientHeight > page.scrollHeight) {
        return triggerBlur()
    }
}

// webview发生平移,则及时更新fixed元素的定位
function handleWdinowScroll () {
    // 当平移时H5的内容已经滚动到顶部或底部,且 是因为发生滑动手势引起的scroll事件,此时就输入框失焦收起软键盘
    // 聚焦到输入框也会引起scroll事件,所以要加startMove区分开是滑动手势引起的
    if ((page.scrollTop === 0 || page.scrollTop + page.clientHeight >= page.scrollHeight) && startMove) {
        return triggerBlur()
    }
    // 聚焦输入框引起的平移
    fixedEle.style.top = window.pageYOffset + 'px'
}

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

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

// 失焦时,重置一些数据
function handleBlur () {
    currentInput = null
    window.removeEventListener('scroll', handleWdinowScroll)
    page.removeEventListener('touchmove', handleTouchmove)
    page.removeEventListener('scroll', handlePageScroll)
    fixedEle.style.top = 0
    startMove = false
}

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

方案六(交互+布局,极致)

该方案在方案五的思路上,做到更加极致。方案五是在H5已经滚动到顶部或底部时如果因为滚动手势引起webview的平移,则失焦收起软键盘。

如果能在H5滚动的时候就看到被平移出去的内容就好了。这样就更大幅度减少触发主动收起软键盘的概率了,做到极致!

为达到目的,我要在这基础上,再做一些改变。

增高

首先,在滚动body里的滚动条时,就希望能滚动看到网页webview被平移出去的内容,为了达到这个目的,我们需要动态增加body里滚动容器的内容,以足以滚动查看到这些内容。

增高的原则是,被平移出去了多少,就在顶部插入多高的内容,被软键盘挡住了多少,就在底部插入多高的内容。

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

处理webview的平移

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

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

那怎么应付这种情况呢?

这里有两个方式:

  1. 监听windowscroll事件,webview平移时根据平移值来不断动态计算增高的内容高度,以达到慢慢变少,逐渐恢复到原样(理想很丰满,现实很骨感,我实现了,发现效果不太好,逐渐改变的过程中页面卡卡的)
  2. 当body里的内容滚动到顶/底部时,如果再作滚动手势,则收起软键盘,取消增高的内容,一切恢复到原样

第二种方式这种效果我试了,看起来很自然,最后收起软键盘的这种交互,看人吧,因人而异,有人觉得好有人可能会吐槽,但是我觉得这种交互比上面方案三四要好,起码不是一刀切阻止滚动了。而实际这种明知滚动到顶部或底部了,接下来还继续滚的场景,也很少人会这样,所以总的来说,我是非常推荐这种方式的。

接下来主要说第二种方式。

同样地,为了不必要的麻烦,不直接设置body作为滚动容器,而是在底下用一个div来替代body作为网页的内容区。

html结构:

<html>
   <body>
       <div class="page">
           <!--这个可以写在page外面-->
           <div class="fixed">这是fixed顶部的内容栏</div>
           <!--这个是内容占位,用于顶部内容区增高-->
           <div class="prefix-placeholder"></div>
           <div>这里是内容展示部分,input在这里面</div>
           <!--这个是内容占位,用于底部内容区增高-->
           <div class="suffix-placeholder"></div>
       </div>
   </body>
</html>

css部分:

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

.page {
    overflow: auto;
}

.fixed {
    position: fixed;
    top: 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) {
       // 聚焦输入框引起的平移
       fixedEle.style.top = window.pageYOffset + 'px'
   }
}

// 当页面里的输入框聚焦时(随后会出现软键盘)
function handleFocusin (e) {
   var el = e || window.event
   currentInput = el.target
   // 延迟是为了确保软键盘出来后才开始计算,因为软键盘出来有个过程动画
   setTimeout(() => {
       // 计算过就不用再计算了,一般软键盘的高度是固定的,这样做还有一个顾虑:有些情况下,滚动的时候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
   }, 400)
   // 因为上一个聚焦的输入框因为失焦导致top置为0了,如果新聚焦的输入框不会触发webview平移,则沿用当时的位移就好了
   fixedEle.style.top = window.pageYOffset + 'px'
   // 添加滚动监听,为了软键盘出现 以及 从一个聚焦输入框聚焦到另外一个输入框时, 重新定位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.top = 0
   startMove = false
   window.removeEventListener('scroll', handleWdinowScroll)
   page.removeEventListener('touchmove', handleTouchmove)
}

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

优点

  • 实现起来不算很复杂,理解也容易理解
  • 定位效果展示良好
  • 满足绝大部分场景了,safari支持也良好
  • 效果上来讲,很完美

缺点

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

方案七

根据手势模拟滚动效果,内容滚动。

上面的方案都是尝试去不断改变fixed元素的定位,来适应webview的平移,这里的思路是反其道而行之,来让页面适应变化来发生定位或偏移动画,这样有一个好处就是,不会有上述方案看起来fixed元素定位延迟迟钝等生硬问题。

基本思路:软键盘出现后,监听掉页面的滚动touchmove事件,让页面变成定位,模拟触碰手势,监听手势向下滚动还是向上,滚动距离等情况,让页面随着手势进行重新定位,进而让页面看起来是滚动的。也可以选择让页面不是定位而是用translate进行动画滚动。

该方案我这里就不具体放实现代码了,感兴趣的小伙伴可以自己尝试一下效果。

总结

方案有点多,可能大家有点眼花缭乱。一下子接收大量信息可能屡不过来,这里简单做下总结。

固定顶部的元素之所以失效,是因为ios webview软键盘出现后发生平移,随之一起被移出去了。要解决的话只能不断根据平移出去的距离而调整定位top值。

上述是总方向,根据这个方向,不断探索解决思路:

监听webview平移不断变化fixed元素的top (方案一)

但是存在以下问题:

  1. 要区分是webview平移导致的pageYOffset变化还是本身webview内容滚动导致,麻烦。【问题1】
  2. 监听scroll变化的频次太高,且被节流了,响应时机被延迟,计算不准【问题2】

上述问题可以通过两个方向解决:

  1. 结合css布局
  2. 从交互上优化

结合Css布局(方案二)

  • 调整布局,让webview不会出现自身的滚动条,把网页内容放在H5的容器里滚动查看,从而让pageYOffset的变化只代表webview平移。 —— 解决问题1
  • 除了上述意图外,更大的作用是,在webview本身没有因为内容过长产生它自己的滚动条时,我们在界面上操作滚动查看时,是先滚动h5容器先后再平移webview。这样就减少了监听scroll执行回调的机会,减少了频次。 —— 优化问题2

从交互上优化

  • 简单粗暴,当软键盘出现后,滚动时就收起软键盘,这样就不用监听每次滚动然后重新定位了。这样就不单单是减少频次,直接频次为0,简单粗暴。—— 解决问题2
  • 那么对于解决问题1,也有两个方向,可结合上述的css布局来解决 (方案三),抑或借助参考物解决 (方案四)

追求极致—— css布局 + 交互

如果把css布局和交互设计方案相结合起来,那么思路就扩得更大了,可以做得更极致:

  1. 追求极致点,在css布局方案基础上,因为先滚动H5容器内容,所以就让页面在大部分场景下能滚动,当滚动到H5容器顶部或底部时,再继续滚动就会平移webview了,此时就应该收起软键盘(即结合交互优化方案)。这样的话,就能在保障大部分场景下可滚动,小概率下触发收起软键盘查看被平移出去的内容和软键盘遮挡的内容。(方案五)
  2. 更加极致点,就在第二点方案的基础上,想办法解决掉小概率触发收起软键盘的可能性:通过给H5滚动容器内容“增高”的手段,在容器顶部增加被平移出手机可视窗口的高度,以及底部增加软键盘遮挡webview高度内容,这样就能够在不会触发webview的平移的前提下,滚动H5的时候能看到被平移出去的内容和软键盘遮挡的内容。而当滚动到H5容器顶部或底部时,就需要收起软键盘了,避免webview的平移。(方案六)

最后总结下来, 就实现效果和复杂性等因素综合考虑,个人推荐【方案三】,这是最简单的且效果不错,如果觉得还不能满足你的需求,则用【方案六】,【方案五】跟【方案六】差不多,但是都既然复杂化了,干脆一不做二不休,做到最好吧。【方案六】是最为通用的,效果也是最完美的

当然只要能满足你的需求的,能接受的,肯定要用最简单的。

注意

这里的方案都是针对IOS导致的置顶元素失效而解决的,所以在写代码的时候,需要判断下是IOS下就加入这些脚本。

方案三可以不判断区分IOS和安卓,保持一样的表现行为。这里的方案六就一定需要了。

未经允许,请勿私自转载