移动端吸顶fixbar的解决方案详解


Posted in Javascript onJuly 17, 2019

需求背景

经常会有这样的需求,当页面滚动到某一个位置时,需要某个页面元素固定在屏幕顶部,并且有时需要连续滚动吸顶。在PC端主要的实现是通过 CSS 的 position: fixed 属性,但是在移动端,尤其是在安卓端,存在诸多的兼容性问题。

问题

position:fixed给移动端带来的问题:

  • IOS8在页面滚动时,吸顶不连续;页面滑动时,不见吸顶,页面滚动停止后,吸顶缓慢出现
  • 滚动到顶部之后,会出现两个一样的吸顶, 过一会才恢复正常。
  • footer底部输入框 focus 状态,footer 底部输入框被居中,而不是吸附在软键盘上部。
  • iPhone 4s&5 / iOS 6&7 / Safari 下,页面底部footer输入框失去焦点时,header定位出错。当页面有滚动动作时,header定位恢复正常。
  • iPhone 4 / iOS 5 / Safari下,当页面发生跳转,再退回时,fixed区域消失,当内容获得焦点时,fixed区域才显示。
  • 安卓低版本/自带浏览器,不支持fixed属性,iOS4 也是不支持 fixed 的。
  • 三星i9100(S2) / 自带浏览器,在滚屏过程中,fixed定位异常,touchend之后恢复正常。
  • 部分低版本Android对支持不好,video poster属性设置的封面图会遮挡fixed元素。
  • QQ、UC浏览器滚动页面时footer定位错误,会往上偏移,是由于地址栏收起的缘故。
  • *remind:不要在 fixed 元素中使用 input / textarea 元素。

解决方案

分别处理各个问题:

IOS

在IOS端,使用 position: sticky 这个属性,使用类似于 position: relative 和 position: absolute 的结合体。在目标区域在屏幕中可见时,它的行为就像position:relative; 而当页面滚动超出目标区域时,它的表现就像position:fixed,它会固定在目标位置。

使用时,需要加上私有前缀

position: -webkit-sticky;
position: -moz-sticky;
position: -ms-sticky;
position: sticky;

对于 position:sticky 的使用,需要注意很多的细节,sticky满足以下条件才能生效:

1、具有sticky属性的元素,其父级高度必须大于sticky元素的高度。

2、sticky元素的底部,不能和父级底部重叠。(这条不好表述,文后详细说明)

3、sticky元素的父级不能含有overflow:hidden 和 overflow:auto 属性

4、必须具有top,或 bottom 属性。

同时要注意,sticky元素仅在他父级容器内有效,超出容器范围则不再生效了。

安卓

滚动距离超过某位置时,js动态设置样式;为了防止惯性滚动引起的fix不及时的情况,在 touchstart、 touchmove 、 touchend 事件都进行监听。

// 注意处理遮罩层的位置
  var scrollHandler = function () {
    if (topLength < me.getScrollTop()) {
      target.css('position', 'fixed');
      me.replaceEle.show();
    }
    else {
      target.css('position', 'relative');
      me.replaceEle.hide();
    }
  };
  // 安卓情况下,防止惯性滚动引起的fix不及时的情况
  if (/(Android)/i.test(navigator.userAgent)) {

    $(window).on('scroll', scrollHandler);

    $(document.body).on('touchstart', scrollHandler);
    $(document.body).on('touchmove', scrollHandler);
    $(document.body).on('touchend', function () {
      scrollHandler();
      setTimeout(scrollHandler, 1000);
    });
  }

不支持sticky

如果浏览器不支持position:sticky,那么就使用js动态的在节点在fixed定位于static定位中切换,但是需要对切换过程做一些优化。

1、使用函数节流防抖减少dom操作频繁粗发,但是保证在规定时间内必须执行一次。

2、使用window.requestAnimationFrame 方法在下一帧前触发浏览器的强制同步布局,是对dom的操作能及时渲染到页面上。

3、减少对dom的读写操作,或者把dom操作把读、写操作分开,可以减少渲染次数。

原文代码

