大数跨境

微信小程序Skyline实战:滚动联动吸顶搜索栏+分类栏

微信小程序Skyline实战:滚动联动吸顶搜索栏+分类栏 wordpress知识
2026-11-01
1

在微信小程序开发中,实现丝滑的滚动联动效果是提升用户体验的关键。传统渲染层在处理复杂滚动动画时容易出现卡顿和掉帧,而采用 Skyline 渲染引擎结合 worklet 滚动驱动动画,可将动画执行移至主线程之外,从而实现高性能的交互体验。

以下方案展示了如何实现页面顶部 Logo 与标语随滚动平滑渐变消失,同时搜索框与分类栏渐变为顶部固定悬浮导航的效果,并完整保留下拉刷新、上拉加载及回到顶部功能。

核心实现代码

页面结构 (index.wxml)

通过双层 UI 设计(初始展示层与固定悬浮层),利用 opacity 属性控制显隐,确保视觉过渡自然。注意 scroll-view 需设置 padding-top 以适配状态栏高度。

<!-- 状态栏白色底层,永久不透明,盖住状态栏区域 --><view class="status-bar-bg" style="height:{{statusBarHeight}}px"></view><!-- 顶部固定区:初始透明,滚动后承载胶囊左侧搜索 --><view class="nav-sticky" style="height:{{statusBarHeight + 44}}px;padding-top:{{statusBarHeight}}px">  <view class="search-wrap">    <input class="search-input" placeholder="搜商品找好价" value="{{keyword}}" bindinput="onInputKeyword" confirm-type="search" bindconfirm="onSearch"/>    <view wx:if="{{keyword}}" class="search-clear" bindtap="clearKeyword">×</view>  </view></view><!-- 固定分类栏:滚动后吸附在搜索导航下方 --><view class="category-fixed" style="top:{{statusBarHeight + 44}}px">  <scroll-view class="category-scroll" scroll-x>    <view class="category-item {{activeCat===index?'active':''}}" wx:for="{{categoryList}}" wx:key="index" bindtap="changeCat" data-index="{{index}}">      {{item.name}}    </view>  </scroll-view></view><!-- ✅ 重点:增加 padding-top="{{statusBarHeight}}", 避开状态栏胶囊 --><scroll-view  class="scroll"  style="padding-top:{{statusBarHeight}}px"  scroll-y  refresher-enabled  refresher-triggered="{{refreshing}}"  bindrefresherrefresh="onRefresh"  bindscrolltolower="loadMore"  lower-threshold="150"  worklet:onscrollupdate="onScrollUpdate">  <!-- 初始最顶部:Logo + 标语 -->  <view class="nav-origin">    <view class="logo-row">      <view class="logo">lg</view>      <view class="title-text">小程序名称</view>    </view>    <view class="desc">这里是小程序宣传标语,认识小程序的作用</view>  </view>  <!-- 页面内搜索 + 分类 -->  <view class="search-category-origin">    <view class="search-bar">      <view class="search-wrap">        <input class="search-input" placeholder="搜商品找好价" value="{{keyword}}" bindinput="onInputKeyword" confirm-type="search" bindconfirm="onSearch"/>        <view wx:if="{{keyword}}" class="search-clear" bindtap="clearKeyword">×</view>      </view>    </view>    <scroll-view class="category-scroll" scroll-x>      <view class="category-item {{activeCat===index?'active':''}}" wx:for="{{categoryList}}" wx:key="index" bindtap="changeCat" data-index="{{index}}">        {{item.name}}      </view>    </scroll-view>  </view>  <view class="banner">    <image class="banner-img" src="https://picsum.photos/id/1011/750/400" mode="aspectFill"></image>  </view>  <block wx:for="{{goodsList}}" wx:key="id">    <view class="goods-item" bindtap="tapGoods" data-index="{{index}}">      <image class="goods-img" src="{{item.img}}" mode="aspectFill"></image>      <view class="goods-info">        <view class="goods-name">{{item.title}}</view>        <view class="goods-price">¥{{item.price}}</view>      </view>    </view>  </block>  <view wx:if="{{loading && !refreshing}}" class="loading-wrap">    <view class="dot dot1"></view><view class="dot dot2"></view><view class="dot dot3"></view>  </view>  <view wx:if="{{noMore}}" class="nomore">没有更多了</view></scroll-view><view class="back-top" bindtap="tapBackTop">  <view class="back-top-icon">↑</view></view>

逻辑处理 (index.js)

利用 shared 共享变量在 worklet 线程中直接修改样式,避免 setData 造成的通信开销和卡顿。

