
Skyline Components
- 711 installs
- 48 repo stars
- Updated June 3, 2026
- wechat-miniprogram/skyline-skills
skyline-components is an agent skill that guides high-performance WeChat Mini Program UI development using Skyline enhanced scroll-view, swiper, list-view, grid-view, and animation components for developers building list
About
skyline-components is a WeChat Mini Program skill from wechat-miniprogram/skyline-skills covering Skyline rendering engine components for production mobile interfaces. The skill documents enhanced scroll-view modes including nested scrolling, pull-to-refresh, and worklet callbacks; swiper layout types; form inputs with keyboard worklets; and Skyline-only containers like list-view and grid-view for waterfall layouts. Developers use skyline-components when building scroll lists, carousels, half-screen draggable panels, or shared-element page transitions in Mini Programs. The skill maps which components are fully supported versus Skyline-exclusive additions, helping agents avoid legacy view patterns that miss performance features. It targets WeChat Mini Program frontend engineers adopting the Skyline renderer.
- Complete support for scroll-view with type mode, worklet callbacks, nested scrolling and pull-to-refresh
- Advanced swiper with layout-type stacking, card effects and indicator animations
- New native components including list-view, grid-view, draggable-sheet, share-element and sticky-section
- Optimized text, image, input and textarea components with overflow handling, lazy loading and keyboard worklets
- 12 specialized documentation guides covering virtual lists, nested scroll, sticky headers and shared element transitions
Skyline Components by the numbers
- 711 all-time installs (skills.sh)
- +31 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #485 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/wechat-miniprogram/skyline-skills --skill skyline-componentsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 711 |
|---|---|
| repo stars | ★ 48 |
| Last updated | June 3, 2026 |
| Repository | wechat-miniprogram/skyline-skills ↗ |
How do you build high-performance WeChat Mini Program UIs?
Rapidly implement high-performance WeChat Mini Program interfaces using Skyline's enhanced scroll, swiper, list, grid and animation components.
Who is it for?
WeChat Mini Program developers adopting the Skyline renderer who need scroll lists, waterfall grids, carousels, or page transition animations.
Skip if: Developers building standard web React apps, native iOS/Android apps, or Mini Programs not using the Skyline rendering mode.
When should I use this skill?
User builds WeChat Mini Program UI with Skyline scroll-view, swiper, list-view, grid-view, nested scrolling, or shared-element animations.
What you get
Skyline Mini Program pages with optimized scroll lists, swipers, forms, grid layouts, and shared-element transition animations.
- Skyline component pages
- scroll and swiper layouts
- shared-element transition config
Files
Skyline 组件开发指南
适用场景
- 开发滚动列表/瀑布流/网格布局
- 实现下拉刷新、下拉二级功能
- 使用嵌套滚动模式
- 开发轮播图、卡片切换效果
- 处理表单输入(input/textarea)
- 实现共享元素/页面转场动画
- 开发半屏可拖拽面板
组件支持概览
完全支持的高频组件
| 组件 | Skyline 特性增强 |
|---|---|
| scroll-view | type 模式、worklet 回调、嵌套滚动、下拉二级 |
| swiper | layout-type 堆叠/卡片、indicator 动画类型 |
| text | overflow 文本溢出处理、max-lines |
| image | fade-in 渐显、默认懒加载 |
| input/textarea | worklet 键盘回调、输入法事件 |
| view/button | 完全支持 |
Skyline 新增组件
| 组件 | 说明 |
|---|---|
| list-view | 列表布局容器 |
| grid-view | 网格/瀑布流容器 |
| sticky-section/sticky-header | 吸顶布局 |
| nested-scroll-header/body | 嵌套滚动 |
| draggable-sheet | 半屏可拖拽 |
| share-element | 共享元素动画 |
| snapshot | 截图组件 |
| span | 内联混排 |
文档索引
根据需求快速定位(路径相对于 references/):
| 我想要... | 查阅文档 |
|---|---|
| 实现长列表/虚拟列表 | scroll/scroll-view.md |
| 下拉刷新/下拉二级 | scroll/scroll-view.md#下拉刷新 |
| 嵌套滚动 | scroll/nested-scroll.md |
| 列表/网格布局 | scroll/list-grid-view.md |
| 吸顶效果 | scroll/sticky.md |
| 半屏拖拽面板 | scroll/draggable-sheet.md |
| 轮播图/卡片效果 | layout/swiper.md |
| 文本处理 | media/text.md |
| 图片显示 | media/image.md |
| 输入框开发 | form/input.md |
| 共享元素动画 | special/share-element.md |
| 截图功能 | special/snapshot.md |
强制规则
MUST(必须遵守)
1. scroll-view 必须指定 type:Skyline 下使用 scroll-view 必须设置 type 属性
<!-- ✅ 正确 -->
<scroll-view type="list" scroll-y>...</scroll-view>
<!-- ❌ 错误:缺少 type -->
<scroll-view scroll-y>...</scroll-view>2. 横向滚动需 enable-flex:横向滚动需同时开启 enable-flex 以兼容 WebView
<scroll-view scroll-x enable-flex style="flex-direction: row;">3. 文本必须用 text 组件:内联文本只能用 <text> 组件
<!-- ✅ 正确 -->
<text>Hello <text>World</text></text>
<!-- ❌ 错误:内联文本不能用 view -->
<view>Hello <view>World</view></view>4. list-view/grid-view 必须在 custom 模式:
<scroll-view type="custom" scroll-y>
<list-view>...</list-view>
</scroll-view>5. 嵌套滚动必须使用 nested 模式:
<scroll-view type="nested" scroll-y>
<nested-scroll-header>...</nested-scroll-header>
<nested-scroll-body>...</nested-scroll-body>
</scroll-view>NEVER(禁止行为)
1. NEVER 在 scroll-view 外直接放置 list-view/grid-view 2. NEVER 在 list-view 中使用非直接子节点列表项 3. NEVER 在 Skyline 使用 web-view/editor/movable-view 组件 4. NEVER 依赖 image 的 WebView-only 裁剪模式(top/bottom/center/left/right)
scroll-view 核心用法
type 属性详解
| type 值 | 说明 | 使用场景 |
|---|---|---|
| list | 列表模式,按需渲染直接子节点 | 普通长列表 |
| custom | 自定义模式,支持 list-view/grid-view/sticky | 复杂布局 |
| nested | 嵌套模式,处理父子 scroll-view 联动 | Tab + 列表 |
基础列表模式
<!-- 列表模式:直接子节点按需渲染 -->
<scroll-view
type="list"
scroll-y
style="height: 100vh;"
bindscrolltolower="loadMore"
>
<view wx:for="{{list}}" wx:key="id">
{{item.name}}
</view>
</scroll-view>下拉刷新
<scroll-view
type="list"
scroll-y
refresher-enabled="{{true}}"
refresher-triggered="{{refreshing}}"
bindrefresherrefresh="onRefresh"
>
<view slot="refresher" class="custom-refresher">
自定义刷新区域
</view>
<!-- 列表内容 -->
</scroll-view>Page({
data: { refreshing: false },
onRefresh() {
this.setData({ refreshing: true })
// 加载数据...
setTimeout(() => {
this.setData({ refreshing: false })
}, 1000)
}
})嵌套滚动模式
<scroll-view type="nested" scroll-y>
<nested-scroll-header>
<view>顶部固定区域</view>
</nested-scroll-header>
<nested-scroll-body>
<swiper>
<swiper-item wx:for="{{tabs}}" wx:key="id">
<scroll-view
type="list"
scroll-y
associative-container="nested-scroll-view"
>
<!-- 列表内容 -->
</scroll-view>
</swiper-item>
</swiper>
</nested-scroll-body>
</scroll-view>Worklet 滚动回调
<scroll-view
type="list"
scroll-y
worklet:onscrollupdate="onScrollUpdate"
>Page({
onScrollUpdate(e) {
'worklet'
// 在 UI 线程执行,无延迟
console.log(e.detail.scrollTop)
}
})swiper 增强特性
布局类型
<!-- 堆叠效果 -->
<swiper layout-type="stackLeft">
<swiper-item>...</swiper-item>
</swiper>
<!-- 卡片滑动 -->
<swiper layout-type="tinder">
<swiper-item>...</swiper-item>
</swiper>
<!-- 过渡动画 -->
<swiper layout-type="transformer" transformer-type="threeD">
<swiper-item>...</swiper-item>
</swiper>指示器样式
<swiper
indicator-dots
indicator-type="worm"
indicator-color="rgba(0,0,0,0.3)"
indicator-active-color="#000"
indicator-margin="10"
indicator-spacing="4"
>| indicator-type | 效果 |
|---|---|
| normal | 默认圆点 |
| worm | 蠕虫动画 |
| expand | 展开动画 |
| jump | 跳跃动画 |
| slide | 滑动动画 |
| scale | 缩放动画 |
text 组件
文本溢出处理
<!-- 单行省略 -->
<text overflow="ellipsis" max-lines="1">
这是一段很长的文本内容...
</text>
<!-- 多行省略 -->
<text overflow="ellipsis" max-lines="3">
这是一段很长的文本内容...
</text>
<!-- 渐隐效果 -->
<text overflow="fade" max-lines="1">
这是一段很长的文本内容...
</text>内联混排
<!-- 使用 span 实现图文混排 -->
<span>
<image src="icon.png" style="width:20px;height:20px;" />
<text>图文混排内容</text>
</span>input 组件
Skyline 特有事件
<input
value="{{value}}"
bindinput="onInput"
bind:selectionchange="onSelectionChange"
bind:keyboardcompositionend="onCompositionEnd"
worklet:onkeyboardheightchange="onKeyboardHeight"
/>Page({
onKeyboardHeight(e) {
'worklet'
// UI 线程回调,可用于同步调整布局
const { height, pageBottomPadding } = e.detail
}
})draggable-sheet 半屏组件
<draggable-sheet
class="sheet"
initial-child-size="0.5"
min-child-size="0.2"
max-child-size="0.8"
snap="{{true}}"
snap-sizes="{{[0.4, 0.6]}}"
worklet:onsizeupdate="onSizeUpdate"
>
<scroll-view
scroll-y
type="list"
associative-container="draggable-sheet"
>
<!-- 内容 -->
</scroll-view>
</draggable-sheet>Page({
onReady() {
this.createSelectorQuery()
.select('.sheet')
.node()
.exec(res => {
const sheetContext = res[0].node
sheetContext.scrollTo({
size: 0.7,
animated: true,
duration: 300
})
})
}
})share-element 共享元素
页面间共享元素动画
源页面:
<share-element key="hero-image" transform>
<image src="{{item.cover}}" />
</share-element>目标页面:
<share-element key="hero-image" transform transition-on-gesture>
<image src="{{detail.cover}}" />
</share-element>属性说明
| 属性 | 说明 |
|---|---|
| key | 页面内唯一标识,源和目标必须匹配 |
| transform | 是否启用动画 |
| transition-on-gesture | 手势返回时是否动画 |
| rect-tween-type | 动画轨迹(materialRectArc/linear) |
性能优化
列表性能
1. 使用 list 模式:确保只渲染可见区域
<scroll-view type="list" scroll-y cache-extent="500">2. 指定 cache-extent:预渲染视口外区域,优化滚动体验
3. 使用 list-item 属性:启用样式共享
<view wx:for="{{list}}" wx:key="id" list-item>swiper 性能
<!-- 使用 cache-extent 预渲染相邻页 -->
<swiper cache-extent="1">组件差异速查
| 组件 | WebView | Skyline | 备注 |
|---|---|---|---|
| scroll-view | 无 type | 必须 type | Skyline 增强 |
| text | space 属性 | overflow/max-lines | 不同特性 |
| image | 9种裁剪模式 | 5种缩放模式 | 裁剪模式仅 WebView |
| swiper | snap-to-edge | layout-type/indicator-type | 不同特性 |
| input | placeholder-class | worklet 回调 | 不同特性 |
相关技能
| 场景 | 推荐技能 | 说明 |
|---|---|---|
| 滚动 API | skyline-scroll-api | ScrollViewContext 接口 |
| 动画开发 | skyline-worklet | Worklet 动画系统 |
| 样式开发 | skyline-wxss | WXSS 支持情况 |
| 路由转场 | skyline-route | 页面转场动画 |
| 兼容性诊断 | skyline-diagnostics | 自动检查组件兼容性问题 |
References 目录结构
references/
├── form/
│ └── input.md
├── gesture/
│ ├── gesture-negotiation.md
│ └── gesture-system.md
├── layout/
│ └── swiper.md
├── media/
│ ├── image.md
│ └── text.md
├── scroll/
│ ├── draggable-sheet.md
│ ├── list-grid-view.md
│ ├── nested-scroll.md
│ ├── scroll-view.md
│ └── sticky.md
└── special/
├── share-element.md
└── snapshot.mdinput 与 textarea 组件
概述
Skyline 下的 input 和 textarea 组件在保持与 WebView 兼容的基础上,新增了 worklet 键盘回调、输入法组合事件等增强特性,可以实现更流畅的键盘交互体验。
input 组件
基本用法
<input
value="{{value}}"
placeholder="请输入内容"
bindinput="onInput"
bindconfirm="onConfirm"
/>通用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| value | string | - | 输入框初始内容 |
| type | string | text | 输入类型 |
| password | boolean | false | 密码类型 |
| placeholder | string | - | 占位提示 |
| placeholder-style | string | - | 占位符样式 |
| disabled | boolean | false | 禁用 |
| maxlength | number | 140 | 最大长度,-1 为不限制 |
| cursor-spacing | number | 0 | 光标与键盘距离 |
| focus | boolean | false | 获取焦点 |
| confirm-type | string | done | 键盘右下角按钮文字 |
| confirm-hold | boolean | false | 点击完成不收起键盘 |
| cursor | number | - | 光标位置 |
| cursor-color | string | - | 光标颜色 |
| selection-start | number | -1 | 选区起始位置 |
| selection-end | number | -1 | 选区结束位置 |
| adjust-position | boolean | true | 键盘弹起时上推页面 |
| hold-keyboard | boolean | false | 点击页面不收起键盘 |
type 属性值
| 值 | 说明 |
|---|---|
| text | 文本键盘 |
| number | 数字键盘 |
| idcard | 身份证键盘 |
| digit | 带小数点数字键盘 |
| nickname | 昵称键盘(2.21.2+) |
confirm-type 属性值
| 值 | 按钮文字 |
|---|---|
| send | 发送 |
| search | 搜索 |
| next | 下一个 |
| go | 前往 |
| done | 完成 |
通用事件
| 事件 | 说明 | detail |
|---|---|---|
| bindinput | 输入时触发 | { value, cursor, keyCode } |
| bindchange | 内容改变(非聚焦) | { value } |
| bindfocus | 聚焦 | { value, height } |
| bindblur | 失焦 | { value } |
| bindconfirm | 点击完成按钮 | { value } |
| bindkeyboardheightchange | 键盘高度变化 | { height, duration } |
Skyline 特有属性
| 属性 | 类型 | 说明 |
|---|---|---|
| bind:selectionchange | event | 选区改变事件 |
| bind:keyboardcompositionstart | event | 输入法开始输入 |
| bind:keyboardcompositionupdate | event | 输入法输入字符 |
| bind:keyboardcompositionend | event | 输入法输入结束 |
| worklet:onkeyboardheightchange | worklet | 键盘高度变化(UI线程) |
Worklet 键盘回调
<input
value="{{value}}"
worklet:onkeyboardheightchange="onKeyboardHeight"
/>Page({
onKeyboardHeight(e) {
'worklet'
// 在 UI 线程执行,可实现同步动画
const { height, pageBottomPadding } = e.detail
// height: 键盘高度
// pageBottomPadding: 页面上推高度
}
})使用场景:
- 键盘弹起时同步调整其他元素位置
- 实现自定义的键盘跟随动画
输入法组合事件
<input
bind:keyboardcompositionstart="onCompositionStart"
bind:keyboardcompositionupdate="onCompositionUpdate"
bind:keyboardcompositionend="onCompositionEnd"
/>Page({
onCompositionStart(e) {
// 输入法开始新的输入(如拼音输入开始)
this.isComposing = true
},
onCompositionUpdate(e) {
// 输入法正在输入(如拼音字母输入中)
},
onCompositionEnd(e) {
// 输入法输入完成(如选择了中文字符)
this.isComposing = false
}
})使用场景:
- 实时搜索时避免在拼音输入过程中触发搜索
- 联想输入时等待输入法完成再处理
选区变化事件
<input bind:selectionchange="onSelectionChange" />Page({
onSelectionChange(e) {
const { selectionStart, selectionEnd } = e.detail
// 可用于实现富文本工具栏状态同步
}
})textarea 组件
textarea 的使用方式与 input 基本一致,主要区别:
特有属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| auto-height | boolean | false | 自动增高 |
| fixed | boolean | false | 在 position:fixed 区域时需设置 |
| show-confirm-bar | boolean | true | 是否显示完成栏 |
多行输入
<textarea
value="{{content}}"
placeholder="请输入详细描述"
auto-height
maxlength="500"
bindinput="onContentInput"
/>示例代码
搜索框
<view class="search-box">
<input
class="search-input"
type="text"
placeholder="搜索"
confirm-type="search"
value="{{keyword}}"
bindinput="onSearchInput"
bindconfirm="onSearch"
bind:keyboardcompositionend="onCompositionEnd"
/>
<view class="search-btn" bindtap="onSearch">搜索</view>
</view>Page({
data: {
keyword: '',
isComposing: false
},
onSearchInput(e) {
this.setData({ keyword: e.detail.value })
// 非输入法组合状态下才触发实时搜索
if (!this.isComposing) {
this.doSearch(e.detail.value)
}
},
onCompositionEnd(e) {
this.isComposing = false
this.doSearch(this.data.keyword)
},
onSearch() {
this.doSearch(this.data.keyword)
},
doSearch(keyword) {
// 执行搜索逻辑
}
})聊天输入框
<view class="chat-input" style="padding-bottom: {{bottomPadding}}px;">
<input
class="input"
value="{{message}}"
placeholder="输入消息"
confirm-type="send"
hold-keyboard
adjust-position="{{false}}"
bindinput="onMessageInput"
bindconfirm="sendMessage"
worklet:onkeyboardheightchange="onKeyboardHeight"
/>
<view class="send-btn" bindtap="sendMessage">发送</view>
</view>Page({
data: {
message: '',
bottomPadding: 0
},
onKeyboardHeight(e) {
'worklet'
const { height } = e.detail
// 使用 runOnJS 通知逻辑线程更新状态
wx.worklet.runOnJS(() => {
this.setData({ bottomPadding: height })
})()
},
sendMessage() {
if (!this.data.message.trim()) return
// 发送消息
this.setData({ message: '' })
}
})表单验证
<view class="form-item">
<text class="label">手机号</text>
<input
type="number"
maxlength="11"
placeholder="请输入手机号"
value="{{phone}}"
bindinput="onPhoneInput"
bindblur="validatePhone"
/>
<text wx:if="{{phoneError}}" class="error">{{phoneError}}</text>
</view>
<view class="form-item">
<text class="label">验证码</text>
<input
type="number"
maxlength="6"
placeholder="请输入验证码"
value="{{code}}"
bindinput="onCodeInput"
/>
<view
class="code-btn {{countdown > 0 ? 'disabled' : ''}}"
bindtap="getCode"
>
{{countdown > 0 ? countdown + 's' : '获取验证码'}}
</view>
</view>WebView 特有属性
以下属性仅在 WebView 下生效:
| 属性 | 说明 |
|---|---|
| placeholder-class | 占位符样式类 |
| safe-password-* | 安全键盘相关属性 |
注意事项
1. input 是原生组件,字体是系统字体,无法设置 font-family 2. 在 input 聚焦期间,避免使用 CSS 动画 3. 自定义组件中的 input 需要使用 wx://form-field behavior 才能被外层 form 获取 4. 键盘高度变化事件可能多次触发,应忽略相同高度值 5. Skyline 下 cursor-color 无限制,支持任意颜色值
手势协商
概述
手势协商是 Skyline 手势系统中解决手势冲突的核心机制。当多个手势组件嵌套时,默认行为是内层手势优先识别,一旦内层手势被激活,外层手势将失效。通过手势协商,可以让多个手势同时生效,并精确控制它们的触发条件。
冲突场景分析
典型冲突 1:嵌套滚动
场景:底部弹窗中包含可滚动列表
- 期望行为:列表滚动到顶部后,继续下拉可关闭弹窗
- 默认行为:列表始终响应滚动,弹窗无法通过下拉关闭
典型冲突 2:缩放与拖动
场景:图片查看器同时支持缩放和拖动
- 期望行为:单指拖动移动图片,双指缩放
- 默认行为:双指操作时只有缩放或拖动生效
典型冲突 3:横向滑动与页面返回
场景:列表项左滑删除功能
- 期望行为:左滑显示删除按钮
- 默认行为:可能与系统的右滑返回手势冲突
simultaneous-handlers 属性
基本用法
通过 simultaneous-handlers 属性声明允许同时触发的手势:
<!-- 外层手势 -->
<vertical-drag-gesture-handler
tag="outer"
simultaneous-handlers="{{['inner']}}"
>
<!-- 内层手势 -->
<vertical-drag-gesture-handler
tag="inner"
simultaneous-handlers="{{['outer']}}"
>
<view>内容</view>
</vertical-drag-gesture-handler>
</vertical-drag-gesture-handler>关键点: 1. 使用 tag 属性为手势组件命名 2. 双方都需要在 simultaneous-handlers 中声明对方的 tag 3. 声明后,两个手势的回调都会被触发
三层及以上嵌套
<pan-gesture-handler tag="a" simultaneous-handlers="{{['b', 'c']}}">
<pan-gesture-handler tag="b" simultaneous-handlers="{{['a', 'c']}}">
<pan-gesture-handler tag="c" simultaneous-handlers="{{['a', 'b']}}">
<view>内容</view>
</pan-gesture-handler>
</pan-gesture-handler>
</pan-gesture-handler>worklet:should-accept-gesture
在手势识别早期阶段决定是否接受该手势:
Page({
shouldAcceptGesture(evt) {
'worklet'
// 只在特定区域接受手势
if (evt.absoluteX < 100) {
return false // 不接受
}
return true // 接受
}
})调用时机:手势刚开始识别时,仅调用一次。
适用场景:
- 根据触摸位置判断是否响应
- 根据当前状态(如是否在编辑模式)判断
worklet:should-response-on-move
在手势进行中动态控制是否响应:
Page({
shouldResponseOnMove(evt) {
'worklet'
// 滚动到顶部后不再响应列表滚动
if (this._scrollTop.value <= 0 && evt.deltaY > 0) {
return false
}
return true
}
})调用时机:每次手指移动时都会调用。
适用场景:
- 列表滚动到边界后切换行为
- 根据滑动方向动态判断
native-view 原生组件代理
对于 scroll-view、swiper 等原生组件,需要使用 native-view 属性代理其内部手势:
<vertical-drag-gesture-handler
native-view="scroll-view"
worklet:should-response-on-move="shouldRespond"
>
<scroll-view scroll-y>
<!-- 内容 -->
</scroll-view>
</vertical-drag-gesture-handler>支持的 native-view 值:
scroll-view:代理滚动视图手势swiper:代理轮播组件手势
完整示例:评论区效果
实现类似视频号的评论区——列表滚动到顶部后继续下拉可关闭面板:
<!-- 外层:控制面板拖动 -->
<vertical-drag-gesture-handler
tag="sheet"
simultaneous-handlers="{{['list']}}"
worklet:ongesture="handleSheetDrag"
>
<view class="sheet">
<!-- 内层:控制列表滚动 -->
<vertical-drag-gesture-handler
tag="list"
simultaneous-handlers="{{['sheet']}}"
native-view="scroll-view"
worklet:should-response-on-move="shouldListRespond"
>
<scroll-view
scroll-y
type="list"
worklet:onscrollupdate="handleScrollUpdate"
>
<!-- 评论列表 -->
</scroll-view>
</vertical-drag-gesture-handler>
</view>
</vertical-drag-gesture-handler>Page({
onLoad() {
this._scrollTop = wx.worklet.shared(0)
this._sheetOffset = wx.worklet.shared(0)
this.applyAnimatedStyle('.sheet', () => {
'worklet'
return { transform: `translateY(${this._sheetOffset.value}px)` }
})
},
handleScrollUpdate(evt) {
'worklet'
this._scrollTop.value = evt.detail.scrollTop
},
shouldListRespond(evt) {
'worklet'
// 在顶部且下拉时,不响应列表滚动
if (this._scrollTop.value <= 0 && evt.deltaY > 0) {
return false
}
// 面板已拉下时,不响应列表滚动
if (this._sheetOffset.value > 0) {
return false
}
return true
},
handleSheetDrag(evt) {
'worklet'
const { spring } = wx.worklet
if (evt.state === GestureState.ACTIVE) {
// 只在列表顶部或面板已拉下时响应
if (this._scrollTop.value <= 0 || this._sheetOffset.value > 0) {
const newOffset = this._sheetOffset.value + evt.deltaY
this._sheetOffset.value = Math.max(0, newOffset)
}
} else if (evt.state === GestureState.END) {
// 下拉超过阈值或速度够快则关闭
if (this._sheetOffset.value > 200 || evt.velocityY > 1000) {
this._sheetOffset.value = spring(500)
wx.worklet.runOnJS(this.closeSheet.bind(this))()
} else {
this._sheetOffset.value = spring(0)
}
}
},
closeSheet() {
wx.navigateBack()
}
})最佳实践
1. 明确手势职责
为每个手势组件设定清晰的职责:
- 外层手势:控制整体容器
- 内层手势:控制内部交互
2. 使用 shared 值同步状态
通过 shared 值在多个手势回调间共享状态:
this._isListAtTop = wx.worklet.shared(true)
this._isDraggingSheet = wx.worklet.shared(false)3. 处理边界情况
确保在各种边界情况下都有正确行为:
- 快速滑动
- 滑动方向改变
- 手势取消
4. 提供视觉反馈
手势切换时提供平滑的视觉过渡:
// 使用 spring 动画而非直接设置值
this._offset.value = spring(targetValue)注意事项
1. 性能考虑:should-response-on-move 在每次移动时调用,避免复杂计算 2. 状态同步:使用 shared 值确保多个回调间状态一致 3. 降级处理:为不支持手势系统的环境提供替代方案 4. 调试技巧:使用 console.log 在 worklet 函数中输出状态,便于调试
手势系统
概述
Skyline 内置了一套手势系统,直接在 UI 线程响应用户的手势操作,避免了传统 WXS 方案的跨线程通信延迟,使拖拽、缩放等交互动画更加流畅。
手势组件是"虚组件",不参与布局渲染,事件由其直接子节点响应。
版本要求:需使用最新的 Nightly 工具进行调试。
手势组件列表
| 组件名 | 触发条件 | 典型用途 |
|---|---|---|
tap-gesture-handler | 点击 | 按钮点击、菜单选择 |
double-tap-gesture-handler | 双击 | 图片放大、点赞 |
long-press-gesture-handler | 长按 | 菜单弹出、删除确认 |
pan-gesture-handler | 拖动(横向/纵向) | 拖拽排序、滑动删除 |
horizontal-drag-gesture-handler | 横向滑动 | 左滑删除、抽屉菜单 |
vertical-drag-gesture-handler | 纵向滑动 | 下拉刷新、页面返回 |
scale-gesture-handler | 多指缩放 | 图片缩放、地图操作 |
force-press-gesture-handler | iPhone 重按 | 3D Touch 快捷操作 |
手势状态
手势识别过程包含以下状态,通过回调参数中的 state 字段判断:
| 状态 | 值 | 说明 |
|---|---|---|
POSSIBLE | 0 | 手势未识别(手指刚接触屏幕) |
BEGIN | 1 | 手势已识别(开始生效) |
ACTIVE | 2 | 连续手势活跃状态(移动中) |
END | 3 | 手势终止(手指离开) |
CANCELLED | 4 | 手势取消(被中断或识别失败) |
使用示例:
Page({
handlePan(evt) {
'worklet'
if (evt.state === GestureState.BEGIN) {
// 手势开始
} else if (evt.state === GestureState.ACTIVE) {
// 手势进行中
this._offset.value += evt.deltaX
} else if (evt.state === GestureState.END) {
// 手势结束
} else if (evt.state === GestureState.CANCELLED) {
// 手势取消
}
}
})通用属性
所有手势组件共享以下属性:
| 属性名 | 类型 | 必填 | 说明 |
|---|---|---|---|
tag | string | 否 | 手势协商时的组件标识 |
worklet:ongesture | eventhandler | 否 | 手势识别成功的回调(必须为 Worklet) |
worklet:should-response-on-move | callback | 否 | 手指移动过程中是否响应该手势 |
worklet:should-accept-gesture | callback | 否 | 手势是否应该被识别 |
simultaneous-handlers | Array<string> | 否 | 声明可同时触发的手势节点 tag 列表 |
native-view | string | 否 | 代理的原生节点类型(如 scroll-view) |
基本用法
pan-gesture-handler 拖拽
<pan-gesture-handler worklet:ongesture="handlePan">
<view class="draggable">可拖拽元素</view>
</pan-gesture-handler>Page({
onLoad() {
const offset = wx.worklet.shared(0)
this.applyAnimatedStyle('.draggable', () => {
'worklet'
return {
transform: `translateX(${offset.value}px)`
}
})
this._offset = offset
},
handlePan(evt) {
'worklet'
if (evt.state === GestureState.ACTIVE) {
this._offset.value += evt.deltaX
}
}
})scale-gesture-handler 缩放
<scale-gesture-handler worklet:ongesture="handleScale">
<image class="photo" src="{{url}}" />
</scale-gesture-handler>Page({
onLoad() {
const scale = wx.worklet.shared(1)
this._scale = scale
this.applyAnimatedStyle('.photo', () => {
'worklet'
return { transform: `scale(${scale.value})` }
})
},
handleScale(evt) {
'worklet'
if (evt.state === GestureState.ACTIVE) {
this._scale.value = evt.scale
}
}
})回调参数
pan/horizontal-drag/vertical-drag-gesture-handler
| 参数名 | 类型 | 说明 |
|---|---|---|
| state | number | 手势状态(0-4) |
| absoluteX | number | 相对于全局(屏幕)的 X 坐标 |
| absoluteY | number | 相对于全局(屏幕)的 Y 坐标 |
| deltaX | number | 相对上一次回调,X 轴方向移动距离 |
| deltaY | number | 相对上一次回调,Y 轴方向移动距离 |
| velocityX | number | 手指离开时的横向速度(px/s) |
| velocityY | number | 手指离开时的纵向速度(px/s) |
scale-gesture-handler
| 参数名 | 类型 | 说明 |
|---|---|---|
| state | number | 手势状态 |
| focalX | number | 中心点相对于全局的 X 坐标 |
| focalY | number | 中心点相对于全局的 Y 坐标 |
| focalDeltaX | number | 相对上一次,中心点 X 轴移动距离 |
| focalDeltaY | number | 相对上一次,中心点 Y 轴移动距离 |
| scale | number | 累计缩放比例(1 为原始大小) |
| rotation | number | 旋转角度(单位:弧度) |
| pointerCount | number | 当前跟踪的手指数 |
tap/double-tap-gesture-handler
| 参数名 | 类型 | 说明 |
|---|---|---|
| state | number | 手势状态 |
| absoluteX | number | 相对于全局的 X 坐标 |
| absoluteY | number | 相对于全局的 Y 坐标 |
long-press-gesture-handler
| 参数名 | 类型 | 说明 |
|---|---|---|
| state | number | 手势状态 |
| absoluteX | number | 相对于全局的 X 坐标 |
| absoluteY | number | 相对于全局的 Y 坐标 |
| translationX | number | 相对于初始触摸点的 X 轴偏移量 |
| translationY | number | 相对于初始触摸点的 Y 轴偏移量 |
force-press-gesture-handler
| 参数名 | 类型 | 说明 |
|---|---|---|
| state | number | 手势状态 |
| absoluteX | number | 相对于全局的 X 坐标 |
| absoluteY | number | 相对于全局的 Y 坐标 |
| pressure | number | 压力大小(0-1 之间) |
使用规则
MUST(必须遵守)
- 必须声明 `'worklet'` 指令:所有手势回调函数开头必须声明
- 必须在 Skyline 模式下使用:手势组件仅支持 Skyline 渲染
- 必须使用单子节点:手势组件只能包含一个直接子节点
NEVER(禁止行为)
- 禁止在 WebView 模式使用:手势组件不支持 WebView
- 禁止在回调中执行耗时操作:回调运行在 UI 线程,阻塞会导致卡顿
- 禁止忘记处理 CANCELLED 状态:手势被中断时需要正确恢复状态
注意事项
1. 手势不冒泡:手势事件与普通 touch 事件不同,不会发生冒泡 2. 回调在 UI 线程:worklet:ongesture 等回调运行在 UI 线程,可直接操作 shared 值 3. 坐标参考系:absoluteX/Y 相对于屏幕,deltaX/Y 相对于上一次回调 4. 速度单位:velocityX/Y 单位为 pixels per second(每秒像素数)
示例代码片段
swiper 组件增强特性
概述
Skyline 下的 swiper 组件在保持与 WebView 兼容的基础上,新增了多种布局模式(layout-type)、过渡动画(transformer-type)和指示器动画(indicator-type)等增强特性。
基本用法
<swiper
indicator-dots
autoplay
circular
style="height: 200px;"
>
<swiper-item wx:for="{{banners}}" wx:key="id">
<image src="{{item.image}}" mode="aspectFill" />
</swiper-item>
</swiper>通用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| indicator-dots | boolean | false | 显示指示点 |
| indicator-color | color | rgba(0,0,0,0.3) | 指示点颜色 |
| indicator-active-color | color | #000 | 当前指示点颜色 |
| autoplay | boolean | false | 自动播放 |
| current | number | 0 | 当前滑块索引 |
| interval | number | 5000 | 自动播放间隔(ms) |
| duration | number | 500 | 滑动动画时长(ms) |
| circular | boolean | false | 循环滑动 |
| vertical | boolean | false | 纵向滑动 |
| display-multiple-items | number | 1 | 同时显示滑块数 |
| previous-margin | string | "0px" | 前边距 |
| next-margin | string | "0px" | 后边距 |
| easing-function | string | "default" | 缓动函数 |
Skyline 特有属性
layout-type 布局类型
| 值 | 效果 | 说明 |
|---|---|---|
| normal | 默认 | 标准轮播 |
| stackLeft | 左向堆叠 | 卡片向左堆叠效果 |
| stackRight | 右向堆叠 | 卡片向右堆叠效果 |
| tinder | 滑动卡片 | 类似 Tinder App 的卡片效果 |
| transformer | 过渡动画 | 配合 transformer-type 使用 |
transformer-type 过渡动画
当 layout-type="transformer" 时生效:
| 值 | 效果 |
|---|---|
| scaleAndFade | 缩放淡入淡出 |
| accordion | 手风琴效果 |
| threeD | 3D 翻转 |
| zoomIn | 放大进入 |
| zoomOut | 缩小退出 |
| deepthPage | 深度页面 |
indicator-type 指示器动画
| 值 | 效果 |
|---|---|
| normal | 默认圆点 |
| worm | 蠕虫动画 |
| wormThin | 细蠕虫 |
| wormUnderground | 下沉蠕虫 |
| wormThinUnderground | 细下沉蠕虫 |
| expand | 展开动画 |
| jump | 跳跃动画 |
| jumpWithOffset | 偏移跳跃 |
| scroll | 滚动指示器 |
| scrollFixedCenter | 固定中心滚动 |
| slide | 滑动动画 |
| slideUnderground | 下沉滑动 |
| scale | 缩放动画 |
| swap | 交换动画 |
| swapYRotation | Y轴旋转交换 |
| color | 颜色渐变 |
指示器样式属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| indicator-margin | number | 10 | 指示点四周边距 |
| indicator-spacing | number | 4 | 指示点间距 |
| indicator-radius | number | 4 | 指示点圆角 |
| indicator-width | number | 8 | 指示点宽度 |
| indicator-height | number | 8 | 指示点高度 |
| indicator-alignment | Array/string | auto | 指示点位置 |
| indicator-offset | Array | [0,0] | 指示点偏移 |
indicator-alignment
auto: 横向滑动时居底中,纵向滑动时居中右- 数组:
[x, y],取值范围 [-1, 1],如[0, 1]表示底边中点
其他 Skyline 属性
| 属性 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
| scroll-with-animation | boolean | true | 改变 current 时使用动画 | - |
| cache-extent | number | 0 | 预渲染区域(1=上下各一屏) | - |
| direction | string | all | 滑动方向限制:all/positive/negative | 3.8.10 |
direction 属性说明:
all:默认,允许双向滑动positive:vertical=true 时允许下滑,vertical=false 时允许右滑negative:vertical=true 时允许上滑,vertical=false 时允许左滑
Worklet 回调
<swiper
worklet:onscrollstart="onScrollStart"
worklet:onscrollupdate="onScrollUpdate"
worklet:onscrollend="onScrollEnd"
>Page({
onScrollStart(e) {
'worklet'
console.log('开始滑动', e.detail.dx, e.detail.dy)
},
onScrollUpdate(e) {
'worklet'
// 可用于同步动画
const { dx, dy } = e.detail
},
onScrollEnd(e) {
'worklet'
console.log('滑动结束', e.detail.dx, e.detail.dy)
}
})通过手势监听屏蔽用户拖拽
swiper 可以配合手势组件屏蔽用户手势,最常见的是屏蔽用户拖拽输入。这个能力来自手势监听对原生组件输入的拦截,而不是 swiper 默认自带的自动禁用行为。
典型做法是在外层包裹 horizontal-drag-gesture-handler,并通过 native-view="swiper" 代理内部手势;当 worklet:should-accept-gesture 返回 false 时,用户横向拖拽不会交给 swiper 处理。
<horizontal-drag-gesture-handler
native-view="swiper"
worklet:should-accept-gesture="onSwiperShouldResponse"
>
<swiper class="swiper" autoplay circular interval="{{3000}}" duration="{{500}}">
<swiper-item wx:for="{{swiperList}}" wx:key="id">
<view class="swiper-item">{{item.title}}</view>
</swiper-item>
</swiper>
</horizontal-drag-gesture-handler>Page({
onSwiperShouldResponse() {
'worklet'
return false
}
})如果需要按状态决定是否允许用户滑动,可以在回调里结合当前业务状态有条件地返回 true 或 false。
示例代码
堆叠效果
<swiper
layout-type="stackLeft"
style="height: 300px;"
>
<swiper-item wx:for="{{cards}}" wx:key="id">
<view class="card" style="background: {{item.color}}">
{{item.title}}
</view>
</swiper-item>
</swiper>Tinder 卡片效果
<swiper
layout-type="tinder"
style="height: 400px;"
bindchange="onCardChange"
>
<swiper-item wx:for="{{users}}" wx:key="id">
<view class="user-card">
<image src="{{item.avatar}}" mode="aspectFill" />
<text>{{item.name}}, {{item.age}}</text>
</view>
</swiper-item>
</swiper>3D 过渡效果
<swiper
layout-type="transformer"
transformer-type="threeD"
style="height: 200px;"
>
<swiper-item wx:for="{{banners}}" wx:key="id">
<image src="{{item.image}}" mode="aspectFill" />
</swiper-item>
</swiper>蠕虫指示器
<swiper
indicator-dots
indicator-type="worm"
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#fff"
style="height: 200px;"
>
<swiper-item wx:for="{{banners}}" wx:key="id">
<image src="{{item.image}}" mode="aspectFill" />
</swiper-item>
</swiper>自定义指示器位置
<swiper
indicator-dots
indicator-type="expand"
indicator-alignment="{{[0, 0.9]}}"
indicator-offset="{{[0, -20]}}"
indicator-width="6"
indicator-height="6"
indicator-spacing="6"
>
<!-- ... -->
</swiper>缓存预加载
<!-- 预渲染相邻一屏区域 -->
<swiper cache-extent="1">
<swiper-item wx:for="{{heavyItems}}" wx:key="id">
<!-- 复杂内容 -->
</swiper-item>
</swiper>注意事项
1. layout-type 为 stackLeft、stackRight 和 tinder 时仅支持 indicator-type=normal 2. indicator-type 为 scrollFixedCenter、swap、swapYRotation 时不支持循环模式 circular 3. Skyline 的 previous-margin、display-multiple-items 和 vertical 与 WebView 表现略有不同 4. 当 next-margin > 0 时,Skyline 会将上述属性对齐 WebView 实现
change 事件 source 字段
| 值 | 说明 |
|---|---|
| autoplay | 自动播放导致变化 |
| touch | 用户滑动导致变化 |
| "" | 其他原因 |
Page({
onSwiperChange(e) {
const { current, source } = e.detail
// 避免在 autoplay 时重复设置
if (source === 'touch') {
this.setData({ current })
}
}
})完整示例:图片轮播
<view class="banner-container">
<swiper
class="banner"
indicator-dots
indicator-type="worm"
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#fff"
autoplay
circular
interval="{{4000}}"
duration="{{500}}"
>
<swiper-item wx:for="{{banners}}" wx:key="id">
<image
class="banner-image"
src="{{item.image}}"
mode="aspectFill"
bindtap="onBannerTap"
data-url="{{item.url}}"
/>
</swiper-item>
</swiper>
</view>.banner-container {
width: 100%;
padding: 12px;
box-sizing: border-box;
}
.banner {
height: 150px;
border-radius: 8px;
overflow: hidden;
}
.banner-image {
width: 100%;
height: 100%;
}示例代码片段
增强特性演示:{% minicode('2XhTe3m67uTb') %} 基础示例:{% minicode('mI88kSm57nJ9') %}
image 组件
概述
Skyline 下的 image 组件默认懒加载,并新增了 fade-in 渐显效果属性。部分 WebView 的裁剪模式在 Skyline 下不支持。
基本用法
<image
src="https://example.com/image.jpg"
mode="aspectFill"
bindload="onImageLoad"
binderror="onImageError"
/>通用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| src | string | - | 图片地址(支持云文件ID) |
| mode | string | scaleToFill | 裁剪/缩放模式 |
| show-menu-by-longpress | boolean | false | 长按显示菜单 |
| binderror | event | - | 加载失败回调 |
| bindload | event | - | 加载完成回调 |
mode 属性
通用缩放模式
| 值 | 说明 | Skyline | WebView |
|---|---|---|---|
| scaleToFill | 拉伸填满(不保持比例) | ✅ | ✅ |
| aspectFit | 保持比例,完全显示(可能有留白) | ✅ | ✅ |
| aspectFill | 保持比例,填满(可能被裁剪) | ✅ | ✅ |
| widthFix | 宽度不变,高度自动 | ✅ | ✅ |
| heightFix | 高度不变,宽度自动 | ✅ | ✅ |
WebView-only 裁剪模式
以下模式仅在 WebView 下支持,Skyline 不支持:
| 值 | 说明 |
|---|---|
| top | 只显示顶部 |
| bottom | 只显示底部 |
| center | 只显示中间 |
| left | 只显示左边 |
| right | 只显示右边 |
| top left | 只显示左上 |
| top right | 只显示右上 |
| bottom left | 只显示左下 |
| bottom right | 只显示右下 |
Skyline 特有属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| fade-in | boolean | false | 图片加载完成后渐显 |
渐显效果
<image
src="{{imageUrl}}"
mode="aspectFill"
fade-in
/>使用场景:
- 图片列表加载
- 大图展示
- 提升视觉体验
WebView 特有属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| webp | boolean | false | 解析 webP 格式(仅网络图片) |
| lazy-load | boolean | false | 懒加载(进入三屏内才加载) |
| forceHttps | boolean | false | 自动将 http 替换为 https |
注意:Skyline 下默认懒加载,无需设置 lazy-load
事件回调
加载完成
Page({
onImageLoad(e) {
const { width, height } = e.detail
console.log(`图片尺寸: ${width} x ${height}`)
}
})加载失败
Page({
onImageError(e) {
console.log('图片加载失败:', e.detail.errMsg)
// 可设置默认图片
this.setData({ imageUrl: '/images/default.png' })
}
})长按菜单
<image
src="{{imageUrl}}"
show-menu-by-longpress
/>支持的菜单选项:
- 发送给朋友
- 收藏
- 保存图片
- 搜一搜
- 打开名片/前往群聊/打开小程序(若图片包含对应二维码)
支持识别的码
| 类型 | 最低版本 |
|---|---|
| 小程序码 | - |
| 微信个人码 | 2.18.0 |
| 企业微信个人码 | 2.18.0 |
| 普通群码 | 2.18.0 |
| 互通群码 | 2.18.0 |
| 公众号二维码 | 2.18.0 |
示例代码
头像显示
<image
class="avatar"
src="{{user.avatar}}"
mode="aspectFill"
binderror="onAvatarError"
/>.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #f5f5f5;
}图片列表
<view class="image-list">
<image
wx:for="{{images}}"
wx:key="id"
class="image-item"
src="{{item.url}}"
mode="aspectFill"
fade-in
bindtap="previewImage"
data-index="{{index}}"
/>
</view>Page({
previewImage(e) {
const { index } = e.currentTarget.dataset
wx.previewImage({
current: this.data.images[index].url,
urls: this.data.images.map(img => img.url)
})
}
})自适应高度
<!-- widthFix: 宽度固定,高度自适应 -->
<image
class="banner"
src="{{bannerUrl}}"
mode="widthFix"
style="width: 100%;"
/>
<!-- heightFix: 高度固定,宽度自适应 -->
<image
class="icon"
src="{{iconUrl}}"
mode="heightFix"
style="height: 24px;"
/>带占位符的图片
<view class="image-wrapper">
<image
wx:if="{{imageLoaded}}"
src="{{imageUrl}}"
mode="aspectFill"
fade-in
bindload="onImageLoad"
/>
<view wx:else class="placeholder">
<text class="loading-text">加载中...</text>
</view>
</view>Page({
data: {
imageLoaded: false,
imageUrl: 'https://example.com/image.jpg'
},
onImageLoad() {
this.setData({ imageLoaded: true })
}
})错误处理
<image
src="{{imageUrl || defaultImage}}"
mode="aspectFill"
binderror="onImageError"
/>Page({
data: {
imageUrl: '',
defaultImage: '/images/default.png'
},
onImageError() {
this.setData({ imageUrl: '' })
}
})SVG 支持
注意事项
1. SVG 格式使用 mode="scaleToFill" 时:
- WebView:会居中显示(除非 SVG 添加
preserveAspectRatio="none") - Skyline:会撑满容器
2. SVG 格式不支持:
- 百分比单位
<style>元素
性能优化
1. 使用合适的 mode
<!-- 列表缩略图:使用 aspectFill 避免变形 -->
<image src="{{item.cover}}" mode="aspectFill" />
<!-- Banner:使用 widthFix 保持比例 -->
<image src="{{banner}}" mode="widthFix" style="width: 100%;" />2. 利用默认懒加载
Skyline 下图片默认懒加载,无需额外配置。
3. 使用渐显效果
<!-- 大图建议使用 fade-in 改善体验 -->
<image src="{{largeImage}}" mode="aspectFill" fade-in />4. 图片尺寸优化
使用 CDN 时指定合适的尺寸:
// 根据设备像素比获取合适尺寸
const ratio = wx.getSystemInfoSync().pixelRatio
const imageUrl = `${baseUrl}?width=${300 * ratio}`注意事项
1. 默认尺寸:image 组件默认宽度 320px、高度 240px 2. 小数精度:缩放后的宽高可能有小数,不同内核渲染可能会抹去小数 3. Skyline 懒加载:Skyline 默认懒加载,WebView 需要手动设置 lazy-load 4. 裁剪模式兼容:需要 WebView 裁剪模式时,应判断渲染引擎做兼容处理
示例代码片段
{% minicode('NwPpTRmS7AJf') %}
text 与 span 组件
概述
Skyline 对文本处理有特殊要求:内联文本只能使用 text 组件。同时,Skyline 新增了 span 组件用于图文混排,并为 text 组件增加了 overflow 和 max-lines 属性实现文本溢出处理。
核心规则
MUST(必须遵守)
1. 内联文本必须用 text 组件
<!-- ✅ 正确 -->
<text>Hello <text>World</text></text>
<!-- ❌ 错误 -->
<view>Hello <view>World</view></view>2. text 组件内只能嵌套 text
<!-- ✅ 正确 -->
<text>
<text style="color: red;">红色</text>文字
</text>
<!-- ❌ 错误 -->
<text>
<view>不能嵌套 view</view>
</text>text 组件
通用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| user-select | boolean | false | 文本是否可选(会使节点变为 inline-block) |
Skyline 特有属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| overflow | string | visible | 文本溢出处理 |
| max-lines | number | - | 最大行数限制 |
overflow 属性值
| 值 | 效果 |
|---|---|
| visible | 不截断(默认) |
| clip | 直接裁剪 |
| fade | 渐隐效果 |
| ellipsis | 显示省略号 |
WebView 特有属性
| 属性 | 类型 | 说明 |
|---|---|---|
| space | string | 显示连续空格:ensp/emsp/nbsp |
| decode | boolean | 是否解码 HTML 实体 |
文本溢出处理
单行省略
<text
overflow="ellipsis"
max-lines="1"
style="width: 200px;"
>
这是一段很长的文本内容,超出部分会显示省略号
</text>多行省略
<text
overflow="ellipsis"
max-lines="3"
style="width: 200px;"
>
这是一段很长的文本内容,
当文本超过三行时,
第三行末尾会显示省略号,
后面的内容会被截断...
</text>渐隐效果
<text
overflow="fade"
max-lines="1"
style="width: 200px;"
>
这是一段很长的文本,末尾会渐隐消失
</text>span 组件
span 是 Skyline 新增的内联容器组件,用于实现图文混排。
基本用法
<span>
<image src="icon.png" style="width: 16px; height: 16px;" />
<text>图文混排内容</text>
</span>图标 + 文字
<span class="tag">
<image class="tag-icon" src="/images/hot.png" />
<text class="tag-text">热门</text>
</span>.tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background: #fff5f5;
border-radius: 4px;
}
.tag-icon {
width: 14px;
height: 14px;
margin-right: 4px;
}
.tag-text {
font-size: 12px;
color: #f5222d;
}文字 + 链接
<span>
<text>同意</text>
<navigator url="/pages/agreement/index">
<text style="color: #1890ff;">《用户协议》</text>
</navigator>
<text>和</text>
<navigator url="/pages/privacy/index">
<text style="color: #1890ff;">《隐私政策》</text>
</navigator>
</span>价格显示
<span class="price">
<text class="currency">¥</text>
<text class="amount">99</text>
<text class="decimal">.00</text>
</span>.price {
display: inline-flex;
align-items: baseline;
color: #f5222d;
}
.currency {
font-size: 12px;
}
.amount {
font-size: 24px;
font-weight: bold;
}
.decimal {
font-size: 12px;
}示例代码
文章内容
<view class="article">
<text class="title" max-lines="2" overflow="ellipsis">
{{article.title}}
</text>
<text class="summary" max-lines="3" overflow="ellipsis">
{{article.summary}}
</text>
<span class="meta">
<image class="avatar" src="{{article.author.avatar}}" />
<text class="author">{{article.author.name}}</text>
<text class="time">{{article.time}}</text>
</span>
</view>.article {
padding: 16px;
}
.title {
display: block;
font-size: 18px;
font-weight: bold;
line-height: 1.4;
color: #333;
}
.summary {
display: block;
margin-top: 8px;
font-size: 14px;
line-height: 1.6;
color: #666;
}
.meta {
display: flex;
align-items: center;
margin-top: 12px;
}
.avatar {
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 8px;
}
.author {
font-size: 12px;
color: #999;
margin-right: 12px;
}
.time {
font-size: 12px;
color: #999;
}商品标签
<view class="product-title">
<span wx:for="{{product.tags}}" wx:key="*this" class="tag {{item}}">
<text>{{item === 'new' ? '新品' : item === 'hot' ? '热卖' : '特价'}}</text>
</span>
<text class="name" max-lines="2" overflow="ellipsis">
{{product.name}}
</text>
</view>.product-title {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.tag {
padding: 0 4px;
margin-right: 4px;
border-radius: 2px;
font-size: 10px;
}
.tag.new {
background: #e6f7ff;
color: #1890ff;
}
.tag.hot {
background: #fff2e8;
color: #fa541c;
}
.name {
flex: 1;
font-size: 14px;
color: #333;
line-height: 1.4;
}可选文本
<text user-select class="selectable-text">
这段文字可以被选中复制
</text>注意:设置 user-select 后,text 会变为 inline-block 显示注意事项
1. 文本节点:在 Skyline 中,纯文本必须用 <text> 包裹 2. 嵌套规则:text 内只能嵌套 text,不能嵌套其他组件 3. 长按选中:除了文本节点外的其他节点都无法长按选中 4. 操作系统差异:各系统的空格标准不一致 5. decode:可解析 < > & '    
WebView vs Skyline 对比
| 特性 | WebView | Skyline |
|---|---|---|
| 空格处理 | space 属性 | 不支持 |
| HTML 实体 | decode 属性 | 不支持 |
| 文本溢出 | CSS 实现 | overflow + max-lines |
| 图文混排 | 任意嵌套 | 必须用 span |
示例代码片段
{% minicode('LQxYkQmm7fJj') %}
draggable-sheet 半屏可拖拽组件
概述
draggable-sheet 是 Skyline 新增的半屏可拖拽组件,用于实现类似地图 App 底部面板的效果。用户可以通过拖拽来调整面板的高度。
基本用法
<draggable-sheet
class="sheet"
initial-child-size="0.5"
min-child-size="0.2"
max-child-size="0.8"
>
<scroll-view scroll-y type="list" associative-container="draggable-sheet">
<!-- 内容 -->
</scroll-view>
</draggable-sheet>属性说明
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| initial-child-size | number | 0.5 | 初始占父容器比例 |
| min-child-size | number | 0.25 | 最小占父容器比例 |
| max-child-size | number | 1.0 | 最大占父容器比例 |
| snap | boolean | false | 拖拽后是否自动对齐关键点 |
| snap-sizes | Array | [] | 对齐关键点(不含最小/最大值) |
| worklet:onsizeupdate | worklet | - | 尺寸变化回调(worklet) |
吸附功能
设置 snap 和 snap-sizes 可以让面板在拖拽松手后自动吸附到指定位置:
<draggable-sheet
snap="{{true}}"
snap-sizes="{{[0.3, 0.5, 0.7]}}"
min-child-size="0.2"
max-child-size="0.9"
>上例中,松手后面板会自动吸附到 0.2、0.3、0.5、0.7、0.9 中最近的位置。
配合 scroll-view 使用
重要:内部的 scroll-view 必须设置 associative-container="draggable-sheet" 才能正确联动。
<draggable-sheet class="sheet">
<scroll-view
scroll-y
type="list"
associative-container="draggable-sheet"
bounces="{{true}}"
>
<view wx:for="{{list}}" wx:key="id" class="item">
{{item.name}}
</view>
</scroll-view>
</draggable-sheet>联动策略:
- 向上拖拽时:先展开面板,面板到达最大高度后滚动内容
- 向下拖拽时:先滚动内容到顶部,然后收起面板
DraggableSheetContext API
通过 createSelectorQuery 获取组件节点后,可以使用以下 API:
scrollTo
Page({
onReady() {
this.createSelectorQuery()
.select('.sheet')
.node()
.exec(res => {
const sheetContext = res[0].node
// 滚动到指定位置
sheetContext.scrollTo({
size: 0.7, // 目标尺寸比例
animated: true, // 是否动画
duration: 300, // 动画时长(ms)
easingFunction: 'ease' // 缓动函数
})
})
}
})scrollTo 参数
| 参数 | 类型 | 说明 |
|---|---|---|
| size | number | 目标尺寸比例(0~1) |
| animated | boolean | 是否使用动画 |
| duration | number | 动画时长(毫秒) |
| easingFunction | string | 缓动函数 |
缓动函数可选值:
easeease-inease-outease-in-outlinear
Worklet 回调
使用 worklet:onsizeupdate 可以在 UI 线程监听尺寸变化:
<draggable-sheet
class="sheet"
worklet:onsizeupdate="onSizeUpdate"
>Page({
onSizeUpdate(e) {
'worklet'
// 在 UI 线程执行
const { pixels, size } = e
console.log(`尺寸变化: ${pixels}px, 比例: ${size}`)
// 可以在这里做同步动画
// 例如:根据面板高度调整其他元素透明度
}
})完整示例:地图底部面板
<view class="container">
<!-- 地图区域 -->
<map class="map" latitude="{{latitude}}" longitude="{{longitude}}" />
<!-- 底部面板 -->
<draggable-sheet
class="sheet"
initial-child-size="0.4"
min-child-size="0.15"
max-child-size="0.85"
snap="{{true}}"
snap-sizes="{{[0.4]}}"
worklet:onsizeupdate="onSizeUpdate"
>
<scroll-view
class="sheet-content"
scroll-y
type="list"
associative-container="draggable-sheet"
>
<!-- 拖拽指示条 -->
<view class="drag-handle">
<view class="handle-bar"></view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<input placeholder="搜索地点" />
</view>
<!-- 附近地点列表 -->
<view class="section-title">附近地点</view>
<view
wx:for="{{nearbyPlaces}}"
wx:key="id"
class="place-item"
bindtap="onPlaceTap"
data-place="{{item}}"
>
<view class="place-icon">
<image src="{{item.icon}}" />
</view>
<view class="place-info">
<text class="place-name">{{item.name}}</text>
<text class="place-address">{{item.address}}</text>
</view>
<text class="place-distance">{{item.distance}}</text>
</view>
</scroll-view>
</draggable-sheet>
</view>.container {
position: relative;
height: 100vh;
}
.map {
width: 100%;
height: 100%;
}
.sheet {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-radius: 16px 16px 0 0;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.sheet-content {
height: 100%;
}
.drag-handle {
display: flex;
justify-content: center;
padding: 12px 0;
}
.handle-bar {
width: 40px;
height: 4px;
background: #ddd;
border-radius: 2px;
}
.search-box {
margin: 0 16px 16px;
padding: 8px 12px;
background: #f5f5f5;
border-radius: 8px;
}
.section-title {
padding: 12px 16px;
font-size: 14px;
color: #999;
}
.place-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.place-icon {
width: 40px;
height: 40px;
margin-right: 12px;
}
.place-icon image {
width: 100%;
height: 100%;
}
.place-info {
flex: 1;
}
.place-name {
display: block;
font-size: 15px;
color: #333;
}
.place-address {
display: block;
font-size: 12px;
color: #999;
margin-top: 4px;
}
.place-distance {
font-size: 12px;
color: #999;
}Page({
data: {
latitude: 39.908823,
longitude: 116.397470,
nearbyPlaces: [
{ id: 1, name: '天安门广场', address: '北京市东城区', distance: '500m', icon: '/images/landmark.png' },
{ id: 2, name: '故宫博物院', address: '北京市东城区景山前街4号', distance: '1.2km', icon: '/images/museum.png' },
// ...
]
},
onSizeUpdate(e) {
'worklet'
console.log('sheet size:', e.size)
},
onPlaceTap(e) {
const place = e.currentTarget.dataset.place
// 处理地点点击
},
// 展开面板
expandSheet() {
this.createSelectorQuery()
.select('.sheet')
.node()
.exec(res => {
res[0].node.scrollTo({
size: 0.85,
animated: true,
duration: 300
})
})
},
// 收起面板
collapseSheet() {
this.createSelectorQuery()
.select('.sheet')
.node()
.exec(res => {
res[0].node.scrollTo({
size: 0.15,
animated: true,
duration: 300
})
})
}
})注意事项
1. 仅限 Skyline:该组件仅在 Skyline 渲染模式下可用 2. 关联容器:内部 scroll-view 必须设置 associative-container="draggable-sheet" 3. 尺寸比例:所有尺寸属性都是相对于父容器的比例(0~1) 4. worklet 回调:onsizeupdate 仅支持 worklet 函数
示例代码片段
{% minicode('K5M2NamJ7ILV') %}
list-view 与 grid-view
概述
list-view 和 grid-view 是 Skyline 新增的布局容器组件,用于实现列表和网格布局。它们必须作为 <scroll-view type="custom"> 或 <sticky-section> 的直接子节点使用。
list-view 列表布局
基本用法
<scroll-view type="custom" scroll-y style="height: 100vh;">
<list-view>
<view wx:for="{{list}}" wx:key="id">
{{item.name}}
</view>
</list-view>
</scroll-view>属性说明
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| padding | Array | [0,0,0,0] | 内边距 [top, right, bottom, left] |
配合吸顶使用
<scroll-view type="custom" scroll-y>
<sticky-section>
<sticky-header>分类 A</sticky-header>
<list-view>
<view wx:for="{{categoryA}}" wx:key="id">{{item.name}}</view>
</list-view>
</sticky-section>
<sticky-section>
<sticky-header>分类 B</sticky-header>
<list-view>
<view wx:for="{{categoryB}}" wx:key="id">{{item.name}}</view>
</list-view>
</sticky-section>
</scroll-view>grid-view 网格/瀑布流布局
基本用法
<scroll-view type="custom" scroll-y>
<grid-view type="aligned" cross-axis-count="2">
<view wx:for="{{list}}" wx:key="id" class="grid-item">
{{item.name}}
</view>
</grid-view>
</scroll-view>属性说明
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| type | string | - | 布局类型:aligned(网格)/masonry(瀑布流) |
| cross-axis-count | number | 2 | 交叉轴元素数量(列数) |
| main-axis-gap | number | 0 | 主轴方向间距 |
| cross-axis-gap | number | 0 | 交叉轴方向间距 |
| padding | Array | [0,0,0,0] | 内边距 |
| max-cross-axis-extent | number | - | 交叉轴元素最大范围 |
type 属性
| 值 | 说明 |
|---|---|
| aligned | 网格布局,所有项等高 |
| masonry | 瀑布流布局,项高度可不同 |
网格布局示例
<scroll-view type="custom" scroll-y>
<grid-view
type="aligned"
cross-axis-count="3"
cross-axis-gap="8"
main-axis-gap="8"
>
<view
wx:for="{{products}}"
wx:key="id"
class="product-card"
>
<image src="{{item.cover}}" mode="aspectFill" />
<text>{{item.name}}</text>
<text class="price">¥{{item.price}}</text>
</view>
</grid-view>
</scroll-view>瀑布流布局示例
<scroll-view type="custom" scroll-y>
<grid-view
type="masonry"
cross-axis-count="2"
cross-axis-gap="12"
main-axis-gap="12"
>
<view
wx:for="{{images}}"
wx:key="id"
class="masonry-item"
>
<!-- 瀑布流中每项高度可以不同 -->
<image
src="{{item.url}}"
mode="widthFix"
style="width: 100%;"
/>
<text>{{item.title}}</text>
</view>
</grid-view>
</scroll-view>list-builder 列表构造器
列表构造器用于实现可回收的虚拟列表,适合超长列表场景。
基本用法
<scroll-view type="custom" scroll-y>
<list-builder
list="{{list}}"
child-count="{{list.length}}"
child-height="200"
bind:itembuild="onItemBuild"
bind:itemdispose="onItemDispose"
>
<view slot:item slot:index style="height: 200px;">
<view>{{index}}</view>
</view>
</list-builder>
</scroll-view>属性说明
| 属性 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
| type | string | static | 列表模式:static(定高)/ dynamic(不定高) | - |
| list | Array | - | 数据列表 | - |
| child-count | number | - | 列表项数量 | - |
| child-height | number | - | 列表项高度(定高模式必填) | - |
| padding | Array | [0,0,0,0] | 内边距 [top, right, bottom, left] | - |
| initial-child-count | number | 0 | 首次渲染列表项数量,可减少初始白屏时间 | 3.7.12 |
| binditembuild | eventhandle | - | 列表项创建时触发,event.detail = { index } | - |
| binditemdispose | eventhandle | - | 列表项回收时触发,event.detail = { index } | - |
Bug & Tip:
- 目前只支持纵向滚动列表
事件说明
| 事件 | 说明 | detail |
|---|---|---|
| bind:itembuild | 列表项被创建 | { index } |
| bind:itemdispose | 列表项被回收 | { index } |
完整示例
<scroll-view type="custom" scroll-y style="height: 100vh;">
<list-builder
list="{{list}}"
child-count="{{list.length}}"
child-height="100"
bind:itembuild="onItemBuild"
bind:itemdispose="onItemDispose"
>
<view slot:item slot:index class="list-item">
<image class="avatar" src="{{item.avatar}}" />
<view class="info">
<text class="name">{{item.name}}</text>
<text class="desc">{{item.desc}}</text>
</view>
</view>
</list-builder>
</scroll-view>Page({
data: {
list: Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `用户 ${i}`,
desc: `这是第 ${i} 个用户的描述`,
avatar: `https://example.com/avatar/${i}.jpg`
}))
},
onItemBuild(e) {
console.log('创建列表项:', e.detail.index)
},
onItemDispose(e) {
console.log('回收列表项:', e.detail.index)
}
})注意事项
1. scroll-view 必须设置 type="custom" 2. 列表项默认为定高模式,需通过 child-height 指定 3. 不定高模式会存在滚动条跳动问题 4. 只支持纵向滚动 5. 不支持 scroll-into-view
grid-builder 网格构造器
网格构造器与列表构造器类似,用于可回收的网格布局。
基本用法
<scroll-view type="custom" scroll-y>
<grid-builder
list="{{list}}"
child-count="{{list.length}}"
cross-axis-count="4"
cross-axis-gap="8"
main-axis-gap="8"
>
<view slot:item slot:index class="grid-item">
<view>{{index}}</view>
</view>
</grid-builder>
</scroll-view>属性说明
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| type | string | aligned | 网格模式:aligned(对齐)/ masonry(瀑布流) |
| list | Array | - | 数据列表 |
| child-count | number | - | 网格项数量 |
| cross-axis-count | number | - | 交叉轴元素数量(列数) |
| cross-axis-gap | number | 0 | 交叉轴间距 |
| main-axis-gap | number | 0 | 主轴间距 |
| max-cross-axis-extent | number | 0 | 交叉轴元素最大范围 |
| padding | Array | [0,0,0,0] | 内边距 [top, right, bottom, left] |
| binditembuild | eventhandle | - | 列表项创建时触发,event.detail = { index } |
| binditemdispose | eventhandle | - | 列表项回收时触发,event.detail = { index } |
Bug & Tip:
- 目前只支持纵向滚动列表
- ⚠️ Bug:grid-builder 进入屏幕后不允许再滚出屏幕,否则会被判定成列表需要重新布局进而自动滚动到顶端
使用规则
MUST(必须遵守)
1. list-view/grid-view 必须在 type="custom" 的 scroll-view 下使用 2. 使用 list-builder 时必须指定 child-height(定高模式) 3. 网格布局必须指定 cross-axis-count
NEVER(禁止行为)
1. 不要在 type="list" 的 scroll-view 中使用 list-view/grid-view 2. 不要在 list-builder/grid-builder 外直接放置列表项
性能对比
| 方案 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| scroll-view type="list" | 普通列表 | 简单易用 | 大数据量性能一般 |
| list-view | 分组列表 | 配合吸顶 | 需要 custom 模式 |
| list-builder | 超长列表(1000+) | 真正虚拟列表 | 配置复杂,定高限制 |
| grid-view | 网格/瀑布流 | 布局灵活 | 需要 custom 模式 |
| grid-builder | 超长网格 | 虚拟网格 | 配置复杂 |
示例代码片段
列表构造器:{% minicode('wiAQEwmN7VRz') %} 网格构造器:{% minicode('GSAP4wms7LRe') %}
嵌套滚动模式
概述
在 Skyline 渲染模式下,当存在两个 scroll-view 相互嵌套的场景时(如 Tab 列表页),两者的滚动不能很丝滑地衔接。使用嵌套模式(type="nested")可以让父子 scroll-view 的滚动无缝联动。
基本结构
<!-- 外层 scroll-view -->
<scroll-view type="nested" scroll-y>
<nested-scroll-header>
<view>头部区域(会被滚动走)</view>
</nested-scroll-header>
<nested-scroll-body>
<!-- 内层可滚动区域 -->
</nested-scroll-body>
</scroll-view>组件说明
nested-scroll-header
头部区域组件,会随着外层 scroll-view 滚动而滚走。
约束:
- 只能渲染在
nested-scroll-body上面 - ⚠️ 不支持复数子节点,只渲染第一个子节点(其他子节点不会渲染)
- 可以有多个
nested-scroll-header,每个包裹一个头部元素
nested-scroll-body
主体区域组件,包含内层可滚动内容。
约束:
- 外层 scroll-view 只能有一个
nested-scroll-body - ⚠️ 不支持复数子节点,只渲染第一个子节点
属性:
| 属性 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
| offset-top | number | 0 | 滚动目标距离顶部的距离(px)。外层滚动时此组件会逐渐撑开,直到顶部与视窗顶部距离为该值时才开始里层滚动 | 3.6.2 |
典型应用:Tab + 列表
<scroll-view type="nested" scroll-y style="height: 100vh;">
<!-- 头部:Banner + Tab -->
<nested-scroll-header>
<view class="banner">
<image src="banner.jpg" />
</view>
</nested-scroll-header>
<nested-scroll-header>
<view class="tabs">
<view
wx:for="{{tabs}}"
wx:key="id"
class="tab {{currentTab === index ? 'active' : ''}}"
bindtap="switchTab"
data-index="{{index}}"
>
{{item.name}}
</view>
</view>
</nested-scroll-header>
<!-- 主体:Tab 内容 -->
<nested-scroll-body>
<swiper
current="{{currentTab}}"
bindchange="onSwiperChange"
style="height: 100%;"
>
<swiper-item wx:for="{{tabs}}" wx:key="id">
<!-- 内层列表必须关联外层嵌套滚动 -->
<scroll-view
type="list"
scroll-y
associative-container="nested-scroll-view"
style="height: 100%;"
>
<view wx:for="{{item.list}}" wx:key="id" wx:for-item="listItem">
{{listItem.name}}
</view>
</scroll-view>
</swiper-item>
</swiper>
</nested-scroll-body>
</scroll-view>Page({
data: {
currentTab: 0,
tabs: [
{ id: 1, name: '推荐', list: [] },
{ id: 2, name: '热门', list: [] },
{ id: 3, name: '最新', list: [] }
]
},
switchTab(e) {
this.setData({ currentTab: e.currentTarget.dataset.index })
},
onSwiperChange(e) {
this.setData({ currentTab: e.detail.current })
}
})滚动策略
嵌套滚动的联动策略:
| 滚动方向 | 策略 |
|---|---|
| 向下滚动 | 先滚动外层 scroll-view,再滚动内层 scroll-view |
| 向上滚动 | 先滚动内层 scroll-view,再滚动外层 scroll-view |
这种策略确保了:
- 下滑时先看到完整头部消失
- 上滑时先让列表滚到顶部,再显示头部
配合下拉刷新
<scroll-view
type="nested"
scroll-y
refresher-enabled="{{true}}"
refresher-triggered="{{refreshing}}"
bindrefresherrefresh="onRefresh"
>
<view slot="refresher">自定义 refresher</view>
<nested-scroll-header>
<view>头部</view>
</nested-scroll-header>
<nested-scroll-body>
<scroll-view
type="list"
scroll-y
associative-container="nested-scroll-view"
>
<!-- 列表内容 -->
</scroll-view>
</nested-scroll-body>
</scroll-view>注意:下拉刷新的 slot="refresher" 与 nested-scroll-header、nested-scroll-body 是平级的。
使用规则
MUST(必须遵守)
1. 外层 scroll-view 必须设置 type="nested" 2. 内层 scroll-view 必须设置 associative-container="nested-scroll-view" 3. nested-scroll-header 和 nested-scroll-body 只能有一个子节点 4. 外层只能有一个 nested-scroll-body
NEVER(禁止行为)
1. 不要在 nested-scroll-body 下面放 nested-scroll-header 2. 不要有多个 nested-scroll-body 3. 不要忘记给内层 scroll-view 设置 associative-container
完整示例
<!-- index.wxml -->
<scroll-view
class="container"
type="nested"
scroll-y
refresher-enabled="{{true}}"
refresher-triggered="{{refreshing}}"
bindrefresherrefresh="onRefresh"
>
<view slot="refresher" class="refresher">
<view class="loading">{{refreshing ? '刷新中...' : '下拉刷新'}}</view>
</view>
<nested-scroll-header>
<view class="header">
<image class="banner" src="/images/banner.jpg" mode="aspectFill" />
</view>
</nested-scroll-header>
<nested-scroll-header>
<view class="tab-bar">
<view
wx:for="{{tabs}}"
wx:key="id"
class="tab-item {{currentTab === index ? 'active' : ''}}"
bindtap="onTabTap"
data-index="{{index}}"
>
{{item.title}}
</view>
</view>
</nested-scroll-header>
<nested-scroll-body>
<swiper
class="tab-content"
current="{{currentTab}}"
bindchange="onSwiperChange"
>
<swiper-item wx:for="{{tabs}}" wx:key="id">
<scroll-view
class="list-scroll"
type="list"
scroll-y
associative-container="nested-scroll-view"
bindscrolltolower="onLoadMore"
data-tab="{{index}}"
>
<view
wx:for="{{item.list}}"
wx:key="id"
wx:for-item="card"
class="card"
>
<image class="card-cover" src="{{card.cover}}" />
<text class="card-title">{{card.title}}</text>
</view>
<view wx:if="{{item.loading}}" class="loading-more">
加载中...
</view>
</scroll-view>
</swiper-item>
</swiper>
</nested-scroll-body>
</scroll-view>/* index.wxss */
.container {
height: 100vh;
}
.banner {
width: 100%;
height: 200px;
}
.tab-bar {
display: flex;
background: #fff;
position: sticky;
top: 0;
}
.tab-item {
flex: 1;
text-align: center;
padding: 12px 0;
font-size: 14px;
}
.tab-item.active {
color: #07c160;
border-bottom: 2px solid #07c160;
}
.tab-content {
height: calc(100vh - 44px); /* 减去 tab-bar 高度 */
}
.list-scroll {
height: 100%;
}
.card {
padding: 12px;
border-bottom: 1px solid #eee;
}// index.js
Page({
data: {
refreshing: false,
currentTab: 0,
tabs: [
{ id: 1, title: '推荐', list: [], loading: false },
{ id: 2, title: '关注', list: [], loading: false },
{ id: 3, title: '热榜', list: [], loading: false }
]
},
onLoad() {
this.loadData(0)
},
onRefresh() {
this.setData({ refreshing: true })
this.loadData(this.data.currentTab).then(() => {
this.setData({ refreshing: false })
})
},
onTabTap(e) {
const index = e.currentTarget.dataset.index
this.setData({ currentTab: index })
if (!this.data.tabs[index].list.length) {
this.loadData(index)
}
},
onSwiperChange(e) {
const index = e.detail.current
this.setData({ currentTab: index })
if (!this.data.tabs[index].list.length) {
this.loadData(index)
}
},
onLoadMore(e) {
const tabIndex = e.currentTarget.dataset.tab
this.loadMore(tabIndex)
},
async loadData(tabIndex) {
// 模拟加载数据
const list = await this.fetchList(tabIndex, 0)
this.setData({
[`tabs[${tabIndex}].list`]: list
})
},
async loadMore(tabIndex) {
const tab = this.data.tabs[tabIndex]
if (tab.loading) return
this.setData({ [`tabs[${tabIndex}].loading`]: true })
const newList = await this.fetchList(tabIndex, tab.list.length)
this.setData({
[`tabs[${tabIndex}].list`]: [...tab.list, ...newList],
[`tabs[${tabIndex}].loading`]: false
})
},
fetchList(tabIndex, offset) {
return new Promise(resolve => {
setTimeout(() => {
const list = Array.from({ length: 10 }, (_, i) => ({
id: `${tabIndex}-${offset + i}`,
title: `Tab ${tabIndex + 1} - Item ${offset + i + 1}`,
cover: 'https://example.com/image.jpg'
}))
resolve(list)
}, 1000)
})
}
})示例代码片段
{% minicode('1IaEOym777Mx') %}
scroll-view 组件详解
概述
scroll-view 是 Skyline 中最核心的滚动容器组件。相比 WebView,Skyline 的 scroll-view 增加了多种渲染模式,支持按需渲染、嵌套滚动、下拉二级等高级功能。
必要配置
<!-- Skyline 必须指定 type 属性 -->
<scroll-view type="list" scroll-y style="height: 100vh;">
<!-- 内容 -->
</scroll-view>⚠️ MUST: Skyline 下使用 scroll-view 必须设置 type 属性,否则性能会退化。type 属性
| 值 | 说明 | 适用场景 |
|---|---|---|
| list | 列表模式,根据直接子节点是否在屏按需渲染 | 普通长列表 |
| custom | 自定义模式,支持 list-view/grid-view/sticky-section | 复杂布局 |
| nested | 嵌套模式,处理父子 scroll-view 滚动联动 | Tab + 列表 |
list 模式
<scroll-view type="list" scroll-y>
<!-- 直接子节点会按需渲染 -->
<view wx:for="{{items}}" wx:key="id">{{item.name}}</view>
</scroll-view>注意事项:
- 只会渲染在屏节点
- 若只有一个直接子节点,性能会退化
- 列表项必须是 scroll-view 的直接子节点
custom 模式
<scroll-view type="custom" scroll-y>
<sticky-section>
<sticky-header>分类标题</sticky-header>
<list-view>
<!-- 列表内容 -->
</list-view>
</sticky-section>
</scroll-view>支持的子组件:
sticky-section/sticky-headerlist-view/grid-viewlist-builder/grid-builder
nested 模式
<scroll-view type="nested" scroll-y>
<nested-scroll-header>
<view>头部区域(会被滚动走)</view>
</nested-scroll-header>
<nested-scroll-body>
<scroll-view type="list" associative-container="nested-scroll-view">
<!-- 内层可滚动内容 -->
</scroll-view>
</nested-scroll-body>
</scroll-view>通用属性
滚动控制
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| scroll-x | boolean | false | 允许横向滚动 |
| scroll-y | boolean | false | 允许纵向滚动 |
| scroll-top | number | - | 设置竖向滚动位置 |
| scroll-left | number | - | 设置横向滚动位置 |
| scroll-into-view | string | - | 滚动到指定 id 元素 |
| scroll-into-view-offset | number | 0 | scroll-into-view 额外偏移 |
| scroll-with-animation | boolean | false | 滚动时使用动画 |
| upper-threshold | number | 50 | 触发 scrolltoupper 的距离 |
| lower-threshold | number | 50 | 触发 scrolltolower 的距离 |
滚动事件
| 事件 | 说明 | detail |
|---|---|---|
| bindscrolltoupper | 滚动到顶部/左边 | - |
| bindscrolltolower | 滚动到底部/右边 | - |
| bindscroll | 滚动时触发 | scrollLeft, scrollTop, scrollHeight, scrollWidth, deltaX, deltaY |
Skyline 特有属性
渲染优化
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| cache-extent | number | - | 视口外预渲染距离(px) |
| reverse | boolean | false | 反向滚动(初始在底部) |
| clip | boolean | true | 是否裁剪溢出内容 |
| min-drag-distance | number | 18 | 触发滚动的最小拖动距离 |
| padding | Array | [0,0,0,0] | 内边距 [top, right, bottom, left] |
scroll-into-view 增强
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| scroll-into-view-alignment | string | start | 目标节点位置:start/center/end/nearest |
| scroll-into-view-within-extent | boolean | false | 仅滚动到 cache-extent 内的节点 |
Worklet 回调
| 属性 | 说明 |
|---|---|
| worklet:onscrollstart | 滚动开始,仅支持 worklet 回调 |
| worklet:onscrollupdate | 滚动中,仅支持 worklet 回调 |
| worklet:onscrollend | 滚动结束,仅支持 worklet 回调 |
| worklet:adjust-deceleration-velocity | 调整惯性滚动初速度 |
Page({
onScrollUpdate(e) {
'worklet'
// 在 UI 线程执行
const { scrollTop, scrollLeft, isDrag } = e.detail
}
})通过手势监听屏蔽用户滚动
scroll-view 也可以通过手势监听屏蔽用户手势,最常见的是屏蔽用户滚动输入,但前提是使用 Skyline 手势系统在组件外层拦截输入;这不是 scroll-view 默认自动禁用滚动的行为。
对于原生滚动组件,需要在外层手势组件上声明 native-view="scroll-view" 来代理内部手势。纵向滚动通常配合 vertical-drag-gesture-handler,横向滚动则配合对应方向的手势组件。
<vertical-drag-gesture-handler
native-view="scroll-view"
worklet:should-accept-gesture="shouldScrollRespond"
>
<scroll-view type="list" scroll-y>
<!-- 内容 -->
</scroll-view>
</vertical-drag-gesture-handler>Page({
shouldScrollRespond() {
'worklet'
return false
}
})当回调返回 false 时,用户拖拽不会交给内部 scroll-view 处理。若需要在滚动到边界、编辑态或弹层打开时再切换行为,也可以改用 worklet:should-response-on-move 做动态拦截。
屏蔽滚动的三种常见方式
| 方式 | 适用场景 | 特点 |
|---|---|---|
| 手势监听拦截 | 需要按状态、按时机或按边界动态决定是否响应用户滑动 | 本质是手势处理器拦截输入,适合复杂交互和嵌套协商 |
scroll-x / scroll-y | 直接关闭某个方向的滚动 | 是方向开关,不是手势协商能力,适合简单禁用 |
| 直接关闭滚动能力 | 需要程序化地整体禁用滚动 | 可通过 ScrollViewContext.scrollEnabled = false 等方式关闭滚动,与手势拦截语义不同 |
如何选择:
- 只想简单禁用某个方向滚动时,优先使用
scroll-x或scroll-y - 需要在一次拖动过程中动态切换是否继续滚动时,使用
worklet:should-accept-gesture或worklet:should-response-on-move - 需要通过逻辑统一开启/关闭整个滚动能力时,使用
ScrollViewContext.scrollEnabled = false这类程序化方式
关联容器
| 属性 | 类型 | 说明 |
|---|---|---|
| associative-container | string | 关联的滚动容器 |
合法值:
draggable-sheet: 关联半屏拖拽组件nested-scroll-view: 关联嵌套滚动父容器pop-gesture: 关联页面手势返回
下拉刷新
基础配置
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| refresher-enabled | boolean | false | 开启下拉刷新 |
| refresher-threshold | number | 45 | 刷新阈值 |
| refresher-default-style | string | "black" | 默认样式:black/white/none |
| refresher-background | string | - | 刷新区域背景色 |
| refresher-triggered | boolean | false | 当前刷新状态 |
刷新事件
| 事件 | 说明 |
|---|---|
| bindrefresherpulling | 下拉中 |
| bindrefresherrefresh | 触发刷新 |
| bindrefresherrestore | 刷新复位 |
| bindrefresherabort | 刷新中止 |
| bind:refresherwillrefresh | 即将触发刷新(Skyline) |
| bind:refresherstatuschange | 刷新状态变化(Skyline) |
自定义刷新区域
<scroll-view
type="list"
scroll-y
refresher-enabled="{{true}}"
refresher-default-style="none"
refresher-triggered="{{refreshing}}"
bindrefresherrefresh="onRefresh"
bind:refresherstatuschange="onStatusChange"
>
<view slot="refresher" class="custom-refresher">
<view wx:if="{{status === 0}}">下拉刷新</view>
<view wx:elif="{{status === 1}}">松手刷新</view>
<view wx:elif="{{status === 2}}">刷新中...</view>
<view wx:elif="{{status === 3}}">刷新完成</view>
</view>
<!-- 列表内容 -->
</scroll-view>Page({
data: {
refreshing: false,
status: 0 // RefreshStatus 枚举值
},
onStatusChange(e) {
this.setData({ status: e.detail.status })
},
onRefresh() {
this.setData({ refreshing: true })
// 加载数据
setTimeout(() => {
this.setData({ refreshing: false })
}, 2000)
}
})RefreshStatus 枚举
const RefreshStatus = {
Idle: 0, // 空闲
CanRefresh: 1, // 可刷新(超过阈值)
Refreshing: 2, // 刷新中
Completed: 3, // 刷新完成
Failed: 4, // 刷新失败
CanTwoLevel: 5, // 可进入二级
TwoLevelOpening: 6,// 二级打开中
TwoLeveling: 7, // 二级已打开
TwoLevelClosing: 8 // 二级关闭中
}下拉二级
下拉二级是下拉刷新的扩展,继续下拉可进入"二楼"页面。
配置
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| refresher-two-level-enabled | boolean | false | 开启下拉二级 |
| refresher-two-level-triggered | boolean | false | 设置打开/关闭二级 |
| refresher-two-level-threshold | number | 150 | 二级阈值 |
| refresher-two-level-close-threshold | number | 80 | 关闭二级阈值 |
| refresher-two-level-scroll-enabled | boolean | false | 二级状态可滑动 |
| refresher-two-level-pinned | boolean | false | 即将打开二级时是否定住 |
| refresher-ballistic-refresh-enabled | boolean | false | 惯性滚动触发刷新 |
示例
<scroll-view
type="list"
scroll-y
refresher-enabled="{{true}}"
refresher-two-level-enabled="{{true}}"
refresher-two-level-scroll-enabled="{{true}}"
refresher-two-level-threshold="{{150}}"
bind:refresherstatuschange="onStatusChange"
>
<view slot="refresher" class="refresher-container">
<!-- 根据 status 显示不同内容 -->
<view wx:if="{{status < 5}}">下拉刷新区域</view>
<view wx:else class="two-level-content">
二级页面内容
</view>
</view>
<!-- 列表内容 -->
</scroll-view>相关 API
ScrollViewContext.triggerRefresh()- 触发下拉刷新ScrollViewContext.closeRefresh()- 关闭下拉刷新ScrollViewContext.triggerTwoLevel()- 触发下拉二级ScrollViewContext.closeTwoLevel()- 关闭下拉二级
横向滚动
<!-- 横向滚动需同时开启 enable-flex -->
<scroll-view
scroll-x
enable-flex
style="flex-direction: row; white-space: nowrap;"
>
<view style="display: inline-block; width: 200px;">Item 1</view>
<view style="display: inline-block; width: 200px;">Item 2</view>
</scroll-view>⚠️ MUST: 横向滚动需开启 enable-flex 以兼容 WebView。性能优化建议
1. 指定 cache-extent
<!-- 预渲染视口外 500px 区域 -->
<scroll-view type="list" cache-extent="500">适当的 cache-extent 可优化滚动体验,但会增加内存占用。
2. 使用 list-item 属性
<scroll-view type="list" scroll-y>
<view wx:for="{{list}}" wx:key="id" list-item>
<!-- 列表项内容 -->
</view>
</scroll-view>list-item 属性可启用样式共享优化。
3. 避免单一子节点
list 模式下若只有一个直接子节点,按需渲染会退化:
<!-- ❌ 性能退化 -->
<scroll-view type="list">
<view>
<view wx:for="{{list}}">{{item}}</view>
</view>
</scroll-view>
<!-- ✅ 正确做法 -->
<scroll-view type="list">
<view wx:for="{{list}}">{{item}}</view>
</scroll-view>Bug & Tip
1. 不支持嵌套 textarea/map/canvas/video(基础库 2.4.0 以下) 2. scroll-into-view 优先级高于 scroll-top 3. 滚动 scroll-view 会阻止页面回弹,无法触发 onPullDownRefresh 4. 自定义下拉刷新节点需声明 slot="refresher" 5. 滚动条长度是预估的,子节点高度差异大时可能不准确
吸顶布局:sticky-section 与 sticky-header
概述
sticky-section 和 sticky-header 是 Skyline 新增的吸顶布局组件,用于实现分组列表的吸顶效果。它们必须在 <scroll-view type="custom"> 模式下使用。
组件说明
sticky-section
吸顶区段容器,包含一个 sticky-header 和列表内容。
sticky-header
吸顶头部组件,当滚动到该区段时会固定在顶部。
基本结构
<scroll-view type="custom" scroll-y>
<sticky-section>
<sticky-header>区段 1 标题</sticky-header>
<view>区段 1 内容</view>
</sticky-section>
<sticky-section>
<sticky-header>区段 2 标题</sticky-header>
<view>区段 2 内容</view>
</sticky-section>
</scroll-view>典型应用:通讯录
<scroll-view type="custom" scroll-y style="height: 100vh;">
<sticky-section wx:for="{{contacts}}" wx:key="letter">
<sticky-header class="letter-header">
{{item.letter}}
</sticky-header>
<list-view>
<view
wx:for="{{item.list}}"
wx:for-item="contact"
wx:key="id"
class="contact-item"
>
<image class="avatar" src="{{contact.avatar}}" />
<text>{{contact.name}}</text>
</view>
</list-view>
</sticky-section>
</scroll-view>Page({
data: {
contacts: [
{
letter: 'A',
list: [
{ id: 1, name: 'Alice', avatar: '...' },
{ id: 2, name: 'Amy', avatar: '...' }
]
},
{
letter: 'B',
list: [
{ id: 3, name: 'Bob', avatar: '...' },
{ id: 4, name: 'Bill', avatar: '...' }
]
}
// ...
]
}
}).letter-header {
height: 32px;
line-height: 32px;
padding: 0 16px;
background: #f5f5f5;
font-size: 14px;
color: #999;
font-weight: bold;
}
.contact-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.avatar {
width: 44px;
height: 44px;
border-radius: 4px;
margin-right: 12px;
}商品分类列表
<scroll-view type="custom" scroll-y>
<sticky-section wx:for="{{categories}}" wx:key="id">
<sticky-header class="category-header">
<image class="category-icon" src="{{item.icon}}" />
<text>{{item.name}}</text>
</sticky-header>
<grid-view type="aligned" cross-axis-count="3" cross-axis-gap="8" main-axis-gap="8">
<view
wx:for="{{item.products}}"
wx:for-item="product"
wx:key="id"
class="product-card"
>
<image src="{{product.cover}}" mode="aspectFill" />
<text class="name">{{product.name}}</text>
<text class="price">¥{{product.price}}</text>
</view>
</grid-view>
</sticky-section>
</scroll-view>属性说明
sticky-section
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| push-pinned-header | boolean | true | 是否推动之前吸顶的头部 |
sticky-header
| 属性 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
| padding | Array | [0,0,0,0] | 内边距 | - |
| offset-top | number | 0 | 吸顶时与视窗顶部的距离(px) | 3.0.0 |
| allow-overlapping | boolean | false | 是否允许与前一个 sticky-header 重叠 | 3.7.11 |
| bind:stickontopchange | eventhandle | - | 吸顶状态变化事件,event.detail = { isStickOnTop } | 3.6.2 |
使用规则
MUST(必须遵守)
1. 必须在 type="custom" 的 scroll-view 中使用 2. sticky-header 必须是 sticky-section 的第一个子元素 3. 每个 sticky-section 只能有一个 sticky-header
NEVER(禁止行为)
1. 不要在 type="list" 的 scroll-view 中使用 2. 不要将 sticky-header 放在 sticky-section 之外
配合 list-view 使用
<scroll-view type="custom" scroll-y>
<sticky-section>
<sticky-header>今日推荐</sticky-header>
<list-view padding="{{[12, 16, 12, 16]}}">
<view wx:for="{{todayList}}" wx:key="id" class="item">
{{item.title}}
</view>
</list-view>
</sticky-section>
<sticky-section>
<sticky-header>历史记录</sticky-header>
<list-view padding="{{[12, 16, 12, 16]}}">
<view wx:for="{{historyList}}" wx:key="id" class="item">
{{item.title}}
</view>
</list-view>
</sticky-section>
</scroll-view>与 WebView position: sticky 的区别
| 特性 | WebView sticky | Skyline sticky-header |
|---|---|---|
| 实现方式 | CSS 属性 | 组件 |
| 推动效果 | 需要 JS 处理 | 内置支持 |
| 性能 | 可能有闪烁 | 原生流畅 |
| 使用场景 | 任意元素 | 仅限 custom 模式 |
样式建议
吸顶头部样式
sticky-header {
/* 背景必须设置,否则会透出下层内容 */
background: #fff;
/* 可添加阴影增强层次感 */
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
/* 设置较高 z-index 确保在上层 */
z-index: 10;
}区段间距
sticky-section {
margin-bottom: 12px;
}
sticky-section:last-child {
margin-bottom: 0;
}完整示例:外卖菜单
<view class="container">
<!-- 左侧分类导航 -->
<scroll-view class="category-nav" scroll-y>
<view
wx:for="{{categories}}"
wx:key="id"
class="nav-item {{currentCategory === index ? 'active' : ''}}"
bindtap="scrollToCategory"
data-index="{{index}}"
>
{{item.name}}
</view>
</scroll-view>
<!-- 右侧商品列表 -->
<scroll-view
class="product-list"
type="custom"
scroll-y
scroll-into-view="{{scrollToId}}"
bindscroll="onScroll"
>
<sticky-section
wx:for="{{categories}}"
wx:key="id"
id="category-{{index}}"
>
<sticky-header class="category-title">
{{item.name}}
</sticky-header>
<list-view>
<view
wx:for="{{item.products}}"
wx:for-item="product"
wx:key="id"
class="product-item"
>
<image class="product-image" src="{{product.image}}" />
<view class="product-info">
<text class="product-name">{{product.name}}</text>
<text class="product-price">¥{{product.price}}</text>
</view>
</view>
</list-view>
</sticky-section>
</scroll-view>
</view>.container {
display: flex;
height: 100vh;
}
.category-nav {
width: 80px;
background: #f5f5f5;
}
.nav-item {
padding: 12px 8px;
text-align: center;
font-size: 12px;
}
.nav-item.active {
background: #fff;
color: #07c160;
}
.product-list {
flex: 1;
}
.category-title {
padding: 8px 12px;
background: #f9f9f9;
font-size: 14px;
font-weight: bold;
}
.product-item {
display: flex;
padding: 12px;
border-bottom: 1px solid #eee;
}
.product-image {
width: 80px;
height: 80px;
border-radius: 4px;
margin-right: 12px;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 14px;
color: #333;
}
.product-price {
font-size: 16px;
color: #f5222d;
font-weight: bold;
}share-element 共享元素动画
概述
share-element 是用于实现页面间共享元素动画的组件,类似于 Flutter 的 Hero 动画。元素看起来像是在页面间"穿越",可以创造流畅的过渡体验。
使用场景
- 图片详情页转场
- 商品详情页转场
- 卡片展开效果
- 任意需要元素连续性动画的场景
基本原理
1. 在源页面放置 share-element,设置唯一的 key 2. 在目标页面放置相同 key 的 share-element 3. 页面跳转时,元素会从源位置动画过渡到目标位置
基本用法
源页面
<!-- pages/list/index.wxml -->
<view class="card" bindtap="goDetail" data-id="{{item.id}}">
<share-element key="image-{{item.id}}" transform>
<image src="{{item.cover}}" mode="aspectFill" />
</share-element>
<text>{{item.title}}</text>
</view>// pages/list/index.js
Page({
goDetail(e) {
const { id } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/detail/index?id=${id}`
})
}
})目标页面
<!-- pages/detail/index.wxml -->
<share-element key="image-{{detail.id}}" transform>
<image class="detail-image" src="{{detail.cover}}" mode="aspectFill" />
</share-element>
<view class="content">
<text class="title">{{detail.title}}</text>
<text class="desc">{{detail.desc}}</text>
</view>通用属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| key | string | - | 必填,页面内唯一标识 |
| transform | boolean | false | 是否启用动画 |
| duration | number | 300 | 动画时长(ms) |
| easing-function | string | ease-out | CSS 缓动函数 |
Skyline 特有属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| transition-on-gesture | boolean | false | 手势返回时是否动画 |
| shuttle-on-push | string | to | push 阶段飞跃物 |
| shuttle-on-pop | string | to | pop 阶段飞跃物 |
| rect-tween-type | string | materialRectArc | 动画轨迹类型 |
| worklet:onframe | callback | - | 动画帧回调 |
shuttle-on-push / shuttle-on-pop
| 值 | 说明 |
|---|---|
| from | 使用源页面节点作为飞跃物 |
| to | 使用目标页面节点作为飞跃物 |
rect-tween-type 动画轨迹
| 值 | 效果 |
|---|---|
| materialRectArc | 矩形对角动画(默认) |
| materialRectCenterArc | 径向动画 |
| linear | 线性动画 |
| elasticIn | 弹性进入 |
| elasticOut | 弹性退出 |
| elasticInOut | 弹性进出 |
| bounceIn | 弹跳进入 |
| bounceOut | 弹跳退出 |
| bounceInOut | 弹跳进出 |
| cubic-bezier(x1,y1,x2,y2) | 自定义贝塞尔曲线 |
手势返回动画
开启 transition-on-gesture 后,用户侧滑返回时也会触发共享元素动画:
<share-element
key="hero-image"
transform
transition-on-gesture
>
<image src="{{imageUrl}}" />
</share-element>Worklet 动画帧回调
使用 worklet:onframe 可以在动画过程中同步其他动画:
<share-element
key="card"
transform
worklet:onframe="onShareElementFrame"
>
<view class="card">...</view>
</share-element>
<view class="backdrop" style="opacity: {{backdropOpacity}};"></view>Page({
data: {
backdropOpacity: 0
},
onShareElementFrame(e) {
'worklet'
// e.progress: 动画进度 0-1
// 可用于同步其他元素的动画
}
})配合 page-container 使用
在同一页面内使用 page-container 实现模态转场:
<view class="page">
<view
wx:for="{{items}}"
wx:key="id"
class="card"
bindtap="showDetail"
data-index="{{index}}"
>
<share-element key="item-{{item.id}}" transform="{{currentId === item.id}}">
<image src="{{item.cover}}" />
</share-element>
<text>{{item.title}}</text>
</view>
</view>
<page-container
show="{{showContainer}}"
overlay
duration="{{300}}"
bindbeforeenter="onBeforeEnter"
bindafterleave="onAfterLeave"
>
<view class="detail">
<share-element key="item-{{detail.id}}" transform>
<image class="detail-image" src="{{detail.cover}}" />
</share-element>
<view class="detail-content">
<text class="title">{{detail.title}}</text>
<text class="desc">{{detail.desc}}</text>
</view>
</view>
</page-container>Page({
data: {
items: [],
detail: null,
currentId: null,
showContainer: false
},
showDetail(e) {
const { index } = e.currentTarget.dataset
const item = this.data.items[index]
this.setData({
detail: item,
currentId: item.id,
showContainer: true
})
},
onBeforeEnter() {
// 容器进入前
},
onAfterLeave() {
this.setData({ currentId: null })
}
})完整示例:图片详情
列表页
<!-- pages/gallery/index.wxml -->
<view class="gallery">
<view
wx:for="{{images}}"
wx:key="id"
class="gallery-item"
bindtap="goDetail"
data-item="{{item}}"
>
<share-element key="gallery-{{item.id}}" transform>
<image
class="thumb"
src="{{item.thumb}}"
mode="aspectFill"
/>
</share-element>
</view>
</view>// pages/gallery/index.js
Page({
data: {
images: [
{ id: 1, thumb: '...', full: '...', title: '图片1' },
{ id: 2, thumb: '...', full: '...', title: '图片2' },
// ...
]
},
goDetail(e) {
const { item } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/gallery/detail?id=${item.id}`
})
}
})详情页
<!-- pages/gallery/detail.wxml -->
<view class="detail-page">
<share-element
key="gallery-{{image.id}}"
transform
transition-on-gesture
rect-tween-type="materialRectCenterArc"
>
<image
class="full-image"
src="{{image.full}}"
mode="widthFix"
bindtap="toggleInfo"
/>
</share-element>
<view class="info-panel {{showInfo ? 'show' : ''}}">
<text class="title">{{image.title}}</text>
<text class="desc">{{image.desc}}</text>
</view>
</view>/* pages/gallery/detail.wxss */
.detail-page {
position: relative;
min-height: 100vh;
background: #000;
}
.full-image {
width: 100%;
}
.info-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
background: rgba(0, 0, 0, 0.7);
transform: translateY(100%);
transition: transform 0.3s ease;
}
.info-panel.show {
transform: translateY(0);
}
.title {
display: block;
font-size: 18px;
color: #fff;
font-weight: bold;
}
.desc {
display: block;
margin-top: 8px;
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
}// pages/gallery/detail.js
Page({
data: {
image: null,
showInfo: false
},
onLoad(options) {
const { id } = options
// 获取图片详情
this.loadImage(id)
},
loadImage(id) {
// 模拟加载
const image = {
id: parseInt(id),
thumb: '...',
full: '...',
title: '风景照片',
desc: '拍摄于 2024 年春天'
}
this.setData({ image })
},
toggleInfo() {
this.setData({ showInfo: !this.data.showInfo })
}
})注意事项
1. key 必须匹配:源页面和目标页面的 share-element 必须有相同的 key 2. transform 属性:两边都需要设置 transform 才会触发动画 3. 内容变化:如果源和目标的内容不同,动画会在两者间过渡 4. 性能考虑:共享元素内容不宜过于复杂
Skyline vs WebView
| 特性 | Skyline | WebView |
|---|---|---|
| 基础动画 | ✅ | ✅ |
| 手势返回动画 | ✅ | ❌ |
| worklet 帧回调 | ✅ | ❌ |
| 飞跃物选择 | ✅ | ❌ |
| 动画轨迹类型 | 丰富 | 有限 |
示例代码片段
Skyline:{% minicode('t584gymu7VMM') %} WebView:{% minicode('NqVP7ImA73ou') %}
snapshot 截图组件
概述
snapshot 是 Skyline 新增的截图组件,可以将组件内的内容渲染为图片。适用于生成分享海报、保存页面截图等场景。
属性
| 属性 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
| mode | string | view | 渲染模式:view / picture | 3.1.0 |
渲染模式说明
- view 模式(默认):与普通 view 无差别,子节点样式变化会实时体现在界面上
- picture 模式:对子节点截图渲染为纹理,后续样式变化不会体现在界面上
使用场景:大范围节点进行 scale/rotate 动画时,动画开始设为 picture 模式,动画结束设为 view 模式,可显著提高动画性能。
基本用法
<snapshot id="my-snapshot">
<view class="poster">
<image src="{{productImage}}" />
<text>{{productName}}</text>
<text>¥{{productPrice}}</text>
</view>
</snapshot>
<button bindtap="takeSnapshot">生成图片</button>Page({
takeSnapshot() {
this.createSelectorQuery()
.select('#my-snapshot')
.node()
.exec(res => {
const snapshotNode = res[0].node
snapshotNode.takeSnapshot({
type: 'arraybuffer',
format: 'png',
success: (res) => {
// res.data 是图片的 ArrayBuffer
const fs = wx.getFileSystemManager()
const filePath = `${wx.env.USER_DATA_PATH}/snapshot.png`
fs.writeFile({
filePath,
data: res.data,
encoding: 'binary',
success: () => {
// 保存到相册
wx.saveImageToPhotosAlbum({
filePath,
success: () => {
wx.showToast({ title: '保存成功' })
}
})
}
})
},
fail: (err) => {
console.error('截图失败', err)
}
})
})
}
})takeSnapshot 方法
参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| type | string | file | 输出类型:file/arraybuffer |
| format | string | png | 图片格式:png/jpg |
| quality | number | 1.0 | jpg 质量(0-1) |
| success | function | - | 成功回调 |
| fail | function | - | 失败回调 |
返回值
| 属性 | 类型 | 说明 |
|---|---|---|
| data | string/ArrayBuffer | type=file 时为临时文件路径,type=arraybuffer 时为 ArrayBuffer |
| width | number | 图片宽度 |
| height | number | 图片高度 |
示例代码
生成分享海报
<view class="container">
<!-- 海报预览 -->
<snapshot id="poster" class="poster">
<view class="poster-content">
<!-- 商品图片 -->
<image class="product-image" src="{{product.image}}" mode="aspectFill" />
<!-- 商品信息 -->
<view class="product-info">
<text class="product-name">{{product.name}}</text>
<text class="product-desc" max-lines="2" overflow="ellipsis">
{{product.desc}}
</text>
<view class="price-row">
<text class="price">¥{{product.price}}</text>
<text class="original-price">¥{{product.originalPrice}}</text>
</view>
</view>
<!-- 底部信息 -->
<view class="poster-footer">
<image class="qrcode" src="{{qrcodeUrl}}" />
<view class="footer-text">
<text>长按识别小程序码</text>
<text>立即购买</text>
</view>
</view>
</view>
</snapshot>
<!-- 操作按钮 -->
<view class="actions">
<button class="save-btn" bindtap="saveToAlbum">保存到相册</button>
<button class="share-btn" open-type="share">分享给好友</button>
</view>
</view>.poster {
width: 300px;
margin: 20px auto;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.poster-content {
background: #fff;
}
.product-image {
width: 100%;
height: 300px;
}
.product-info {
padding: 16px;
}
.product-name {
display: block;
font-size: 16px;
font-weight: bold;
color: #333;
}
.product-desc {
display: block;
margin-top: 8px;
font-size: 12px;
color: #999;
line-height: 1.5;
}
.price-row {
display: flex;
align-items: baseline;
margin-top: 12px;
}
.price {
font-size: 20px;
font-weight: bold;
color: #f5222d;
}
.original-price {
margin-left: 8px;
font-size: 12px;
color: #999;
text-decoration: line-through;
}
.poster-footer {
display: flex;
align-items: center;
padding: 16px;
border-top: 1px solid #eee;
}
.qrcode {
width: 60px;
height: 60px;
}
.footer-text {
margin-left: 12px;
}
.footer-text text {
display: block;
font-size: 12px;
color: #666;
}
.actions {
display: flex;
padding: 20px;
}
.save-btn,
.share-btn {
flex: 1;
margin: 0 8px;
}Page({
data: {
product: {
image: 'https://example.com/product.jpg',
name: '精选商品名称',
desc: '这是商品的详细描述信息,可能会很长,需要截断显示...',
price: '99.00',
originalPrice: '199.00'
},
qrcodeUrl: '' // 小程序码
},
onLoad() {
this.generateQrcode()
},
generateQrcode() {
// 调用云函数生成小程序码
wx.cloud.callFunction({
name: 'getQrcode',
data: { page: 'pages/product/index', scene: this.data.product.id },
success: res => {
this.setData({ qrcodeUrl: res.result.url })
}
})
},
saveToAlbum() {
wx.showLoading({ title: '生成中...' })
this.createSelectorQuery()
.select('#poster')
.node()
.exec(res => {
const snapshotNode = res[0].node
snapshotNode.takeSnapshot({
type: 'arraybuffer',
format: 'png',
success: (result) => {
const fs = wx.getFileSystemManager()
const filePath = `${wx.env.USER_DATA_PATH}/poster_${Date.now()}.png`
fs.writeFile({
filePath,
data: result.data,
encoding: 'binary',
success: () => {
wx.saveImageToPhotosAlbum({
filePath,
success: () => {
wx.hideLoading()
wx.showToast({ title: '已保存到相册' })
},
fail: (err) => {
wx.hideLoading()
if (err.errMsg.includes('auth deny')) {
wx.showModal({
title: '提示',
content: '请授权保存图片到相册',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
}
})
}
}
})
}
})
},
fail: (err) => {
wx.hideLoading()
wx.showToast({ title: '生成失败', icon: 'error' })
console.error(err)
}
})
})
}
})隐藏截图区域
如果不想在页面上显示截图内容,可以将其移到屏幕外:
<view class="snapshot-container" style="position: fixed; left: -9999px;">
<snapshot id="hidden-snapshot">
<view class="content">
<!-- 截图内容 -->
</view>
</snapshot>
</view>注意事项
1. 仅限 Skyline:snapshot 组件仅在 Skyline 渲染模式下可用 2. 内容限制:截图内容必须在 snapshot 组件内部 3. 图片加载:确保组件内的图片已加载完成再截图 4. 性能考虑:大尺寸截图可能耗时较长,建议显示加载状态 5. 权限:保存到相册需要用户授权
与 canvas 截图对比
| 特性 | snapshot | canvas |
|---|---|---|
| 使用复杂度 | 简单(直接包裹内容) | 复杂(需要绑制代码) |
| 性能 | 较好 | 一般 |
| 内容支持 | 任意 WXML 内容 | 需要手动绘制 |
| 兼容性 | 仅 Skyline | 通用 |
| 定制能力 | 使用 WXSS 样式 | 完全自定义 |
最佳实践
1. 等待图片加载
Page({
data: {
imagesLoaded: 0,
totalImages: 3
},
onImageLoad() {
const loaded = this.data.imagesLoaded + 1
this.setData({ imagesLoaded: loaded })
if (loaded === this.data.totalImages) {
this.setData({ canSnapshot: true })
}
}
})2. 使用 jpg 减小文件大小
snapshotNode.takeSnapshot({
type: 'arraybuffer',
format: 'jpg',
quality: 0.8, // 80% 质量
success: (res) => {
// ...
}
})3. 处理授权被拒
wx.saveImageToPhotosAlbum({
filePath,
fail: (err) => {
if (err.errMsg.includes('auth deny')) {
wx.showModal({
title: '需要授权',
content: '保存图片需要您授权访问相册',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
}
})
}
}
})Related skills
How it compares
Use skyline-components for Skyline-specific Mini Program UI; use standard WeChat docs when the project does not adopt the Skyline renderer.
FAQ
Which Skyline components does skyline-components cover?
skyline-components covers scroll-view, swiper, text, image, input, textarea, view, button, plus Skyline additions list-view and grid-view for list and waterfall layouts.
When should developers use list-view over scroll-view?
skyline-components recommends list-view as a Skyline list layout container and grid-view for grid or waterfall layouts, while scroll-view handles enhanced nested scrolling and pull-to-refresh patterns.