微信小程序-skyline让你拥有超丝滑的Popup组件

手势系统原理

通过给元素添加一个手势组件的父级,通过手势组件的触发事件来进行对应操作

pan-gesture-handler 拖动(横向/纵向)时触发的组件

vertical-drag-gesture-handler 纵向时触发的组件

worklet:ongesture: 手势识别成功的回调

**worklet:should-response-on-move:**手指移动过程中手势是否响应

**simultaneous-handlers:**需要协同的手势,即,如果某个手势组件包含在另一个手势组件里面,那么将会手势冲突。此时需要协商手势,谁在外面谁在里面。设置协商手势后在子级触发组件 那么两者都会触发

native-viewnative-view 支持的枚举值有 scroll-viewswiper。滚动容器纵向滚动时,使用 <vertical-drag-gesture-handler> 手势组件代理内部手势,横向滚动时,则使用 <horizontal-drag-gesture-handler>

怎么使用skyline

diff 复制代码
Skyline 具体支持版本如下:

- 微信安卓客户端 8.0.33 或以上版本(对应基础库为 2.30.4 或以上版本)
- 微信 iOS 客户端 8.0.34 或以上版本(对应基础库为 2.31.1 或以上版本)
- 开发者工具 Stable 1.06.2307260 或以上版本(建议使用 Nightly 最新版)

初始项目配置

  1. 在 app.json 或者所需 page.json 中配上 renderer: skyline
  2. 确保右上角 > 详情 > 本地设置里的 开启 Skyline 渲染调试 选项被勾选上
  3. 使用 worklet 动画特性时,确保右上角 > 详情 > 本地设置里的 编译 worklet 代码 选项被勾选上 (代码包体积会少量增加)
  4. 调试基础库切到 3.0.0 或以上版本
    1. Skyline 依赖 按需注入 特性,需在 app.json 配上 "lazyCodeLoading": "requiredComponents"
  5. 在全局或页面配置中声明使用新版 glass-easel 组件框架,即 { "componentFramework": "glass-easel" }
  6. 在全局配置中声明默认 block 布局,即 app.json 配上 "rendererOptions": { "skyline": { "defaultDisplayBlock": true } }
  7. 在全局配置中声明默认 content-box 布局,即 app.json 配上 "rendererOptions": { "skyline": { "defaultContentBox": true } }

注意事项

  1. Skyline 不支持页面全局滚动,需在页面配置加上 "disableScroll": true(使之与 WebView 保持兼容),在需要滚动的区域使用 scroll-view 实现
  2. Skyline 不支持原生导航栏,需在页面配置加上 "navigationStyle": "custom"(使之与 WebView 保持兼容),并自行实现自定义导航栏
  3. 所有的worklet 代码的函数顶部需要加上"worklet;"

编写Popup效果

WXML

xml 复制代码
<view class="skyline-popup comment-container" style="height: {{height}}px;">
    <pan-gesture-handler worklet:ongesture="handlePan" style="flex-shrink: 0;">
        <view class="comment-header" bind:touchend="handleTouchEnd">
            <view class="comment-handler"></view>
            Popup滑动标题
        </view>
    </pan-gesture-handler>
    <!-- 滚动区要与 pan 手势协商 -->
    <pan-gesture-handler id="pan" worklet:should-response-on-move="shouldPanResponse" simultaneousHandlers="{{['scroll']}}" worklet:ongesture="handlePan">
        <vertical-drag-gesture-handler id="scroll" native-view="scroll-view" worklet:should-response-on-move="shouldScrollViewResponse" simultaneousHandlers="{{['pan']}}">
            <scroll-view class="comment-list" scroll-y worklet:adjust-deceleration-velocity="adjustDecelerationVelocity" worklet:onscrollupdate="handleScroll" type="list" show-scrollbar="{{false}}">
                <view>1</view>
            </scroll-view>
        </vertical-drag-gesture-handler>
    </pan-gesture-handler>
</view>

这个地方参考官方的留言Popup布局,有header显示标题,有scroll-view 对内容进行滚动。headerscroll-view 各自添加了手势组件并且同样添加了拖动事件handlePan,进行Popup 的拖动效果实现。而scroll-view 还添加了vertical-drag-gesture-handler 此处用native-view 进行scroll-view 组件的代理。

JS

kotlin 复制代码
// shared:用于跨线程共享数据和驱动动画。
// timing:基于时间的动画。
const { shared, timing } = wx.worklet;

// 获取系统信息
const { screenHeight, statusBarHeight, safeArea } = wx.getSystemInfoSync();

enum GestureState {
  POSSIBLE = 0, // 0 此时手势未识别,如 panDown等
  BEGIN = 1, // 1 手势已识别
  ACTIVE = 2, // 2 连续手势活跃状态
  END = 3, // 3 手势终止
  CANCELLED = 4, // 4 手势取消,
}

