【移动端bug】iOS 下 Input 和 fixed 的问题

2021-05-13 10:49:33 浏览数 (2)

把工作中做过的一些小东西或者功能总结记录,分享学习

最近在项目中碰到了移动端 IOS 下的一些问题,就打算完整总结一下,以便后续碰到相关问题就不用浪费时间了

你们做移动端页面开发,绝逼也会碰到这个问题的,迟早的问题而已,这种兼容性问题真的是很烦人的,文章很长,看是不可能看的了,所以收藏备用吧

本次文章主要描述两个问题

1、IOS11 下,键盘弹起时导致的光标错位

2、IOS13 下,键盘弹起再收起时导致的 DOM 错位

先来简单描述一下这两个问题

第一个问题

IOS11 下,当你激活定位元素中的输入框的时候,就会发生光标错位

第二个问题

IOS13 下,当你激活定位元素中的输入框时,然后输入框失焦,然后再激活的时候,就会发生DOM 错位

好的,下面我们就来一个个详细地描述这些问题

通过4个方面来探索一下

1、怎么出现的问题

2、猜想一下原因

3、验证一下猜想

4、问题的解决办法

1

IOS11光标错位

一开始以为是 IOS11 下碰到的这个问题

所以发现怎么有时有这个问题,有时又没有。。。还以为不是必现的,害得我纠结了很久后来才发现是因为我使用了不同的浏览器。。

1什么时候会出现

我就列出出现这些问题的包含的元素

  1. ios11
  2. safari 浏览器
  3. 定位元素中有输入框
  4. 定位元素输入框激活时,页面还有很多内容,仍然能往上滚动

来看一下实际的表现是怎么样的

2探索一下原因

正如我上面说,只有在定位元素的输入框被激活时,页面仍有很多内容,仍能往上滚的时候,才出现光标错位的问题

那么 首先,观察一哈这个光标错位时的位置

好像是键盘没有唤起时,定位元素输入框的位置啊

那么说明什么

是不是虽然看着元素被键盘顶上去了,但是实际上DOM 位置还停留在原地?

然后我们还需要明确一个事情,就是

当激活定位元素的输入框时,页面没有内容了,无法往上滚的时候

那么是不会出现光标错位的问题的,像下面这样

所以说明此时,定位元素的 DOM 就不是像上面那样停留在原地了,而是也被顶上去了

上面我们暂且得出一个结果

1、定位元素输入框,唤起键盘,页面可以往上滚动的话,定位元素的 实际DOM 会停留在原地

2、定位元素输入框,唤起键盘,页面不能往上滚动的话,定位元素的 实际DOM 跟随页面被顶上去

上面我们得出了结论,那么我们来证明一下是否我们的结论是否正确

3证明一下猜想

1、证明光标错位时,定位元素实际DOM保留在原位

我获取了正常显示时 和 聚焦时的 输入框距离浏览器顶部的高度,如下图

两者的高度是一样的!

聚焦时的距顶高度 应该要比 正常显示的要小 的猜对,所以证明了我们的猜想是对的

定位元素保留在了原位!!

2、 证明是否页面已经滚到底部时,唤起键盘,定位元素实际DOM被顶上去

获取了正常显示时 和 聚焦时的 输入框距离浏览器顶部的高度,如下图

两者高度不一样了!!

说明实际DOM 的位置也被顶上去了,没有停留在原地

上面我们知道,光标错位的时定位元素实际dom停在了原地

所以我想知道会不会页面文档上虽然看着是往上滚动了,但是Dom也还是停在原地

所以也要证明一下

3、页面文档被顶上去时,实际dom是否也停在原地?

我们就以页面上的一个按钮来做示例

最后查看一下正常时按钮的距顶高度,和 定位元素输入框聚焦时的距顶高度,如下图

你可以看到,聚焦之后,距顶高度变小了,说明往上滚动了

说明,页面文档的元素并不是像 定位元素那样 实际DOM 停留在原地

4为什么会这样

究其原因,其实是 iOS 系统的bug,后续的系统已经修复了

5解决方法

虽然是系统bug,但是我们要照顾这部分人群,总不能让人换手机,只能自己解决了

先想想,当页面滚动到底部时,激活定位元素的输入框,是不会出现光标错位的

