元素

Text

<Text> 绘制一个段落。它是唯一能显示字符串的元素:<View> 的字符串子节点什么也不会画。段落在原生侧排版(iOS 和 Mac Catalyst 上用 CoreText,Android 上用 StaticLayout),并在引擎线程上测量,主线程只负责绘制。

el-text.tsx 在线编辑 在新标签页打开
import { mount, signal, Text, View } from "nsh";

const LONG =
  "Text is typeset natively (CoreText on iOS, StaticLayout on Android) and measured on the engine thread, so the main thread only draws it.";

function App() {
  const tapped = signal("nothing yet");
  const lines = signal(2);
  const link = { color: "#0969da", textDecorationLine: "underline" } as const;
  return (
    <View style={{ flex: 1, backgroundColor: "#ffffff", padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: "#1f2328" }}>text</Text>

      <Text style={{ fontSize: 16, lineHeight: 24, color: "#1f2328" }}>
        {"Nested text elements are spans: "}
        <Text style={{ fontWeight: "700" }}>bold</Text>
        {", "}
        <Text style={{ fontStyle: "italic", color: "#8250df" }}>italic</Text>
        {", and "}
        <Text style={link} onPress={() => tapped.set("the first link")}>
          links
        </Text>
        {" that take presses of "}
        <Text style={link} onPress={() => tapped.set("the second link")}>
          their own
        </Text>
        .
      </Text>
      <Text style={{ fontSize: 13, color: "#6e7781" }}>Pressed: {tapped()}</Text>

      <View style={{ height: 1, backgroundColor: "#d0d7de" }} />

      <Text numberOfLines={lines()} style={{ fontSize: 15, color: "#1f2328" }}>
        {LONG}
      </Text>
      <View style={{ flexDirection: "row", gap: 8 }}>
        <View
          onPress={() => lines.set(lines() === 2 ? 0 : 2)}
          activeOpacity={0.6}
          style={{ backgroundColor: "#ddf4ff", borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8 }}
        >
          <Text style={{ color: "#0969da" }}>{lines() === 2 ? "Show all" : "numberOfLines 2"}</Text>
        </View>
      </View>

      <Text numberOfLines={1} ellipsizeMode="middle" style={{ fontSize: 15, color: "#57606a" }}>
        {LONG}
      </Text>

      <Text style={{ fontSize: 17, color: "#1f2328" }}>
        <Text style={{ textDecorationLine: "line-through", textDecorationColor: "#cf222e" }}>struck</Text>{" "}
        <Text style={{ textTransform: "uppercase", letterSpacing: 2 }}>spaced</Text>{" "}
        <Text style={{ fontVariant: ["tabular-nums"] }}>0123456789</Text>
      </Text>

      <Text
        style={{
          fontSize: 28,
          fontWeight: "800",
          color: "#ffffff",
          backgroundColor: "#54aeff",
          paddingHorizontal: 10,
          borderRadius: 8,
          textShadowColor: "rgba(0,0,0,0.5)",
          textShadowOffset: { width: 2, height: 2 },
          textShadowRadius: 4,
        }}
      >
        Text shadow
      </Text>
    </View>
  );
}

mount(App);
<Text style={{ fontSize: 16, lineHeight: 24, color: "#1f2328" }}>
  继续之前请阅读<text style={{ fontWeight: "700" }}>条款</Text>。
</Text>

#片段

<Text> 里的 <Text> 是同一段落中的一个片段(span),而不是单独的视图。片段继承段落的文本样式(color、fontSize、fontWeight 等),并覆盖自己设置的部分。整个文本块是一个原生视图。

<Text style={{ fontSize: 17 }}>
  <Text style={{ fontWeight: "300" }}>细体 </Text>
  <Text style={{ fontWeight: "bold" }}>粗体 </Text>
  <Text style={{ fontStyle: "italic", color: "#8250df" }}>斜体</Text>
</Text>

#链接

带按压处理函数的片段就是链接:按在它的字符上由该片段处理,按在其他位置则由段落处理。

