移动端软键盘遮挡问题

纵然是瞬间 提交于 2020-01-22 05:35:33

scrollIntoView 介绍

移动端的H5页面,当输入框元素获取焦点时,会吊起软键盘,如果输入框被软键盘遮挡了,则页面会发生滚动使输入框显示在可视区。浏览器这种默认处理机制在元素设置了绝对定位或设置了html,body{height:100%;}时可能会失效,通常需要手动处理。

防软键盘遮挡的处理思路:

  • IOS系统中,软键盘吊起时,整个 window 往上滚,window的宽高不变,不会触发window.onresize 事件。并且,软键盘始终不会遮挡输入框,所以在ios中不处理。有些版中的微信页面存在页面不回滚的bug,这个需要处理。

  • 安卓系统中,软键盘吊起时,window 不滚动,window的高度减小,减少值等于软键盘的高度,会触发 window.onresize 事件。如果输入框被遮挡,可使用 scrollIntoView 方法强制在可视区显示获取焦点的输入框元素。

让当前的元素滚动到浏览器窗口的可视区域内方法。 MDN
scrollIntoView() 浏览器支持度较高(推荐)。
scrollIntoViewIfNeeded() 部分浏览器支持,懒滚动模式。

scrollIntoView()
Element.scrollIntoView() 方法让当前的元素滚动到浏览器窗口的可视区域内。
这是一个实验中的功能,浏览器支持:PC端和移动端都支持

语法:

element.scrollIntoView();        // 等同于element.scrollIntoView(true) 
element.scrollIntoView(bool);    // Boolean型参数 
element.scrollIntoView(options); // Object型参数

bool 可选,一个Boolean值:如果为true,元素的顶端将和其所在滚动区的可视区域的顶端对齐。相应的 options: {block: “start”, inline: “nearest”}。这是这个参数的默认值。如果为false,元素的底端将和其所在滚动区的可视区域的底端对齐。相应的options: {block: “end”, inline: “nearest”}。
options 可选, 一个包含下列属性的对象:
behavior 可选,定义动画过渡效果, "auto"或 “smooth” 之一。默认为 “auto”。
block 可选,定义垂直对齐, “start”, “center”, “end”, “nearest”。默认为 “start”。
inline 可选,定义水平对齐, “start”, “center”, “end”, “nearest”。默认为 “nearest”。

var element = document.getElementById("box");

element.scrollIntoView();
element.scrollIntoView(false);
element.scrollIntoView({block: "end"});
element.scrollIntoView({behavior: "instant", block: "end", inline: "nearest"});

scrollIntoViewIfNeeded()
Element.scrollIntoViewIfNeeded()方法用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。 如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动。 此方法是标准的Element.scrollIntoView()方法的专有变体。

这是一个非标准的功能,浏览器支持:移动端几乎都支持
在这里插入图片描述
语法:

element.scrollIntoViewIfNeeded(); 
// 等同于element.scrollIntoViewIfNeeded(true) 

element.scrollIntoViewIfNeeded(false); 
element.scrollIntoViewIfNeeded(true); 

如果为true,则元素将在其所在滚动区的可视区域中居中对齐。
如果为false,则元素将与其所在滚动区的可视区域最近的边缘对齐。 根据可见区域最靠近元素的哪个边缘,元素的顶部将与可见区域的顶部边缘对准,或者元素的底部边缘将与可见区域的底部边缘对准。

示例:

var element = document.getElementById("child"); 

element.scrollIntoViewIfNeeded();
element.scrollIntoViewIfNeeded(true);
element.scrollIntoViewIfNeeded(false);

1.利用 scrollIntoView 实现防遮挡

1.针对某个元素单独处理

var inp = document.getElementById('inp');
//安卓中获取焦点时防遮挡
inp.onfocus = function(){
  if(this.scrollIntoViewIfNeeded){
    //懒滚动,当在可见区时不发生滚动,元素不再可见区时滚动到可见区中部
    this.scrollIntoViewIfNeeded(true);  
  }else{
    //无论怎样都会滚动到与可见区底部对齐
    this.scrollIntoView(false);           
  }  
}
//ios 中失去焦点时回滚
inp.onblur = function(){
   window.scrollTo(0, 0)
}

2.全局处理

//安卓中获取焦点时防遮挡
window.onresize = function () {
  if (document.activeElement.scrollIntoViewIfNeeded) {
    //懒滚动,当在可见区时不发生滚动,元素不再可见区时滚动到可见区中部
    document.activeElement.scrollIntoViewIfNeeded(true); 
  } else {
    //无论怎样都会滚动到与可见区底部对齐
    document.activeElement.scrollIntoView(false);
  }
}
//ios 中失去焦点时回滚
document.onkeyup = function(e){
   var name = e.target.tagName
   if (name != 'INPUT' && name != 'SELECT' && name != 'TEXTAREA') {
       window.scrollTo(0, 0)
   }
}

