评论

Skyline|小程序手势:让半屏弹窗更顺滑

手势系统:更方便、自然、直观的操作方式,提高用户体验和操作效率

在小程序页面开发中,我们经常用半屏弹窗来进来内容展示,例如:微信开放社区切换主页、加入购物车的选项页、文章留言区等等。



常见的半屏弹窗展示逻辑是这样的:

  • 打开弹窗:点击 “打开弹窗” 按钮展示弹窗
  • 关闭弹窗:点击“关闭按钮” or 遮罩层 关闭弹窗

当我们想在半屏弹窗加一些交互动画时,可以监听节点的 touch 事件来做一些手势判断,进而处理拖拽事件。但是这种方式实现的滚动动画容易卡顿,出现延迟的情况,效果并不理想。

为了丰富小程序的交互体验,我们内置了一批手势组件,可以帮助开发者更好的实现交互动画的效果。

下图演示使用手势的半屏弹窗下拉效果与普通半屏下拉的对比。

当内部评论列表往下拉到顶部时,变为半屏的下拉,可直接下拉关闭弹窗。

我们来看下这种操作是怎么实现的

在上面评论列表的半屏弹窗中会有一个 scroll-view 滚动组件,在 scroll-view 中会有滚动事件,当滚动到顶部时,我们希望有整个半屏的下拉事件。

所以我们需要在半屏的最外层放置一个拖动手势组件 pan-gesture-handler

由于拖动组件内部的 scroll-view 也是可以滚动的,所以这里需要进行一个手势协商的处理,就是什么条件下由哪个组件来响应手势。


当手势往下 ⬇️ 滚动时,此时判断内部 scroll-view 滚动条的位置

  • 滚动条处于顶部:外层 pan-gesture-handler 响应滚动,此时半屏往下拖动至关闭半屏
  • 滚动条不处于顶部:内层 scroll-view 响应滚动,此时内部列表往上滚

当手势往上 ⬆️ 滚动时,此时判断半屏的位置

  • 半屏不完全打开时:外层 pan-gesture-handler 响应滚动,此时半屏往上拖动至完全打开半屏
  • 半屏完全打开时:内层 scroll-view 响应滚动,此时内部列表往下滚

我们来看一下代码的实现,这里用到的手势组件 pan-gesture-handler(拖动时触发)和  vertical-drag-gesture-handler(纵向滑动时触发),手势组件有以下属性

  • on-gesture-event:手势回调事件
  • should-response-on-move:是否响应当前手势的 move 阶段
  • simultaneous-handlers:指定需要协商的手势是哪几个,下面演示表示 pan 和 scroll 协同触发。
  • native-view:代理的原生节点,这里 scroll-view(scroll-y) 内有个 vertical-drag 手势,scroll-view 自身无法处理,需要被代理出来



  
  
     ... 
  




接着,我们看看在页面 js 中怎么处理手势。

在手势处理的回调中因为会改变半屏的状态值,所以这里的回调函数采用 worklet 函数,worklet 函数运行在 UI 线程,使得小程序可以做到类原生动画般的体验

// page.js
// shared 创建的变量为共享变量,可在 UI 线程和 JS 线程间同步
this.transY = wx.worklet.shared(1000)
this.scrollTop = wx.worklet.shared(0)
this.startPan = wx.worklet.shared(true)


// shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商
shouldPanResponse() {
  'worklet'
  return this.startPan.value
},
shouldScrollViewResponse(pointerEvent) {
  'worklet'
  // transY > 0 说明 pan 手势在移动半屏,此时 scroll-view 滚动不应生效
  if (this.transY.value > 0) return false
  const scrollTop = this.scrollTop.value
  const { deltaY } = pointerEvent
  // deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,scroll-view 滚动不生效
  const result = scrollTop <= 0 && deltaY > 0
  this.startPan.value = result
  return !result
},


// pan 手势处理
handlePan(gestureEvent) {
  'worklet'
  if (gestureEvent.state === GestureState.ACTIVE) {
    const curPosition = this.transY.value
    const destination = Math.max(0, curPosition + gestureEvent.deltaY)
	// 改变半屏的位置
    this.transY.value = destination
  }


  // 其他手势状态的处理,如滚动结束时计算半屏处于打开还是关闭的状态
}


目前,同程旅行 已经上线了手势结合半屏的效果

体验路径:酒店查询 - 选择酒店 - 选择入住人 - 新增入住人

普通半屏结合手势代码片段:https://developers.weixin.qq.com/s/lx0RH1mD7rGj


手势除了在普通半屏的应用之外,也可以实现分段式半屏。下面演示的分段式半屏比普通半屏的判断条件更多一些。

判断条件同普通半屏类似,根据手势方向 和 分段式半屏当前的位置来判断是响应分段式半屏还是内部列表,响应分段式半屏是改变到哪一个位置。