是不是说明,只要页面无法滚动了,那么就能解决光标错位的问题?

在网上也查了3种办法

1、弹窗出现时,给body 设置 overflow hidden,弹窗关闭再重置

但是好像我试了一下并没有什么卵用

所以我打算使用第二种

2、弹窗使用 absolute

弹窗不适用 fixed 定位,查了如果在fixed 元素中有input元素,然后input 元素激活的时候,就会出现这个问题

如果弹窗使用 absolute 定义,那么就不会有问题

但是这样整个页面也要做处理,页面不再是 撑开body,而是某个元素占满全屏,然后内部进行滚动

这样 弹窗就可以完美使用 absolute 定位了

但是我觉得这样很麻烦,如果是后面加的需求,不可能有把整个结构改动,这样出问题可能兜不住。。

所以我还是偏向于下面这个方式

3、弹窗出现时,给 html 元素设置 position:fixed,弹窗关闭再重置

但是这样有一个问题,就是设置的时候会丢失页面滚动高度,当然就是体验不好了

所以我打算这么优化一下

  1. 先获取并保存到当前页面滚动高度
  2. 给 html 设置 fixed 的时候,把 top 设置成保存的滚动高度
  3. html 重置的时候,再使用 scrollTop 滚到相应位置

具体如下

代码语言:javascript复制
function BodyScroll() {
  return {
    scrollY: 0,
    lock() {      
      if (verNum !== 11) return;      
      this.scrollY = getScrollPosition().y;
      $('html').css({        
        position: 'fixed',        
        width: '100%',        
        height: '100%',
        top: -this.scrollY   'px',
      });

    },
    unLock() {  
      if (verNum !== 11) return;
      $('html').css({        
         position: '',        
         width: '',        
         height: '',
         top: '',
      });
      window.scrollTo(0, this.scrollY - 1);
    },
  };
}

然后这么使用

然后再列出上面用到的工具函数

代码语言:javascript复制
function getIOSVersion() {  
 const str = navigator.userAgent.toLowerCase();
 const ver = str.match(/cpu iphone os (.*?) like mac os/);
 let verNum = 0;  
 if (
   Array.isArray(ver) &&
   typeof ver[1] !== 'undefined'
 ) {    
   const verResult = ver[1].match(/(d ?)_/);
   verNum = verResult === null ? 0 : verResult[1];
 }
 return Number(verNum);
}
const verNum = getIOSVersion();
function debounce(fn, delay, immediate) {
  let timer = null;  
  return function (...args) {    
   const context = this;
    clearTimeout(timer);    
    const callNow = !timer;
    timer = window.setTimeout(() => {
      fn.apply(context, args);
      timer = null;
    }, delay);    

    if (immediate && callNow) {

      fn.apply(context, args);
    }
  };
}


function isIOS() {  
  if (/iPhone|iPod|iPad/i.test(navigator.userAgent)) {    
    return true;
  }  
  return false;
}

2

IOS13Dom错位

其他就不多哔哔了,直接进入主题

1什么时候会出现

继续列出出现这些问题的包含的元素

1. ios13

2. qq软件内嵌浏览器

3. 定位元素中有输入框

4. 定位元素输入框激活时,页面已经滚到底部

下面来看下实际表现情况

因为动图可能不好看明白,直接用三张图

第一步,正常情况下,定位元素出现在页面中,保证此时底部的页面已经滑到底部,无法往上滚动

第二步,开始激活定位元素中的输入框,键盘被唤起,定位元素被顶上去

第三步,点击键盘右上角的【完成】,输入框失焦,键盘收起

然后再次点击输入框,尝试激活唤起键盘,但是已经无法激活了

没错,做完这三步,这个问题就出现了

2探索一下原因

经过一些尝试,当出现这个问题的时候

我去点击输入框的时候上方一些位置的话,就能激活输入框

然后我尝试确定一下这个位置,发现 DOM 实际位置停留在了之前唤起键盘的位置,如图这样

然后我们还要知道另一个事情,就是

当页面没有滚到底部时,就激活定位元素中的输入框,那么显示就会是正常的

看下图,页面很长,出现弹窗时,没有滚到底部

那么我们从上面两种现象,可以得出一个结论

1、页面已经滚动到底, 定位元素输入框,唤起键盘,再收起键盘,定位元素的 实际DOM 会停留在唤起键盘的位置 ,跟显示的元素错位了