3.由于上面两种简单处理方式可能存在有些手机不触发等细节问题。推荐用下面这种方式。

<input type="text" onfocus="SV.focus(this)" name="">  
<script>
let SV = {
  _el:null,
  _isFocusNow:false,
  _ua:window.navigator.userAgent,
  _init:false,
  _scroll:function(){
  	var el = this._el;
  	if(!el) return false;
    if(el.scrollIntoViewIfNeeded){
      el.scrollIntoViewIfNeeded(true);    
    }else{
      el.scrollIntoView(false);           
    }	  	
  },
  _toTop:function(el){
  	var _t = this;
  	_t._isFocusNow = false;
	setTimeout(function(){
		if(!_t._isFocusNow){
			window.scrollTo(0, 0)
		} 
	},100) 
  },
  // 添加事件句柄
  on(elem, type, listener) {
    if (elem.addEventListener) {
      elem.addEventListener(type, listener, false)
    } else if (elem.attachEvent) {
      elem.attachEvent('on' + type, listener)
    } else {
      elem['on' + type] = listener
    }
  },
  // 移除事件句柄
  off(elem, type, listener) {
    if (elem.removeEventListener) {
      elem.removeEventListener(type, listener)
    } else if (elem.detachEvent) {
      elem.detachEvent('on' + type, listener)
    } else {
      elem['on' + type] = null
    }
  },
  init(){
  	var _t = this;
  	this._init = true;
	this.on(window,'resize',function(){
		setTimeout(()=>{ _t._scroll(); },0)
	})
  },
  focus(el){	
  	!this_init && this.init();
    //ios中失去焦点时回滚
    if(!!this.ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)){
      this._el && this.off(this._el,'blur',this._toTop);
      this.on(el,'blur',this._toTop)         
    }
    this._isFocusNow = true; 
    this._el = el;
  }
}
</script>

2.利用 scroll 实现防遮挡

原理:根据输入框在屏幕中的位置判断是否被键盘遮挡,如果遮挡了,则滚动外层元素使输入框显示在可视区域。

1.自己封装的 SVModel.js

var SVModel = {
  box:null,             //储存滚动的包裹元素
  input:null,           //储存当前获取焦点的元素 
  result:0,             //储存应该滚动的值, 如果为负数则不需要滚动
  kbh:0,                //储存屏幕变化的高度(软键盘的高度)
  scroll:0,             //滚动元素当前滚动的距离
  top:0,                //焦点元素距离屏幕顶部的距离
  //屏幕高度
  SH1: window.innerHeight || document.documentElement.clientHeight 
  || document.body.clientWidth,    //初始化时屏幕高度
  SH2: 0,               //储存 resize 后屏幕高度
  //初始化函数
  init:function(){
    //1取消系统默认操作: 软键盘吊起后,复原box原滚动位置,等于取消系统默认的防遮挡滚动判定
    if(typeof this.default == 'function'){
      this.default(this.scroll)
    }else{
      this.box.scrollTop = this.scroll
    }     
    //2获取软键盘吊起后的屏幕高度
    this.SH2 = window.innerHeight || 
    document.documentElement.clientHeight || document.body.clientWidth;  
    //3.获取软键盘吊起前后,屏幕的高度差,结果即软键盘的高度         
    this.kbh = this.SH1 - this.SH2;   
    //4.焦点元素距离屏幕顶部距离加上软键盘高度与屏幕高比较大小得出结果为正数时则被遮挡了,需要滚动
    this.result = Math.round(this.kbh - (this.SH1 - this.top) + this.add);    
    //5.当屏幕变小(获取焦点时),且当前元素被遮挡时
    if( this.kbh > 0  && this.result > 0 ){   
      if(typeof this.open == 'function'){
        this.open(this.scroll + this.result)
      }else{
        this.box.scrollTop = this.scroll + this.result;
      }   
    }
    if( this.kbh == 0 && this.cz > 0 ){
      if(typeof this.close == 'function'){
        this.close(this.scroll)
      }else{
        this.box.scrollTop = this.scroll;
      }
    }
  },
  run:function(obj){
    this.box = obj.box;
    this.input = obj.input ? obj.input : document.activeElement;
    //1.软键盘未吊起前,box元素向上滚动的距离
    this.scroll = !isNaN(+obj.boxScroll) ? 
                  +obj.boxScroll : 
                  Math.round(this.box.scrollTop);  
    //2.软键盘未吊起前,焦点元素底部距离屏幕顶部的距离
    this.top = Math.round(this.input.getBoundingClientRect().bottom);  
    //3.滚动后的偏移量
    this.add = !isNaN(+obj.add) ? +obj.add : 30
    //4.扩展处理软键盘吊起时,处理焦点元素初始函数
    this.default = obj.default
    //5.扩展处理软键盘吊起时,处理焦点元素滚动函数
    this.open = obj.open
    //6.扩展处理软键盘收起时,处理焦点元素滚动函数
    this.close = obj.close
  },
  // 添加事件句柄
  on(el, type, fn) {
    if(el.addEventListener){
      el.addEventListener(type, fn, false)
    }else{
      el.attachEvent('on' + type, fn)
    } 
  },
  // 移除事件句柄
  off(el, type, fn) {
    if(el.removeEventListener){
      el.removeEventListener(type, fn)
    }else{
      el.detachEvent('on' + type, fn)
    }
  },
  blur(){
    setTimeout(()=>{
      var name = document.activeElement.tagName
      if(name != 'INPUT' || name != 'TEXTAREA'){
        window.scrollTo(0, 0)           //  ios中滚回
      }
    },0)
  }
};
SVModel.on(window, 'resize', function(){
  SVModel.init()
});  