这里与普通半屏不同的是我们还改变了地图的缩放级别(scale)

因为 worklet 函数是在 UI 线程运行的,当要改变 data 值时,需要通过 wx.worklet.runOnJS 调回 JS 线程。

// page.js
// 设置 map scale
// 运行在 JS 线程
setMapScale(scale) {
  this.setData({ scale })
},


// worklet 函数,运行在 UI 线程
scrollTo(toValue) {
  'worklet'
  let scale = 18
  if (toValue > screenHeight / 2) {
    scale = 16
  }
  // 从 UI 线程调回 JS 线程
  wx.worklet.runOnJS(this.setMapScale.bind(this))(scale)


  this.transY.value = timing(toValue, { duration: 200 })
},


// 处理拖动半屏的手势
handlePan(gestureEvent) {
  'worklet'
  // 滚动半屏的位置
  if (gestureEvent.state === GestureState.ACTIVE) {
    // deltaY < 0,往上滑动
    this.upward.value = gestureEvent.deltaY < 0
    // 当前半屏位置
    const curPosition = this.transY.value
    // 只能在 [statusBarHeight, screenHeight] 之间移动
    const destination = clamp(curPosition + gestureEvent.deltaY, statusBarHeight, screenHeight)
    if (curPosition === destination) return
    // 改变 transY,来改变半屏的位置
    this.transY.value = destination
  }


  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)
    }
  }
},


分段式页面代码片段:https://developers.weixin.qq.com/s/fw0U31mI7bGf


半屏的交互除了在页面内实现,也能跨页面实现,如常见的下沉式半屏交互。其中,半屏效果与上述实现类似,而前一页面的下沉实现需要结合自定义路由

后面的文章中我们会介绍自定义路由结合手势怎么去实现下沉式半屏效果,不仅如此,还有很多类原生的页面切换效果都能通过自定义路由实现



最后一次编辑于  2023-08-03  
点赞 13
收藏
评论

13 个评论

  • showms
    showms
    2023-02-14

    进我的收藏夹吃灰吧

    2023-02-14
    赞同 3
    回复 1
  • O...K...O
    O...K...O
    2023-09-21

    有没有用taro的兄弟呀 taro里面怎么用这个呀

    2023-09-21
    赞同 1
    回复 1
    • 加伊
      加伊
      01-05
      tora用不了,不支持手势组件
      01-05
      1
      回复
  • TANGYC_
    TANGYC_
    2023-07-25

    demo三段式的代码片段要在app.json里加上 "componentFramework": "glass-easel",点击预览才能编译成功。

      "renderer": "skyline",
      "componentFramework": "glass-easel",
      "lazyCodeLoading": "requiredComponents",
    


    预览之后,还要在右上角三个点开发调试那里的Switch Render里切换Skyline的渲染模式 才能正常启动这个代码片段

    2023-07-25
    赞同 1
    回复 2
    • 在路上
      在路上
      04-30
      那上线怎么办
      04-30
      回复
    • TANGYC_
      TANGYC_
      05-16回复在路上
      我也有这个疑问
      05-16
      回复
  • Jianbo
    Jianbo
    2023-02-15

    不错,收藏,换skyline就用起来。

    2023-02-15
    赞同 1
    回复
  • 拾忆
    拾忆
    2023-02-14

    已阅

    2023-02-14
    赞同 1
    回复 2
  • 浪子如风
    浪子如风
    04-14

    自定义组件 中使用这个 不生效

    04-14
    赞同
    回复
  • Michael〔〕wong
    Michael〔〕wong
    2023-09-26

    uniapp怎么使用手势事件啊

    2023-09-26
    赞同
    回复
  • 布丁
    布丁
    2023-08-17

    有没有大佬用uniapp封装一下。没有只能自己搞了..........

    2023-08-17
    赞同
    回复
  • 晓宁
    晓宁
    2023-07-14

    这手机上不支持么?真机上没效果啊

    2023-07-14
    赞同
    回复 1
    • TANGYC_
      TANGYC_
      2023-07-25
      demo三段式的代码片段要在app.json里加上 "componentFramework": "glass-easel",点击预览才能编译成功。


        "renderer": "skyline",
        "componentFramework": "glass-easel",
        "lazyCodeLoading": "requiredComponents",




      预览之后,还要在右上角三个点开发调试那里的Switch Render里切换Skyline的渲染模式 才能正常启动这个代码片段
      2023-07-25
      回复
  • 江军
    江军
    2023-06-27

    <view class="section-item">

        <view class="intro">单指拖动</view>

        <pan-gesture-handler onGestureEvent="handlepan">

            <view class="circle" >simple drag</view>

        </pan-gesture-handler>

    </view>

    我试了一下拖不动呢

    2023-06-27
    赞同
    回复

正在加载...

登录 后发表内容