移动端吸顶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 相关文章推荐
javascript 读取XML数据,在页面中展现、编辑、保存的实现
Oct 27 Javascript
jQuery fadeTo方法调整图片的透明度使用介绍
May 06 Javascript
通过location.replace禁止浏览器后退防止重复提交
Sep 04 Javascript
JavaScript实现非常简单实用的下拉菜单效果
Aug 27 Javascript
js动态添加的DIV中的onclick事件简单实例
Jul 25 Javascript
原生js实现仿window10系统日历效果的实例
Oct 31 Javascript
vue中接口域名配置为全局变量的实现方法
Sep 20 Javascript
微信小程序实现弹出层效果
May 26 Javascript
微信小程序 简易计算器实现代码实例
Sep 02 Javascript
JS+Canvas实现五子棋游戏
Aug 26 Javascript
JS操作JSON常用方法(10w阅读)
Dec 06 Javascript
Vue中避免滥用this去读取data中数据
Mar 02 Vue.js
基于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
4.与数据库的连接
2006/10/09 PHP
php gd等比例缩放压缩图片函数
2016/06/12 PHP
PHP自定义错误处理的方法分析
2018/12/19 PHP
PHP类的自动加载与命名空间用法实例分析
2020/06/05 PHP
IE和Firefox在JavaScript应用中的兼容性探讨
2008/04/01 Javascript
检测是否已安装 .NET Framework 3.5的js脚本
2009/02/14 Javascript
jQuery提交表单ajax查询实例代码
2012/10/07 Javascript
js+css实现的圆角边框TAB选项卡滑动门代码分享(2款)
2015/08/26 Javascript
JavaScript编写页面半透明遮罩效果的简单示例
2016/05/09 Javascript
Vue.js 2.0 移动端拍照压缩图片上传预览功能
2017/03/06 Javascript
angular十大常见问题
2017/03/07 Javascript
通过示例彻底搞懂js闭包
2017/08/10 Javascript
js实现多个倒计时并行 js拼团倒计时
2019/02/25 Javascript
通过实例学习React中事件节流防抖
2019/06/17 Javascript
微信小程序实现录音功能
2019/11/22 Javascript
javascript-hashchange事件和历史状态管理实例分析
2020/04/18 Javascript
[46:55]完美世界DOTA2联赛决赛 FTD vs Phoenix 第三场 11.08
2020/11/11 DOTA
python之import机制详解
2014/07/03 Python
用Python编写脚本使IE实现代理上网的教程
2015/04/23 Python
Python 使用类写装饰器的小技巧
2018/09/30 Python
Python面向对象之类的封装操作示例
2019/06/08 Python
python框架flask表单实现详解
2019/11/04 Python
如何利用python 读取配置文件
2021/01/06 Python
Pytorch实现WGAN用于动漫头像生成
2021/03/04 Python
HTML5 Canvas中绘制矩形实例
2015/01/01 HTML / CSS
中国海淘族值得信赖的海淘返利网站:55海淘
2017/01/16 全球购物
夏尔巴人登珠峰品牌:Sherpa Adventure Gear
2018/02/08 全球购物
俄罗斯名牌服装网上商店:UNIQUE FABRIC
2019/07/25 全球购物
机械制造专业个人的自我评价
2013/12/28 职场文书
物流管理专业毕业生求职信
2014/03/23 职场文书
应聘教师自荐书
2014/06/16 职场文书
2014副镇长民主生活会个人对照检查材料思想汇报
2014/09/30 职场文书
先进工作者推荐材料
2014/12/23 职场文书
出国留学自荐信模板
2015/03/06 职场文书
学校开除通知书
2015/04/25 职场文书
Redis 中使用 list,streams,pub/sub 几种方式实现消息队列的问题
2022/03/16 Redis