(function() {
	  function Sticky(){
	    this.init.apply(this, arguments);
	  }
	
	  /**
	   * 滚动fixed组件初始化
	   * @param {object}     setting        allocate传进来的参数
	   * @param {object}     setting.stickyNode   需要设置position:sticky的节点,通常是最外层
	   * @param {object}     setting.fixedNode   当滚动一定距离时需要fixed在顶部的节点
	   * @param {int}      setting.top      fixed之后距离顶部的top值
	   * @param {int}      setting.zIndex     fixed之后的z-index值
	   * @param {string}     setting.fixedClazz   fixed时给fixedNode添加的类
	   * @param {function}   setting.runInScrollFn 滚动期间额外执行的函数
	   * @return {void} 
	   */
	  Sticky.setting = {
	    stickyNode: null,
	    fixedNode: null,
	    top: 0,
	    zIndex: 100,
	    fixedClazz: '',
	    runInScrollFn: null
	  };
	  var sPro = Sticky.prototype;
	  var g = window;
	
	  /**
	   * 初始化
	   * @param {object} options 设置
	   * @return {void}     
	   */
	  sPro.init = function(options){
	    this.setting = $.extend({}, Sticky.setting, options, true);
	    if (options.fixedNode) {
	      this.fixedNode = options.fixedNode[0] || options.fixedNode;
	      this.stickyNode = options.stickyNode[0] || options.stickyNode;
	      this.cssStickySupport = this.checkStickySupport();
	      this.stickyNodeHeight = this.stickyNode.clientHeight;
	      this.fixedClazz = options.fixedClazz;
	      this.top = parseInt(options.top, 10) || 0;
	      this.zIndex = parseInt(options.zIndex) || 1;
	      this.setStickyCss();
	      this.isfixed = false;
	      // 把改变定位的操作添加到节流函数与window.requestAnimationFrame方法中,确保一定事件内必须执行一次
	      this.onscrollCb = this.throttle(function() {
	        this.nextFrame(this.sticky.bind(this));
	      }.bind(this), 50, 100);
	      this.initCss = this.getInitCss();
	      this.fixedCss = this.getFixedCss();
	      this.addEvent();
	    }
	  };
	
	  /**
	   * 获取原始css样式
	   * @return {string} 定位的样式
	   */
	  sPro.getInitCss = function() {
	    if (!!this.fixedNode) {
	      return "position:" + this.fixedNode.style.position + ";top:" + this.fixedNode.style.top + "px;z-index:" + this.fixedNode.style.zIndex + ";";
	    }
	    return "";
	  };
	
	  /**
	   * 生成fixed时的css样式
	   * @return {void}
	   */
	  sPro.getFixedCss = function() {
	    return "position:fixed;top:" + this.top + "px;z-index:" + this.zIndex + ";";
	  };
	
	  /**
	   * 给fixedNode设置fixed定位样式
	   * @param {string} style fixed定位的样式字符串
	   */
	  sPro.setFixedCss = function(style) {
	    if(!this.cssStickySupport){
	      if (!!this.fixedNode){
	        this.fixedNode.style.cssText = style;
	      }
	    }
	  };
	
	  /**
	   * 检查浏览器是否支持positon: sticky定位
	   * @return {boolean} true 支持 false 不支持
	   */
	  sPro.checkStickySupport = function() {
	    var div= null;
	    if(g.CSS && g.CSS.supports){
	      return g.CSS.supports("(position: sticky) or (position: -webkit-sticky)");
	    }
	    div = document.createElement("div");
	    div.style.position = "sticky";
	    if("sticky" === div.style.position){
	      return true;
	    }
	    div.style.position = "-webkit-sticky";
	    if("-webkit-sticky" === div.style.position){
	      return true;
	    }
	    div = null;
	    return false;
	  };
	
	  /**
	   * 给sticyNode设置position: sticky定位
	   */
	  sPro.setStickyCss = function() {
	    if(this.cssStickySupport){
	      this.stickyNode.style.cssText = "position:-webkit-sticky;position:sticky;top:" + this.top + "px;z-index:" + this.zIndex + ";";
	    }
	  };
	
	  /**
	   * 监听window的滚动事件
	   */
	  sPro.addEvent = function() {
	    $(g).on('scroll', this.onscrollCb.bind(this));
	  };
	
	  /**
	   * 让函数在规定时间内必须执行一次
	   * @param {Function} fn   定时执行的函数
	   * @param {int}   delay 延迟多少毫秒执行
	   * @param {[type]}  mustRunDelay 多少毫秒内必须执行一次
	   * @return {[type]}   [description]
	   */
	  sPro.throttle = function(fn, delay, mustRunDelay){
	    var timer = null;
	    var lastTime;
	    return function(){
	      var now = +new Date();
	      var args = arguments;
	      g.clearTimeout(timer);
	      if(!lastTime){
	        lastTime = now;
	      }
	      if(now - lastTime > mustRunDelay){
	        fn.apply(this, args);
	        lastTime = now;
	      }else{
	        g.setTimeout(function(){
	          fn.apply(this, args);
	        }.bind(this), delay);
	      }
	    }.bind(this);
	  };
	
	  /**
	   * window.requestAnimationFrame的兼容性写法,保证在100/6ms执行一次
	   * @param {Function} fn 100/16ms需要执行的函数
	   * @return {void}   
	   */
	  sPro.nextFrame = (function(fn){
	    var prefix = ["ms", "moz", "webkit", "o"];
	    var handle = {};
	    handle.requestAnimationFrame = window.requestAnimationFrame;
	    for(var i = 0; i < prefix.length && !handle.requestAnimationFrame; ++i){
	      handle.requestAnimationFrame = window[prefix[i] + "RequestAnimationFrame"];
	    }
	    if(!handle.requestAnimationFrame){
	      handle.requestAnimationFrame = function(fn) {
	        var raf = window.setTimeout(function() {
	          fn();
	        }, 16);
	        return raf;
	      };
	    }
	    return function(fn) {
	      handle.requestAnimationFrame.apply(g, arguments);
	    }
	  })();
	
	  /**
	   * 判断stickyNode的当前位置设置fixed|static|sticky定位
	   * @return {void}
	   */
	  sPro.sticky = function() {
	    this.setting.runInScrollFn && this.setting.runInScrollFn();
	    var stickyNodeBox = this.stickyNode.getBoundingClientRect();
	    if(stickyNodeBox.top <= this.top && !this.isfixed){
	      this.setFixedCss(this.fixedCss);
	      this.fixedClazz && $(this.fixedNode).addClass(this.fixedClazz);
	      this.isfixed = true;
	      $(this).trigger('onsticky', true);
	    } else if(stickyNodeBox.top > this.top && this.isfixed) {
	      this.setFixedCss(this.initCss.replace(/position:[^;]*/, "position:static"));
	      g.setTimeout(function() {
	        this.setFixedCss(this.initCss)
	      }.bind(this), 30);
	      this.fixedClazz && $(this.fixedNode).removeClass(this.fixedClazz);
	      this.isfixed = false;
	      $(this).trigger('onsticky', true);
	    }
	  };
	
	  $.initSticky = function(options){
	    return new Sticky(options);
  	};
})();