使用示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" 
    content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>scrollTop</title>
    <script src="SVModel.js"></script>
    <style type="text/css">
    *{ margin:0; padding:0; box-sizing: border-box; }
    html,body{ width: 100%; height: 100%; font-size: 15px; overflow: hidden; }
    #box{ height: 100%; width: 100%; overflow: hidden; overflow-y: auto; }
    </style>
    <script>
      function aaa(box, input){
        SVModel.run({
          // add:0,    默认值为30, 滚动偏移量
          box: box,
          input: input
        })  
      }  
      function bbb(){
        SVModel.blur()
      } 
    </script>
  </head>
  <body>
    <div id="box">
      <input type="text" onfocus="aaa(this.parentNode,this)" onblur="bbb()" />
    </div>  
  </body>
</html>  

2.SVModel.js 使用案例2(原生scroll滚动 + MUI滚动插件)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" 
    content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>键盘防遮挡+mui滚动</title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script src="mui.js"></script>
    <script src="SVModel.js"></script>
    <style type="text/css">
    *{ margin:0; padding:0; box-sizing: border-box; }
    html,body{ 
      width: 100%; 
      height: 100%; 
      font-size: 15px; 
      text-align: center; 
      overflow: hidden; 
    }
    input{ height: 30px; width: 100%; height: 40px; }
    #app{ height: 100%; height: 100%; }
    #app>div{ 
      float: left; 
      width: 50%; 
      height: 100%; 
      overflow: hidden;
      border:1px solid red; 
    }
    .mui-scroll-wrapper {
      position: relative;
      z-index: 2;
      top: 0;
      bottom: 0;
      left: 0;
      overflow: hidden;
      width: 100%;
    }
    .mui-scroll {
      min-height: 101%;
      position: absolute;
      z-index: 1;
      width: 100%;
      -webkit-transform: translateZ(0);
      transform: translateZ(0);
    }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 原生滚动防遮挡 -->
      <div style="overflow: auto;" ref="box">
        <input type="text" :value="i" v-for="i in num1" :key="i" 
        @focus="focus1($event)" @blur="blur"/>
      </div>  
      <!-- mui滚动插件防遮挡 -->
      <div class="mui-scroll-wrapper" ref="muiBox" @touchmove.prevent>
        <div class="mui-scroll">
          <input type="text" :value="i" v-for="i in num2" :key="i" 
          @focus="focus2($event)" @blur="blur"/>
          <div style="padding:20px 0">加载中...</div>
        </div>         
      </div>
    </div>
  </body>
</html>
<script>
new Vue({
  el:"#app",
  data:{
    num1:20,
    num2:20,
  },
  mounted(){
    var _this = this 
    var scroll = mui('.mui-scroll-wrapper').scroll({indicators: false})    
    this.$refs.muiBox.addEventListener('scroll', function (e){
      //判断滚动到底部, 上拉加载逻辑代码
      if(scroll.y == scroll.maxScrollY){
        _this.load()
      }
    }) 
  },
  methods:{
    focus1(e){
      SVModel.run({
        // add:0,    默认值为30, 滚动便宜量
        box: this.$refs.box,
        input: e.target
      })
    },
    focus2(e){
      SVModel.run({
        add:20,    //默认值为30, 滚动偏移量
        input: e.target,
        boxScroll: Math.abs(mui('.mui-scroll-wrapper').scroll().y), //mui中向上是负值  
        default:function(n){
          mui('.mui-scroll-wrapper').scroll().scrollTo(0,-n,0);
        },     
        open:function(n){
          mui('.mui-scroll-wrapper').scroll().scrollTo(0,-n,100);
        },
        close:function(n){
          mui('.mui-scroll-wrapper').scroll().scrollTo(0,-n,100);
        }
      })
    },
    blur(){
      SVModel.blur()    
    },
    load(){
      setTimeout(()=>{ this.num2 += 10 },1000)
    }
  }
})
</script>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!