元素

Text

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

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

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: ui().accent, textDecorationLine: "underline" }) as const;
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>text</Text>

      <Text style={{ fontSize: 16, lineHeight: 24, color: ui().text }}>
        {"Nested text elements are spans: "}
        <Text style={{ fontWeight: "700" }}>bold</Text>
        {", "}
        <Text style={{ fontStyle: "italic", color: ui().violet }}>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: ui().muted }}>Pressed: {tapped()}</Text>

      <View style={{ height: 1, backgroundColor: ui().line }} />

      <Text numberOfLines={lines()} style={{ fontSize: 15, color: ui().text }}>
        {LONG}
      </Text>
      <View style={{ flexDirection: "row", gap: 8 }}>
        <View
          onPress={() => lines.set(lines() === 2 ? 0 : 2)}
          activeOpacity={0.6}
          style={{ ...ui().raised, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 8 }}
        >
          <Text style={{ color: ui().text }}>{lines() === 2 ? "Show all" : "numberOfLines 2"}</Text>
        </View>
      </View>

      <Text numberOfLines={1} ellipsizeMode="middle" style={{ fontSize: 15, color: ui().muted }}>
        {LONG}
      </Text>

      <Text style={{ fontSize: 17, color: ui().text }}>
        <Text style={{ textDecorationLine: "line-through", textDecorationColor: ui().down }}>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: ui().onAccent,
          backgroundColor: "#c8f26d",
          paddingHorizontal: 10,
          borderRadius: 8,
          textShadowColor: "rgba(255,255,255,0.55)",
          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)。
selectableboolean用户可以选择文本的任意范围:长按选中一个词,拖动选择柄扩展,编辑菜单复制。不设置时文字不可选择,但桌面浏览器除外:在那里文字默认可选,交互元素里的除外;设为 false 在那里也不可选择。
selectionColorColorValue可选文本的选区高亮(及选择柄)颜色;默认为平台的强调色。
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 方法

没有自己的方法。

#平台说明

  • 选区只在一个文本块(一个 <Text> 及其片段)之内,不跨越多个文本块。iOS 显示其编辑菜单(拷贝、全选、查询、翻译、共享),Android 显示浮动工具栏(复制、全选、分享)。
  • 在 iOS 上,非实线单线的文本装饰线(double、dotted、dashed、wavy)由 NSH 自行绘制。
  • iOS 上的文本不绘制渐变背景和内阴影。
  • nsh/device 的 fontScale() 给出当前系统的文字缩放比例。