html 结构:

<div class="m-nav">
 		<div class="nav-fixed fixed" id="j-nav" style="position: fixed; top: 0px; z-index: 100;">
   	<ul class="f-cb">
     	<li class="active" anchor-id="j-understand">了解儿童编程</li>
      		<li anchor-id="j-join">参与公益直播课</li>
         <li anchor-id="j-upload">上传编程作品</li>
     </ul>
   </div>
</div>

css 结构:

.g-page-box .m-nav {
 	height: 1.33333rem;
}

.g-page-box .m-nav .nav-fixed {
	 height: .86667rem;
	 padding: .22667rem .50667rem;
	 background-color: #1aadbb;
	 position: relative;
	 transform: translate3d(0, 0, 0);
	 -webkit-transform: translate3d(0, 0, 0);
	 transition: height 4s;
}

.fixed {
	position: fixed;
	top: 0px;
	z-index: 100;
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
兼容FireFox 的 js 日历 支持时间的获取
Mar 04 Javascript
高效率JavaScript编写技巧整理
Aug 23 Javascript
火狐下table中创建form导致两个table之间出现空白
Sep 02 Javascript
javascript生成随机颜色示例代码
May 05 Javascript
使用jQuery设置disabled属性与移除disabled属性
Aug 21 Javascript
JavaScript中document.forms[0]与getElementByName区别
Jan 21 Javascript
javascript修改图片src的方法
Jan 27 Javascript
微信开发 使用picker封装省市区三级联动模板
Oct 28 Javascript
原生js代码实现图片放大境效果
Oct 30 Javascript
ES6实现的遍历目录函数示例
Apr 07 Javascript
VueCli3构建TS项目的方法步骤
Nov 07 Javascript
JS 获取文件后缀,判断文件类型(比如是否为图片格式)
May 09 Javascript
基于webpack4+vue-cli3项目实现换肤功能
Jul 17 #Javascript
js getBoundingClientRect使用方法详解
Jul 17 #Javascript
深入了解Hybrid App技术的相关知识
Jul 17 #Javascript
Vue发布项目实例讲解
Jul 17 #Javascript
Vue项目实现简单的权限控制管理功能
Jul 17 #Javascript
百度小程序自定义通用toast组件
Jul 17 #Javascript
微信小程序自定义弹窗滚动与页面滚动冲突的解决方法
Jul 16 #Javascript
You might like
php _autoload自动加载类与机制分析
2012/02/10 PHP
php操作redis中的hash和zset类型数据的方法和代码例子
2014/07/05 PHP
Zend Framework教程之资源(Resources)用法实例详解
2016/03/14 PHP
thinkphp5实现微信扫码支付
2019/12/23 PHP
些很实用且必用的小脚本代码
2006/06/26 Javascript
列表内容的选择
2006/06/30 Javascript
扩展String功能方法
2006/09/22 Javascript
extJs 常用到的增,删,改,查操作代码
2009/12/28 Javascript
Javascript Function对象扩展之延时执行函数
2010/07/06 Javascript
jQuery源码分析-02正则表达式 RegExp 常用正则表达式
2011/11/14 Javascript
jquery click([data],fn)使用方法实例介绍
2013/07/08 Javascript
BootStrap的table表头固定tbody滚动的实例代码
2016/08/24 Javascript
Web前端框架Angular4.0.0 正式版发布
2017/03/28 Javascript
vue.js实现用户评论、登录、注册、及修改信息功能
2020/05/30 Javascript
JS实现给json数组动态赋值的方法示例
2020/03/19 Javascript
vue项目打包之后背景样式丢失的解决方案
2019/01/17 Javascript
vue 中使用 watch 出现了如下的报错的原因分析
2019/05/21 Javascript
微信小程序实现的picker多级联动功能示例
2019/05/23 Javascript
javascript实现blob加密视频源地址的方法
2019/08/08 Javascript
[01:59]深扒TI7聊天轮盘语音出处 1
2017/05/11 DOTA
Python基于sftp及rsa密匙实现远程拷贝文件的方法
2016/09/21 Python
读取本地json文件,解析json(实例讲解)
2017/12/06 Python
python 字典套字典或列表的示例
2019/12/16 Python
多个python文件调用logging模块报错误
2020/02/12 Python
Python使用QQ邮箱发送邮件实例与QQ邮箱设置详解
2020/02/18 Python
介绍一下常见的木马种类
2014/11/15 面试题
毕业生自荐信的主要内容
2013/10/29 职场文书
电气自动化个人求职信范文
2014/02/03 职场文书
社会实践的活动方案
2014/08/22 职场文书
2014年学校国庆主题活动方案
2014/09/16 职场文书
业绩倒数第一的检讨书
2014/09/24 职场文书
应聘教师求职信范文
2015/03/20 职场文书
党员干部廉政承诺书
2015/04/28 职场文书
《学会看病》教学反思
2016/02/17 职场文书
MySQL的存储函数与存储过程的区别解析
2022/04/08 MySQL
使用CSS实现音波加载效果
2023/05/07 HTML / CSS