Component({
  data: {
    height: 600,
  },
  lifetimes: {
    // 生命周期对象 新语法
    created() {
      // 初始化对象
      this.transY = shared(1000); // 留言半屏的位置
      this.scrollTop = shared(0); // 留言列表的滚动位置
      this.startPan = shared(true); // 是否开始滑动
      this.initTransY = shared(0); // 留言半屏的初始位置
      this.upward = shared(false); // 是否向上滑动
    },
    attached() {
      // 组件挂载时
      // 屏幕高度减去状态栏高度 设置最高高度
      this.setData({
        height: screenHeight - statusBarHeight,
      });
    },
    ready() {
      // 在组件在视图层布局完成后执行
      const query = this.createSelectorQuery();
      console.log(safeArea.bottom, "bottom");
      // ready 生命周期里才能获取到首屏的布局信息
      query.select(".comment-header").boundingClientRect();
      query.exec((res) => {
        // 设置transY的值和初始transY的值为屏幕高度减去留言header的高度 再减去安全区域(即可以使用的区域)
        // 因为初始只显示popup的header
        this.transY.value = this.initTransY.value =
          screenHeight - res[0].height - (screenHeight - safeArea.bottom);
      });
      // 通过 transY 一个 SharedValue 控制半屏的位置 设置动画
      this.applyAnimatedStyle(".comment-container", () => {
        "worklet";
        return { transform: `translateY(${this.transY.value}px)` };
      });
    },
  },
  methods: {
    handlePan(gestureEvent) {
      "worklet";
      // 如果手势正在滑动
      if (gestureEvent.state === GestureState.ACTIVE) {
        // 更新是否向上和transY的值
        this.upward.value = getPopupUpward(gestureEvent.deltaY);
        this.transY.value = getUpdatePopupTransY(
          gestureEvent.deltaY,
          this.transY.value
        );
      }else if (
        gestureEvent.state === GestureState.END ||
        gestureEvent.state === GestureState.CANCELLED
      ) {
        // 当前位置 <= 屏幕高度的一半 即在屏幕上方
        if (this.transY.value <= screenHeight / 2) {
          // 在上面的位置
          if (this.upward.value) {
            this.scrollTo(statusBarHeight);
          } else {
            this.scrollTo(screenHeight / 2);
          }
          // 当前位置 > 屏幕高度的一半 即在屏幕下方
        } else if (
          this.transY.value > screenHeight / 2 &&
          this.transY.value <= this.initTransY.value
        ) {
          // 在中间位置的时候
          if (this.upward.value) {
            this.scrollTo(screenHeight / 2);
          } else {
            this.scrollTo(this.initTransY.value);
          }
        } else {
          // 在最下面的位置
          this.scrollTo(this.initTransY.value);
        }
    },
  },
});
function clamp(val, min, max) {
  "worklet";
  return Math.min(Math.max(val, min), max);
}

const getPopupUpward = (deltaY: number) => {
  "worklet";
  // deltaY < 0,往上滑动
  return deltaY < 0;
};

const getUpdatePopupTransY = (deltaY: number, transY: number) => {
  "worklet";
  // deltaY < 0,往上滑动
  // 当前半屏位置
  const curPosition = transY;
  // 只能在 [statusBarHeight, screenHeight] 之间移动
  const destination = clamp(
    curPosition + deltaY,
    statusBarHeight,
    screenHeight
  );
  if (curPosition === destination) return curPosition;
  // 改变 transY,来改变半屏的位置
  return destination;
};

这里通过生命周期 进行初始化数据,通过handlePan进行Popup的拖动

WXSS 详见最下方

到这里就可以实现拖动效果了,但在这里会出现一个问题,那就是在拖动scroll-view的时候会出现里面滚动内容也在滚动的问题。此时就需要worklet:should-response-on-move 来设置手势操作时候是否响应了

设置scroll-view的手势响应

JS

kotlin 复制代码
Component({
	methods: {
		// 编写scroll-view的手势协商
    // shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商
    shouldPanResponse() {
      "worklet";
      return this.startPan.value;
    },
    // 进来这里证明是在拖动scroll-view
    shouldScrollViewResponse(pointerEvent) {
      "worklet";
      // transY(popup当前位置) > 状态栏高度 popup在状态栏下面。代表还没到达最上面一层
      // 因为这里滚动条件为到达最上面一层才开始滚动 到达的条件为 transY === 状态栏高度
      if (this.transY.value > statusBarHeight) return false;
      const scrollTop = this.scrollTop.value;
      const { deltaY } = pointerEvent;
      // deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,滚动不生效
      // 滚动位置到顶部并且向下滚动 则停止响应
      const result = scrollTop <= 0 && deltaY > 0;
      this.startPan.value = result;
      return !result;
    },
    // 设置滚动位置
    handleScroll(evt) {
      "worklet";
      this.scrollTop.value = evt.detail.scrollTop;
    },
	}
})

在这个地方主要用了worklet:should-response-on-move 来进行手势组件的响应,因为vertical-drag-gesture-handler 组件使用native-view 代理了scroll-view 组件,所以它可以控制scroll-view的行为。在这里主要是在scroll-view 触发手势时进行判断,**必须要popup高度到达顶部栏,并且滚动位置不为0,deltaY>0(代表手势为向下滑动)才可以滚动。**详见shouldScrollViewResponse 方法。

相关推荐
懒大王爱吃狼20 分钟前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風4 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
Devil枫5 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
尚梦5 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子6 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
前端青山6 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
毕业设计制作和分享7 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
小飞哥liac8 小时前
微信小程序的组件
微信小程序
清灵xmf9 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
大佩梨9 小时前
VUE+Vite之环境文件配置及使用环境变量
前端