2、页面没有滚动到底,定位元素输入框,唤起键盘,定位元素的 实际DOM 就是正常的

3证明一下猜想

1 、证明聚焦再失焦,定位元素的实际dom是否跟显示元素错位了

我对比了 新打开的定位元素输入框距顶高度 和 聚焦又失焦操作后的 定位元素输入框距顶高度,如下

发现,的确高度不一样,的确实际DOM 和 显示的元素 错位了

2 、证明没有滚动到底部时,实际DOM 的位置是正常的,和显示元素对应

4为什么会这样

你仔细观察,在输入框被激活,唤起键盘时,页面的内容会被往上顶,从而往上滚动一些

所以当我们滚动到底部 再激活输入框的时候,按照惯例,它仍然会把页面往上顶

但是已经没有内容给你顶了啊,那怎么办,直接整个文档都给你顶上去了

所以整个文档都被顶上去了,所有DOM 的位置当然都会往上偏移顶上去的这部分距离

但是你看到整个DOM偏移的过程,定位元素因为都是一直显示的,以整个窗口为定位的,所以就会造成错位但是如果你关闭了定位元素,再打开,就不会这样了

定位元素就会重新渲染,此时DOM 也就不会偏移了

5解决办法

现在我们知道这个问题

“ 因为滚动到底部时,键盘强行把页面顶上去一部分,并且失焦时,页面没有复位 ”

所以我们可以在 输入框失焦的时候,把页面复位就好了

通常最简单的办法是

代码语言:javascript复制
window.scrollTop(0)

直接滚动到顶,从而复位但是这样带来的问题就是体验不好,用户丢失了浏览高度

所以打算是

1、在输入框激活时,保存页面浏览的高度

2、输入框失焦时,获取保存的浏览高度,然后滚动到相应的位置

3、输入框失焦聚焦时要进行防抖处理。否则多个输入框切换的时候,每次切换都会scrollTop滚动没必要,应该要等到当前完全没有输入框聚焦时才开始滚动,所以让 focus 和 blur 相互抵消

代码语言:javascript复制
class Ios13FixDomMisplace {
  scrollY = 0;
  blur = () => {    
    this.bindEvent('blur');

  };
  focus = () => {
    //  只有等于0才获取高度
    if (this.scrollY === 0) {      
      this.scrollY = getScrollPosition().y;
   }    

    this.bindEvent('focus');
  };

  bindEvent = (() => {  
    if (!isIOS) {      
      return () => {};
    }    

    // 只有在 IOS13 才 滚动回原来位置
    const timer = debounce((type) => {      

      if (type === 'blur' && verNum ===13) {        

         window.scrollTo(0, this.scrollY - 1);        

         this.scrollY = 0; // 重置
      }
     }, 200);    

    return timer;
  })();

}

然后在输入框中就这么调用

代码语言:javascript复制
var ios13FixDomMisplace= newIos13FixDomMisplace();

$('input')
  .on('blur', () => ios13_fix_dom_misplace.blur();)
  .on('focus', () => ios13_fix_dom_misplace.focus(););

然后再列出上面用到的工具函数

代码语言:javascript复制
function getIOSVersion() {  
  const str = navigator.userAgent.toLowerCase();  
  const ver = str.match(/cpu iphone os (.*?) like mac os/);
  let verNum = 0;
  if (
    Array.isArray(ver) &&
    typeof ver[1] !== 'undefined'

  ) {  
    const verResult = ver[1].match(/(d ?)_/);
    verNum = verResult === null ? 0 : verResult[1];

  }  
  return Number(verNum)
}
const verNum = getIOSVersion();
function debounce(fn, delay, immediate) {
  let timer = null;  
  return function (...args) {  
   const context = this;
   clearTimeout(timer);  
   const callNow = !timer;
   timer = window.setTimeout(() => {
      fn.apply(context, args);
      timer = null;

    }, delay);    

    if (immediate && callNow) {
      fn.apply(context, args);
    }
  };

}


function isIOS() {  
  if (/iPhone|iPod|iPad/i.test(navigator.userAgent)) {  
    return true;
  }  
  return false;
}

最后

鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵, 如果有任何描述不当的地方,欢迎后台联系本人;

0 人点赞