微信小程序-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 方法。

相关推荐
yqcoder17 分钟前
NPM 包管理问题汇总
前端·npm·node.js
程序菜鸟营23 分钟前
nvm安装详细教程(安装nvm、node、npm、cnpm、yarn及环境变量配置)
前端·npm·node.js
bsr198334 分钟前
前端路由的hash模式和history模式
前端·history·hash·路由模式
杨过姑父1 小时前
ES6 简单练习笔记--变量申明
前端·笔记·es6
Sunny_lxm1 小时前
<keep-alive> <component ></component> </keep-alive>缓存的组件实现组件,实现组件切换时每次都执行指定方法
前端·缓存·component·active
咔咔库奇2 小时前
【TypeScript】命名空间、模块、声明文件
前端·javascript·typescript
兩尛3 小时前
订单状态定时处理、来单提醒和客户催单(day10)
java·前端·数据库
又迷茫了3 小时前
vue + element-ui 组件样式缺失导致没有效果
前端·javascript·vue.js
trabecula_hj3 小时前
微信小程序中实现进入页面时数字跳动效果(自定义animate-numbers组件)
微信小程序·小程序
菜鸟码神3 小时前
微信小程序隐藏右侧胶囊按钮,分享和关闭即右侧三个点和小圆圈按钮
微信小程序·小程序