const { shared } = wx.workletPage({  data: {    goodsList: [],    refreshing: false,    loading: false,    noMore: false,    page: 1,    statusBarHeight: 0,    keyword: '',    activeCat: 0,    categoryList: [      { name: '全部' },      { name: '水果' },      { name: '零食' },      { name: '生鲜' },      { name: '家电' },      { name: '服饰' },      { name: '美妆' }    ]  },  onLoad() {    const info = wx.getWindowInfo()    this.setData({      statusBarHeight: info.statusBarHeight    })    this.backTopOpacity = shared(0)    this.stickyNavOpacity = shared(0)    this.fixedCatOpacity = shared(0)    this.applyAnimatedStyle('.back-top', () => {      'worklet'      return { opacity: this.backTopOpacity.value }    })    this.applyAnimatedStyle('.nav-sticky', () => {      'worklet'      return { opacity: this.stickyNavOpacity.value }    })    this.applyAnimatedStyle('.category-fixed', () => {      'worklet'      return { opacity: this.fixedCatOpacity.value }    })  },  onReady() {    this.loadData()  },  onScrollUpdate(evt) {    'worklet'    const scrollTop = evt.detail.scrollTop    // Logo + 标语区域高度,单位 px    const logoHeight = 140    // 开始显示固定搜索导航的位置    const startFade = logoHeight - 40    // 完全显示固定搜索导航的位置    const endFade = logoHeight    let progress = 0    if (scrollTop > startFade) {      progress = Math.min((scrollTop - startFade) / (endFade - startFade), 1)    }    this.stickyNavOpacity.value = progress    this.fixedCatOpacity.value = progress    const backTopProgress = Math.min(Math.max((scrollTop - 500) / 100, 0), 1)    this.backTopOpacity.value = backTopProgress  },  changeCat(e) {    const idx = e.currentTarget.dataset.index    this.setData({      activeCat: idx,      page: 1,      noMore: false,      goodsList: []    }, () => {      this.loadData()    })  },  onInputKeyword(e) {    this.setData({ keyword: e.detail.value })  },  clearKeyword() {    this.setData({ keyword: '' }, () => {      this.onSearch()    })  },  onSearch() {    this.setData({      page: 1,      noMore: false,      goodsList: []    }, () => {      this.loadData()    })  },  onRefresh() {    this.setData({      page: 1,      noMore: false,      refreshing: true,      goodsList: []    }, () => {      this.loadData()    })  },  loadMore() {    if (this.data.loading || this.data.noMore) return    this.loadData()  },  loadData() {    this.setData({ loading: true })    setTimeout(() => {      const page = this.data.page      const { keyword, activeCat, categoryList } = this.data      const catName = categoryList[activeCat].name      const mock = []      for (let i = 0; i < 10; i++) {        let title = catName + '商品-' + i        if (keyword) {          title = keyword + '-匹配商品'        }        mock.push({          id: `${page}_${i}`,          title: title,          price: (Math.random() * 100).toFixed(2),          img: 'https://picsum.photos/id/237/200/200'        })      }      const list = [...this.data.goodsList, ...mock]      const noMore = page >= 5      this.setData({        goodsList: list,        page: page + 1,        loading: false,        refreshing: false,        noMore      })    }, 600)  },  tapBackTop() {    const query = this.createSelectorQuery()    query.select('.scroll').node(res => {      if (res?.node) {        res.node.scrollTo({ top: 0 })      }    }).exec()  },  tapGoods(e) {    const idx = e.currentTarget.dataset.index    const item = this.data.goodsList[idx]    console.log('点击商品:', item)    if (!item || !item.id) {      console.warn('商品数据异常,缺少id')      return    }    wx.navigateTo({      url: `/pages/detail/detail?id=${item.id}`,      success(res) {        console.log('✅跳转成功', res)      },      fail(err) {        console.error('❌跳转失败', err)      }    })  }  })

样式定义 (index.wxss)

page {  background: #f5f5f5;  height: 100%;}/* 状态栏底色层 */.status-bar-bg {  position: fixed;  top: 0;  left: 0;  width: 100%;  background-color: #ffffff;  z-index: 98;}/* 顶部固定搜索导航:默认透明,滚动后显示 */.nav-sticky {  position: fixed;  top: 0;  left: 0;  width: 100%;  z-index: 99;  background: #fff;  box-sizing: border-box;  padding-left: 20rpx;  padding-right: 160rpx;  display: flex;  align-items: center;  opacity: 0;}.nav-sticky .search-wrap {  width: 100%;  height: 56rpx;  background: #f5f5f5;  border-radius: 28rpx;  display: flex;  align-items: center;  padding: 0 20rpx;  box-sizing: border-box;}/* 初始最顶部:Logo + 标语 */.nav-origin {  background: #fff;  box-sizing: border-box;  padding: 24rpx;}.logo-row {  display: flex;  align-items: center;  gap: 16rpx;  margin-bottom: 8rpx;}.logo {  width: 56rpx;  height: 56rpx;  border-radius: 50%;  background: #ff4444;  color: #fff;  text-align: center;  line-height: 56rpx;  font-size: 32rpx;  font-weight: bold;}.title-text {  font-size: 36rpx;  font-weight: bold;  color: #333;}.desc {  font-size: 26rpx;  color: #666;}/* 页面内搜索 + 分类 */.search-category-origin {  background: #fff;}.search-bar {  width: 100%;  height: 88rpx;  box-sizing: border-box;  padding: 0 20rpx;  display: flex;  align-items: center;}.search-wrap {  width: 100%;  height: 64rpx;  background: #f5f5f5;  border-radius: 32rpx;  display: flex;  align-items: center;  padding: 0 20rpx;  box-sizing: border-box;}.search-input {  flex: 1;  font-size: 26rpx;  color: #333;  background: transparent;}.search-clear {  width: 40rpx;  height: 40rpx;  text-align: center;  line-height: 40rpx;  font-size: 28rpx;  color: #999;  margin-left: 10rpx;}/* 横向分类 */.category-scroll {  white-space: nowrap;  height: 70rpx;  padding: 0 20rpx;  box-sizing: border-box;}.category-item {  display: inline-block;  height: 70rpx;  line-height: 70rpx;  padding: 0 16rpx;  font-size: 26rpx;  color: #666;}.category-item.active {  color: #ff4444;  font-weight: bold;  position: relative;}.category-item.active::after {  content: '';  position: absolute;  bottom: 10rpx;  left: 50%;  transform: translateX(-50%);  width: 32rpx;  height: 4rpx;  background: #ff4444;  border-radius: 2rpx;}/* 固定分类栏 */.category-fixed {  position: fixed;  left: 0;  width: 100%;  z-index: 98;  background: #fff;  opacity: 0;  box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.06);}.banner {  width: 100%;  height: 360rpx;  border-radius: 16rpx;  overflow: hidden;  margin-top: 20rpx;}.banner-img {  width: 100%;  height: 100%;}.scroll {  width: 100%;  height: 100%;  padding-bottom: env(safe-area-inset-bottom);  box-sizing: border-box;}.goods-item {  background: #fff;  border-radius: 16rpx;  margin-top: 20rpx;  margin-bottom: 20rpx;  display: flex;  padding: 20rpx;}.goods-img {  width: 160rpx;  height: 160rpx;  border-radius: 12rpx;  flex-shrink: 0;}.goods-info {  flex: 1;  margin-left: 20rpx;  display: flex;  flex-direction: column;  justify-content: space-between;}.goods-name {  font-size: 28rpx;  color: #333;}.goods-price {  color: #ff4444;  font-size: 30rpx;  font-weight: bold;}.loading-wrap {  height: 80rpx;  display: flex;  align-items: center;  justify-content: center;  gap: 10rpx;}.dot {  width: 12rpx;  height: 12rpx;  border-radius: 50%;  background: #999;  animation: jump 0.6s infinite ease-in-out;}.dot1 { animation-delay: 0s; }.dot2 { animation-delay: 0.2s; }.dot3 { animation-delay: 0.4s; }@keyframes jump {  0%, 100% { transform: translateY(0); }  50% { transform: translateY(-12rpx); }}.nomore {  text-align: center;  padding: 30rpx 0;  color: #999;  font-size: 26rpx;}.back-top {  position: fixed;  right: 30rpx;  bottom: calc(140rpx + env(safe-area-inset-bottom));  width: 80rpx;  height: 80rpx;  border-radius: 50%;  background: #fff;  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.15);  display: flex;  align-items: center;  justify-content: center;  z-index: 100;  opacity: 0;}.back-top-icon {  font-size: 40rpx;  color: #333;  font-weight: bold;}

配置文件 (index.json)

启用 Skyline 渲染引擎并设置自定义导航栏。

{  "renderer": "skyline",  "navigationStyle": "custom"}

技术要点解析

Skyline 与 Worklet 优势

worklet:onscrollupdate 回调运行在独立线程,不会阻塞页面渲染,这是 Skyline 相比旧版渲染层的最大优势,特别适合处理滚动联动渐变和吸顶动画。使用 shared 共享变量在 worklet 内修改值,可自动驱动 applyAnimatedStyle 更新样式。切勿在 onScrollUpdate 中直接调用 setData,否则会导致严重卡顿。

双层 UI 设计策略

页面中包含两套搜索与分类 UI:一套位于页面初始位置,另一套为 fixed 定位的悬浮层。两者保持样式一致,通过 opacity 属性控制淡入淡出效果,而非直接进行位移替换,从而使交互更加顺滑。

状态栏适配方案

通过 wx.getWindowInfo() 获取 statusBarHeight。设置一个白色的 fixed 底层覆盖状态栏区域,并在 scroll-view 顶部添加等高的 padding-top,有效避开胶囊按钮区域,防止内容被遮挡。

交互逻辑闭环

切换分类、执行搜索或下拉刷新时,均会重置 page=1 并清空列表。上拉加载时通过判断 loading 和 noMore 状态锁死请求,防止重复触发;当达到最大页数时自动停止加载。

兼容性注意事项

该方案必须基于 Skyline 渲染引擎,且基础库版本需满足要求。对于不支持 worklet 的低版本机型,需做好降级处理。

#微信小程序 #Skyline渲染引擎 #worklet动画 #自定义导航栏

【声明】内容源于网络
0
0
wordpress知识
各类跨境出海行业相关资讯
内容 388
粉丝 0
wordpress知识 各类跨境出海行业相关资讯
总阅读15.5k
粉丝0
内容388