用户
 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,登录网站

小程序社区 首页 教程 实战教程 查看内容

小程序粘性布局组件实现

Rolan 2021-1-26 17:05

今天我们就一起用小程序来实现一个适用于不同场景下的粘性布局组件。

一、前言

开发中,我们经常会遇需要让组件在屏幕范围内时,按照正常布局排列,而组件滚出屏幕范围时,让其始终固定在屏幕顶部的情况,也就是常说的粘性布局。今天我们就一起用小程序来实现一个适用于不同场景下的粘性布局组件。


二、demo演示

如图,实现的组件主要适用于以下几种场景:

  1. 吸顶页面最上方;
  2. 吸顶与页面有固定距离的位置;
  3. 在指定容器内吸顶;
  4. 嵌套在scroll-view中吸顶。

三、代码演示

其中,粘性组件通过调用,参数信息用法如下:

滚动时触发scroll函数,其中isFixed为是否吸顶,scrollTop为距离顶部的位置。详细代码如下。

3.1 页面代码

3.1.1 基础用法

<view class="weimob-block">
  <view class="weimob-title">基础用法view>
  <view class="weimob-body">
    <weimob-sticky>
    
      <button class="margin-left-base" size="mini">
        基础用法
      button>
    weimob-sticky>
  view>
view>

3.1.2 吸顶距离
<view class="weimob-block">
  <view class="weimob-title">吸顶距离view>
  <view class="weimob-body">
  
    <weimob-sticky offset-top="{{ 50 }}">
    
      <button class="margin-left-top" type="primary" size="mini">
        吸顶距离
      button>
    weimob-sticky>
  view>
view>

3.1.3 指定容器
<view class="weimob-block">
  <view class="weimob-title">指定容器view>
  <view class="weimob-body">
  
    <view id="container" style="height: 300rpx;background-color: #fff">
      <weimob-sticky container="{{ container }}">
        <button size="mini" class="margin-left-special">
          指定容器
        button>
      weimob-sticky>
    view>
  view>
view>

3.1.4 嵌套在scroll-view使用
<view class="weimob-block">
  <view class="weimob-title">嵌套在 scroll-view 内使用view>
  
  <scroll-view
        bind:scroll="onScroll"
        scroll-y
        id="scroller"
        style="height: 400rpx; background-color: #fff;margin-top: 40rpx;"
    >
        <view style="height: 800rpx">
            <weimob-sticky
                scroll-top="{{ scrollTop }}"
                offset-top="{{ offsetTop }}"
            >
                <button size="mini" class="margin-left-scoll">
                    嵌套在 scroll-view 内
                button>
            weimob-sticky>
        view>
    scroll-view>
view>

页面js

Page({
  data: {
    container: null, //一个函数,返回容器对应的 NodesRef 节点
    scrollTop: 60, // 当前滚动区域的滚动位置,非null时会禁用页面滚动事件的监听
    offsetTop: 0  // 吸顶时与顶部的距离,单位px
  },
  
  onReady() {
  // 页面渲染完,获取节点信息
    this.setData({
      container: () => wx.createSelectorQuery().select('#container'),
    });
  },

  onScroll(event) {
   // 容器滚动时获取节点信息
    wx.createSelectorQuery()
      .select('#scroller')
      .boundingClientRect((res) => {
        this.setData({
          scrollTop: event.detail.scrollTop,
          offsetTop: res.top,
        });
      })
      .exec();
  }
});

3.2 组件代码

组件wxml
<wxs src="./index.wxs" module="computed" />

<view
  class="weimob-sticky"
  style="{{ computed.containerStyle({ fixed, height, zIndex }) }}"
>
  <view
    class="{{ fixed ? 'weimob-sticky-wrap--fixed' : ''}}"
    style="{{ computed.wrapStyle({ fixed, offsetTop, transform, zIndex }) }}"
  >
    <slot />
  view>
view>

组件wxs

这里使用使用小程序的wxs对吸顶元素的transform,top,height,z-index元素进行实时渲染,ios设备在滚动监听时性能会优于在js 2-20倍,androd设备效率暂无差异。

function wrapStyle(data) {
  var style = "";

    if (data.transform) {
        style += 'transform: translate3d(0, ' + data.transform + 'px, 0);'
    }

    if (data.fixed) {
    style += 'top: ' + data.offsetTop + 'px;'
    }

    if (data.zIndex) {
        style += 'z-index: ' + data.zIndex + ';'
    }

    return style;
}

function containerStyle(data) {
  var style = "";

    if (data.fixed) {
    style += 'height: ' + data.height + 'px;'
    }

    if (data.zIndex) {
        style += 'z-index: ' + data.zIndex + ';'
    }

    return style;
}
module.exports = {
  wrapStyle: wrapStyle,
  containerStyle: containerStyle
}

