元素
Scroll
<Scroll> 是原生滚动视图(iOS 和 Mac Catalyst 上是 UIScrollView,Android 上是 ScrollView)。它把所有子元素作为内容进行布局,并在原生侧滚动:滚动、惯性滑动、回弹和吸附都不需要等待 JS。
import { mount, Scroll, signal, Text, View, type YNode } from "nsh";
const colors = ["#0969da", "#8250df", "#bf3989", "#cf222e", "#bc4c00", "#1a7f37"];
function App() {
const page = signal(0);
const y = signal(0);
let scroller!: YNode;
return (
<View style={{ flex: 1, backgroundColor: "#f6f8fa", paddingTop: 24, gap: 12 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: "#1f2328", paddingHorizontal: 20 }}>scroll</Text>
<Text style={{ color: "#57606a", paddingHorizontal: 20 }}>Horizontal, pagingEnabled · page {page() + 1}</Text>
<Scroll
horizontal
pagingEnabled
showsIndicator={false}
style={{ height: 140, flexGrow: 0 }}
onMomentumScrollEnd={(e) => page.set(Math.round(e.x / 390))}
>
<Each of={colors}>
{(color, index) => (
<View style={{ width: 390, padding: 12 }}>
<View style={{ flex: 1, borderRadius: 16, backgroundColor: color(), alignItems: "center", justifyContent: "center" }}>
<Text style={{ color: "#ffffff", fontSize: 32, fontWeight: "700" }}>{index() + 1}</Text>
</View>
</View>
)}
</Each>
</Scroll>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 20 }}>
<Text style={{ color: "#57606a" }}>Vertical · y = {Math.round(y())}</Text>
<View
onPress={() => scroller.scrollTo({ y: 0, animated: true })}
activeOpacity={0.6}
style={{ backgroundColor: "#ddf4ff", borderRadius: 8, paddingHorizontal: 12, paddingVertical: 6 }}
>
<Text style={{ color: "#0969da" }}>scrollTo top</Text>
</View>
</View>
<Scroll ref={scroller} style={{ flex: 1 }} scrollEventThrottle={16} onScroll={(e) => y.set(e.y)}>
<View style={{ paddingHorizontal: 20, paddingBottom: 20, gap: 8 }}>
<Each of={Array.from({ length: 40 }, (_, i) => i + 1)}>
{(n) => (
<View style={{ backgroundColor: "#ffffff", borderRadius: 10, padding: 14, borderWidth: 1, borderColor: "#d0d7de" }}>
<Text style={{ color: "#1f2328" }}>Row {n()}</Text>
</View>
)}
</Each>
</View>
</Scroll>
</View>
);
}
mount(App);<Scroll style={{ flex: 1 }}>
<View style={{ padding: 16, gap: 8 }}>
<Each of={rows}>{(row) => <Text>{row().title}</Text>}</Each>
</View>
</Scroll>
<Scroll> 会创建所有子元素,适合中等规模的页面。对于很长或数量不定的数据,请使用列表:它只创建视口附近的行,并复用它们。
滚动视图在滚动方向上必须有确定的尺寸:在有尺寸的容器中给它 flex: 1(或 height)。其内容按滚动视图的宽度(horizontal 时为高度)布局,高度(宽度)按需增长。
#属性
除通用属性外(style 的类型为 ViewStyle):
#滚动
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
horizontal | boolean | false | 横向滚动。 |
scrollEnabled | boolean | true | 用户可以滚动。 |
showsIndicator | boolean | true | 显示滚动条。 |
bounces | boolean | true | 允许越过边缘回弹。 |
pagingEnabled | boolean | false | 停在视口尺寸的整数倍处。 |
snapToInterval | number | 0 | 停在该长度的整数倍处(0 表示关闭)。 |
snapToOffsets | number[] | 停在这些偏移量处。 | |
snapToAlignment | "start" | "center" | "end" | "start" | 使用 snapToInterval 时,条目的哪个位置与视口对齐。 |
disableIntervalMomentum | boolean | false | 一次惯性滑动最多移动一个吸附点。 |
decelerationRate | "normal" | "fast" | number | 平台默认 | 惯性滑动减速的快慢:每毫秒保留的速度比例(normal 为 0.998,fast 为 0.99)。 |
contentInset | { top?, right?, bottom?, left? } | 内容四周的留白。 | |
contentOffset | { x?, y? } | 初始位置。 | |
keyboardDismissMode | "none" | "on-drag" | "interactive" | "none" | 拖动时是否收起键盘。 |
keyboardShouldPersistTaps | "never" | "always" | "handled" | boolean | "never" | 点击是否收起键盘:never(点击只收起键盘)、always(键盘保持)、handled(点击被某个元素处理时键盘保持)。 |
scrollValue | NativeValue | 接收滚动偏移量(主轴方向)的原生值,用于随滚动变化的动画。 | |
scrollEventThrottle | number | 两次 onScroll 事件之间的最小毫秒数(无论如何每帧最多一次)。 |
#下拉刷新
| 属性 | 类型 | 说明 |
|---|---|---|
refreshing | boolean | 显示刷新指示器。 |
onRefresh | () => void | 用户下拉刷新:开始加载,并在结束前保持 refreshing 为真。 |
const refreshing = signal(false);
async function reload() {
refreshing.set(true);
await load();
refreshing.set(false);
}
<Scroll style={{ flex: 1 }} refreshing={refreshing()} onRefresh={reload}>
…
</Scroll>;
#缩放
把 maximumZoomScale 设得大于 minimumZoomScale,内容就可以双指捏合缩放;放大后可以双向滚动。列表不支持缩放。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
minimumZoomScale | number | 1 | 最小缩放比例。 |
maximumZoomScale | number | 1 | 最大缩放比例。 |
zoomScale | number | 值变化时缩放到该比例;否则保留用户的缩放。 | |
bouncesZoom | boolean | true | 捏合可以超出限制并弹回。 |
pinchGestureEnabled | boolean | true | 用户可以捏合缩放。 |
centerContent | boolean | false | 内容(缩小后)小于视口时居中显示。 |
let photo!: YNode;
<Scroll ref={photo} maximumZoomScale={4} centerContent style={{ flex: 1, backgroundColor: "#000" }}>
<Image src={picture} style={{ width: 390, height: 520 }} onPress={() => photo.zoomTo({ scale: 1, animated: true })} />
</Scroll>;
#事件
| 事件 | 载荷 | 触发时机 |
|---|---|---|
onScroll | ScrollEvent | 偏移量变化(每帧最多一次,且不比 scrollEventThrottle 更频繁)。 |
onScrollBeginDrag / onScrollEndDrag | { x, y } | 用户开始 / 停止拖动。 |
onMomentumScrollBegin / onMomentumScrollEnd | { x, y } | 拖动后的惯性滑动开始 / 停止。被新一次拖动打断的滑动同样以 onMomentumScrollEnd 结束。 |
onScrollEnd | { x, y } | 滚动停了下来。 |
onRefresh | 无 | 触发了下拉刷新。 |
ScrollEvent 为 { x, y, contentWidth, contentHeight, width, height, zoomScale }:偏移量、(缩放后的)内容尺寸、视口尺寸和缩放比例(不缩放时为 1)。
#ref 方法
| 方法 | 说明 |
|---|---|
scrollTo({ x?, y?, animated? }) | 滚动到某个偏移量。 |
zoomTo({ scale, x?, y?, animated? }) | 缩放到 scale(限制在上下限内),使内容中的点(x、y,未缩放时的点坐标)位于视口中央,未给出时围绕中央缩放。例如双击放大被点击的位置。 |
let scroller!: YNode;
<Scroll ref={scroller}>…</Scroll>;
scroller.scrollTo({ y: 0, animated: true });
#键盘
滚动视图中的输入框会在原生侧自动避开键盘:滚动视图获得底部内边距,并滚动到获得焦点的输入框,全程不经过 JS。对于滚动视图之外的内容,请使用 nsh/ui/mobile 的 KeyboardAvoidingView。参见表单与键盘。
#从右到左
在从右到左的布局中,横向滚动视图从右端开始,并在内容变化时保持与右端的距离;其子元素会被镜像。
#平台说明
- Android:下拉刷新由 nsh 自行绘制(越界回弹效果仍是原生的),
contentInset以内边距的方式实现,decelerationRate只影响吸附时的滑动。 - Android 没有可缩放的滚动视图:nsh 会切换到自己的实现,围绕内容左上角缩放并支持双向滚动。双指间距超过 Android 的最小距离(约 27 mm)后才开始捏合。
- Mac Catalyst(Mac 界面风格)没有系统刷新控件;nsh 会自行检测下拉并显示加载指示器。
- iOS 在缩放结束后会按缩放后的分辨率重绘文本和画布。