<Text style={{ fontSize: 15 }} onPress={() => log("整句")}>
  {"继续即表示你同意"}
  <Text style={{ color: "#0969da", textDecorationLine: "underline" }} onPress={() => open("terms")}>
    服务条款
  </Text>
  。
</Text>

#行内视图和图片

<Text> 中的其他元素(<View>、<Image>,即使位于片段内)都是行内附件:按自身尺寸布局,坐落在基线上,并随文字一起换行。它们可以单独响应按压。

<Text style={{ fontSize: 16, lineHeight: 26 }}>
  {"由 "}
  <Image src={logo} style={{ width: 18, height: 18, borderRadius: 4 }} />
  {" nsh 构建。状态 "}
  <View style={{ backgroundColor: "#1a7f37", borderRadius: 9, paddingHorizontal: 7, height: 18, justifyContent: "center" }}>
    <Text style={{ color: "#ffffff", fontSize: 11, fontWeight: "700" }}>LIVE</Text>
  </View>
</Text>

被 numberOfLines 或省略号截掉的附件会被隐藏。复制可选中的文本时不包含附件。

#空白字符

JSX 文本遵循 React 的规则:除文本首尾外,每行都会去掉首尾空白,只含空白的行被丢弃,剩下的行用一个空格连接。同一行内的空格会保留({a} {b} 保留中间的空格)。片段需要前导或尾随空格时,写 {" "} 或字符串表达式。

#属性

除通用属性外(style 的类型为 TextStyle):

属性类型默认值说明
numberOfLinesnumber0最多显示的行数;0 表示不限。
ellipsizeMode"tail" | "head" | "middle" | "clip""tail"超出 numberOfLines 的文本在哪里截断。
allowFontScalingbooleantrue跟随系统的文字大小设置。
maxFontSizeMultipliernumber0系统缩放的上限;0 表示不限。
adjustsFontSizeToFitbooleanfalse缩小字号,直到文本能放进其行数(numberOfLines)和盒子。
minimumFontScalenumberadjustsFontSizeToFit 最多缩小到的比例(0..1;至少 0.1)。
selectablebooleanfalse长按高亮整个文本块并提供“复制”。
onTextLayout(e: { lines: TextLayoutLine[] }) => void每次布局后给出排好的各行。

Tailwind 类 select-text 等同于 selectable。

#文本样式

TextStyle 在视图样式之上增加了:color、fontSize、fontWeight("normal"、"bold" 或 100–900)、fontStyle、fontFamily、lineHeight、letterSpacing、textAlign、textAlignVertical、textTransform、textDecorationLine、textDecorationColor、textDecorationStyle、textDecorationThickness、textUnderlineOffset、textShadowColor、textShadowOffset、textShadowRadius、fontVariant 和 writingDirection。参见样式参考和文本。

fontFamily 接受系统字体族,或导入字体文件得到的名称:

import { Text } from "nsh";
import Inter from "./fonts/Inter-Regular.ttf";

<Text style={{ fontFamily: Inter, fontSize: 17 }}>Inter</Text>;

字体会在第一帧之前注册。参见字体。

#事件

事件载荷触发时机
onTextLayout{ lines: TextLayoutLine[] }段落每次布局之后。
onPress、onLongPress 等无与 view 相同;在片段上时,指按在其字符上的按压。
onLayout{ x, y, width, height }文本块的 frame 发生变化。

TextLayoutLine 为 { text, x, y, width, height, ascender, descender, capHeight, xHeight },坐标位于文本的内容盒中:

const lines = signal(0);
<Text onTextLayout={(e) => lines.set(e.lines.length)}>{article}</Text>;

#ref 方法

没有自己的方法。

#平台说明

  • selectable 复制的是整个文本块,不支持选择部分范围。iOS 显示编辑菜单,Android 显示浮动操作模式。
  • 在 iOS 上,非实线单线的文本装饰线(double、dotted、dashed、wavy)由 nsh 自行绘制。
  • iOS 上的文本不绘制渐变背景和内阴影。
  • nsh/device 的 fontScale() 给出当前系统的文字缩放比例。