组件js
import pageScrollMixin from "./page-scroll";
const ROOT_ELEMENT = ".weimob-sticky";
Component({
  options: {
        multipleSlots: true
    },
  properties: {
    zIndex: {
      type: Number,
      value: 99
    },
    offsetTop: {
      type: Number,
      value: 0,
      observer: "onScroll"
    },
    disabled: {
      type: Boolean,
      observer: "onScroll"
    },
    container: {
      type: null,
      observer: "onScroll"
    },
    scrollTop: {
      type: null,
      observer(val) {
        this.onScroll({
          scrollTop: val
        });
      }

    }
  },
  data: {
    height: 0,
    fixed: false,
    transform: 0
  },
  behaviors: [pageScrollMixin(function pageScrollMixinCallback(event) {
    // 非null时会禁用页面滚动事件的监听
    if (this.data.scrollTop != null) {
      return;
    }

    this.onScroll(event);
  })],
  lifetimes: {
    attached() {
      this.onScroll();
    }

  },
  methods: {
    onScroll({
      scrollTop
    } = {}) {
      const {
        container,
        offsetTop,
        disabled
      } = this.data;

      if (disabled) {
        this.setDataAfterDiff({
          fixed: false,
          transform: 0
        });
        return;
      }

      this.scrollTop = scrollTop || this.scrollTop;

      if (typeof container === "function") {
        // 情况一:指定容器下时,吸顶距离+吸顶元素高度>容器高度+容器距顶部距离,随页面滚动;
        // 情况二:指定容器下时,吸顶距离>吸顶元素高度,元素固定;
        // 情况三:元素初始化。
        // this.getRect获取节点ROOT_ELEMENT相对于显示区域的top,height等信息,通过root获取
        // this.getContainerRect获取父容器相对于显示区域的top,height等信息,通过container获取

        Promise.all([this.getRect(ROOT_ELEMENT), this.getContainerRect()]).then( 
        ([root, container]) => {
          if (offsetTop + root.height > container.height + container.top) {
            this.setDataAfterDiff({
              fixed: false,
              transform: container.height - root.height
            });
          } else if (offsetTop >= root.top) {
            this.setDataAfterDiff({
              fixed: true,
              height: root.height,
              transform: 0
            });
          } else {
            this.setDataAfterDiff({
              fixed: false,
              transform: 0
            });
          }
        });
        return;
      }else{
        this.getRect(ROOT_ELEMENT).then(root => {
          // 吸顶时与顶部的距离小于可视区域的top距离时,随着滚动条滚动,否则吸顶
          if (offsetTop >= root.top) {
            this.setDataAfterDiff({
              fixed: true,
              height: root.height
            });
            this.transform = 0;
          } else {
            this.setDataAfterDiff({
              fixed: false
            });
          }
  
          return Promise.resolve();
        });
      }
    },

    setDataAfterDiff(data) {
      // 比较数据是否与上次相同,不同则触发父组件scroll事件更新isFixed,scrollTop。
      wx.nextTick(() => {
        const diff = Object.keys(data).reduce((prev, key) => {
          const prevCopy = prev;

          if (data[key] !== this.data[key]) {
            prevCopy[key] = data[key];
          }

          return prevCopy;
        }, {});
        this.setData(diff);
        this.triggerEvent("scroll", {
          scrollTop: this.scrollTop,
          isFixed: data.fixed || this.data.fixed
        });
      });
    },

    getContainerRect() {
      const nodesRef = this.data.container();
      return new Promise(resolve => nodesRef.boundingClientRect(resolve).exec());
    },

    getRect(selector) {
      return new Promise(resolve => {
        wx.createSelectorQuery().in(this).select(selector).boundingClientRect(rect => {
          resolve(rect);
        }).exec();
      });
    }

  }
});

page-scroll.js

滚动事件在页面进入和离开时共享的pageScrollMixin函数。

function getCurrentPage() {
  const pages = getCurrentPages();
  return pages[pages.length - 1] || {};
}

function onPageScroll(event) {
  const {
    weimobPageScroller = []
  } = getCurrentPage();
  weimobPageScroller.forEach(scroller => {
    if (typeof scroller === "function" && event) {
      // @ts-ignore
      scroller(event);
    }
  });
}

const pageScrollMixin = scroller => Behavior({
  attached() {
    const page = getCurrentPage();

    if (Array.isArray(page.weimobPageScroller)) {
      page.weimobPageScroller.push(scroller.bind(this));
    } else {
      page.weimobPageScroller = typeof page.onPageScroll === "function" ? [page.onPageScroll.bind(page), scroller.bind(this)] : [scroller.bind(this)];
    }

    page.onPageScroll = onPageScroll;
  },

  detached() {
    const page = getCurrentPage();
    page.weimobPageScroller = (page.weimobPageScroller || []).filter(item => item !== scroller);
  }

});

export default pageScrollMixin;

文章作者:Leka  授权极乐技术社区转载

鲜花
鲜花
鸡蛋
鸡蛋
分享至 : QQ空间
收藏
  • 班瑞 2021-1-27 14:37
    好复杂,其实只要设置个样式就能解决。position: sticky;类似relative和fixed的合体,必须设置任意方向,才能吸附,以浏览器窗口定位