元素

Scroll

<Scroll> 是原生滚动视图(iOS 和 Mac Catalyst 上是 UIScrollView,Android 上是 ScrollView)。它把所有子元素作为内容进行布局,并在原生侧滚动:滚动、惯性滑动、回弹和吸附都不需要等待 JS。

el-scroll.tsx 在线编辑 在新标签页打开
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):

#滚动

属性类型默认值说明
horizontalbooleanfalse横向滚动。
scrollEnabledbooleantrue用户可以滚动。
showsIndicatorbooleantrue显示滚动条。
bouncesbooleantrue允许越过边缘回弹。
pagingEnabledbooleanfalse停在视口尺寸的整数倍处。
snapToIntervalnumber0停在该长度的整数倍处(0 表示关闭)。
snapToOffsetsnumber[]停在这些偏移量处。
snapToAlignment"start" | "center" | "end""start"使用 snapToInterval 时,条目的哪个位置与视口对齐。
disableIntervalMomentumbooleanfalse一次惯性滑动最多移动一个吸附点。
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(点击被某个元素处理时键盘保持)。
scrollValueNativeValue接收滚动偏移量(主轴方向)的原生值,用于随滚动变化的动画。
scrollEventThrottlenumber两次 onScroll 事件之间的最小毫秒数(无论如何每帧最多一次)。

#下拉刷新

属性类型说明
refreshingboolean显示刷新指示器。
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,内容就可以双指捏合缩放;放大后可以双向滚动。列表不支持缩放。

属性类型默认值说明
minimumZoomScalenumber1最小缩放比例。
maximumZoomScalenumber1最大缩放比例。
zoomScalenumber值变化时缩放到该比例;否则保留用户的缩放。
bouncesZoombooleantrue捏合可以超出限制并弹回。
pinchGestureEnabledbooleantrue用户可以捏合缩放。
centerContentbooleanfalse内容(缩小后)小于视口时居中显示。
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>;

#事件

事件载荷触发时机
onScrollScrollEvent偏移量变化(每帧最多一次,且不比 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 在缩放结束后会按缩放后的分辨率重绘文本和画布。