Skyroc Native UI

TextEllipsis

多行文本折叠,展开 / 收起入口内联在末行

文本省略(TextEllipsis)把长文本折叠成指定行数,并把「展开 / 收起」入口内联在末行的省略号之后。它不是简单地加一个 numberOfLines —— 为了让操作文本真正跟在省略号后面而不是另起一行,组件会用一段隐藏文本量出真实行数,再用二分找出正文该截到第几个字符。

import { TextEllipsis } from '@skyroc/native-ui';

基础用法

content 是文本内容,rows 是收起态最多显示的行数(默认 1)。不传 expandText 时不做任何测量,直接交给原生的尾部省略号。

TextEllipsisBasic.tsx
Loading…
import { TextEllipsis } from '@skyroc/native-ui';
import { View } from 'react-native';

const LONG_TEXT =
  '在南方的冬天,屋檐下的水滴会沿着瓦片的边缘缓慢聚拢,最后落进院子里那口积满青苔的水缸,一整个下午都是这样重复的声音。他坐在门槛上看着,觉得时间被拉得很长,长到足以把一件小事想上很多遍。';

const TextEllipsisBasic = () => {
  return (
    <View className="gap-3 bg-background p-4">
      <TextEllipsis content={LONG_TEXT} />
      <TextEllipsis
        content={LONG_TEXT}
        rows={3}
      />
    </View>
  );
};

export { TextEllipsisBasic };

何时使用

  • 商品描述、评论、公告这类长文本需要折叠,且要给用户展开入口。
  • 只需要单纯截断、不需要展开入口时,直接给 TextnumberOfLines 更轻。

展开与收起

expandText 是收起态尾部的操作文本,collapseText 是展开态尾部的操作文本(留空则展开后不提供收起入口)。两者都渲染在正文的同一个 Text 里,所以会紧跟在省略号后面。

TextEllipsisExpand.tsx
Loading…
import { TextEllipsis } from '@skyroc/native-ui';
import { View } from 'react-native';

const LONG_TEXT =
  '在南方的冬天,屋檐下的水滴会沿着瓦片的边缘缓慢聚拢,最后落进院子里那口积满青苔的水缸,一整个下午都是这样重复的声音。他坐在门槛上看着,觉得时间被拉得很长,长到足以把一件小事想上很多遍。';

const TextEllipsisExpand = () => {
  return (
    <View className="gap-4 bg-background p-4">
      <TextEllipsis
        collapseText="收起"
        content={LONG_TEXT}
        expandText="展开"
      />
      <TextEllipsis
        collapseText=" 收起"
        content={LONG_TEXT}
        expandText=" 展开"
        rows={3}
      />
      {/* 不给 collapseText,展开后就没有收起入口 */}
      <TextEllipsis
        content={LONG_TEXT}
        expandText="更多"
      />
    </View>
  );
};

export { TextEllipsisExpand };

之所以要自己测量:numberOfLines 会在原生侧先把文本裁掉,onTextLayout 拿到的行数因此永远不超过 rows,靠它只能判断「是否恰好占满」而不是「是否溢出」。组件因此额外渲染一段不带 numberOfLines 的隐藏文本(opacity-0 + pointerEvents="none")量出真实行数,再二分收敛出裁剪点,测完就卸载。

默认展开

defaultExpanded 设置非受控模式的初始展开状态。

TextEllipsisDefaultExpanded.tsx
Loading…
import { TextEllipsis } from '@skyroc/native-ui';
import { View } from 'react-native';

const LONG_TEXT =
  '在南方的冬天,屋檐下的水滴会沿着瓦片的边缘缓慢聚拢,最后落进院子里那口积满青苔的水缸,一整个下午都是这样重复的声音。他坐在门槛上看着,觉得时间被拉得很长,长到足以把一件小事想上很多遍。';

const TextEllipsisDefaultExpanded = () => {
  return (
    <View className="bg-background p-4">
      <TextEllipsis
        defaultExpanded
        collapseText=" 收起"
        content={LONG_TEXT}
        expandText=" 展开"
        rows={2}
      />
    </View>
  );
};

export { TextEllipsisDefaultExpanded };

自定义省略号

dots 默认是 '...'。它只在 expandText 非空、需要给内联操作文本裁剪出位置时生效;没有操作文本时不做裁剪,省略号由原生接管。

TextEllipsisDots.tsx
Loading…
import { TextEllipsis } from '@skyroc/native-ui';
import { View } from 'react-native';

const LONG_TEXT =
  '在南方的冬天,屋檐下的水滴会沿着瓦片的边缘缓慢聚拢,最后落进院子里那口积满青苔的水缸,一整个下午都是这样重复的声音。他坐在门槛上看着,觉得时间被拉得很长,长到足以把一件小事想上很多遍。';

const TextEllipsisDots = () => {
  return (
    <View className="gap-3 bg-background p-4">
      <TextEllipsis
        collapseText="收起"
        content={LONG_TEXT}
        dots="…… "
        expandText="展开"
        rows={2}
      />
      <TextEllipsis
        collapseText=" 收起"
        content={LONG_TEXT}
        dots=" ——"
        expandText=" 展开"
        rows={2}
      />
    </View>
  );
};

export { TextEllipsisDots };

无溢出边界

文本没有超出 rows 时不显示展开入口,也不会多加省略号;空文本同理。

TextEllipsisNoOverflow.tsx
Loading…
import { TextEllipsis } from '@skyroc/native-ui';
import { View } from 'react-native';

const SHORT_TEXT = '一行放得下的短文本。';

const TextEllipsisNoOverflow = () => {
  return (
    <View className="gap-3 bg-background p-4">
      <TextEllipsis
        collapseText="收起"
        content={SHORT_TEXT}
        expandText="展开"
      />
      <TextEllipsis
        collapseText="收起"
        content=""
        expandText="展开"
      />
    </View>
  );
};

export { TextEllipsisNoOverflow };

动态行数

rows 变化后重新测量,并回到收起态。

TextEllipsisRows.tsx
Loading…
import { Button, TextEllipsis } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const LONG_TEXT =
  '在南方的冬天,屋檐下的水滴会沿着瓦片的边缘缓慢聚拢,最后落进院子里那口积满青苔的水缸,一整个下午都是这样重复的声音。他坐在门槛上看着,觉得时间被拉得很长,长到足以把一件小事想上很多遍。';

const ROWS_OPTIONS = [1, 2, 3];

const TextEllipsisRows = () => {
  const [rows, setRows] = useState(2);

  return (
    <View className="bg-background p-4">
      <View className="mb-4">
        <TextEllipsis
          collapseText=" 收起"
          content={LONG_TEXT}
          expandText=" 展开"
          rows={rows}
        />
      </View>
      <View className="flex-row flex-wrap items-center gap-3">
        {ROWS_OPTIONS.map(item => (
          <Button
            key={item}
            variant={item === rows ? 'solid' : 'tonal'}
            onPress={() => setRows(item)}
          >
            {`${item} 行`}
          </Button>
        ))}
      </View>
    </View>
  );
};

export { TextEllipsisRows };

动态内容

content 变化同样触发重测;长文本换成短文本后展开入口会自动消失。

TextEllipsisContent.tsx
Loading…
import { Button, TextEllipsis } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const LONG_TEXT =
  '在南方的冬天,屋檐下的水滴会沿着瓦片的边缘缓慢聚拢,最后落进院子里那口积满青苔的水缸,一整个下午都是这样重复的声音。他坐在门槛上看着,觉得时间被拉得很长,长到足以把一件小事想上很多遍。';

const SHORT_TEXT = '一行放得下的短文本。';

const TextEllipsisContent = () => {
  const [content, setContent] = useState(LONG_TEXT);

  return (
    <View className="bg-background p-4">
      <View className="mb-4">
        <TextEllipsis
          collapseText=" 收起"
          content={content}
          expandText=" 展开"
          rows={2}
        />
      </View>
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          variant={content === LONG_TEXT ? 'solid' : 'tonal'}
          onPress={() => setContent(LONG_TEXT)}
        >
          长文本
        </Button>
        <Button
          variant={content === SHORT_TEXT ? 'solid' : 'tonal'}
          onPress={() => setContent(SHORT_TEXT)}
        >
          短文本
        </Button>
      </View>
    </View>
  );
};

export { TextEllipsisContent };

contentrows 外,dotsexpandTextsizeweightclassNames.text 变化也会触发重测 —— 它们都会占掉正文的位置。运行时通过 style 改字号不在这个信号里,需要重测时请把变化一并反映到上述入参上。

受控模式

expanded + onExpandedChange 接管展开状态,文末操作与外部按钮共享同一份状态。

TextEllipsisControlled.tsx
Loading…
import { Button, Text, TextEllipsis } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const LONG_TEXT =
  '在南方的冬天,屋檐下的水滴会沿着瓦片的边缘缓慢聚拢,最后落进院子里那口积满青苔的水缸,一整个下午都是这样重复的声音。他坐在门槛上看着,觉得时间被拉得很长,长到足以把一件小事想上很多遍。';

const TextEllipsisControlled = () => {
  const [controlledExpanded, setControlledExpanded] = useState(false);

  return (
    <View className="bg-background p-4">
      <View className="mb-4">
        <TextEllipsis
          collapseText=" 收起"
          content={LONG_TEXT}
          expandText=" 展开"
          expanded={controlledExpanded}
          rows={2}
          onExpandedChange={setControlledExpanded}
        />
      </View>
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          variant="tonal"
          onPress={() => setControlledExpanded(prev => !prev)}
        >
          {controlledExpanded ? '外部收起' : '外部展开'}
        </Button>
        <Text color="muted">expanded:{String(controlledExpanded)}</Text>
      </View>
    </View>
  );
};

export { TextEllipsisControlled };

命令式控制

ref.toggle() 取反,ref.toggle(true / false) 指定状态。

TextEllipsisImperative.tsx
Loading…
import { Button, TextEllipsis } from '@skyroc/native-ui';
import type { TextEllipsisRef } from '@skyroc/native-ui';
import { useRef } from 'react';
import { View } from 'react-native';

const LONG_TEXT =
  '在南方的冬天,屋檐下的水滴会沿着瓦片的边缘缓慢聚拢,最后落进院子里那口积满青苔的水缸,一整个下午都是这样重复的声音。他坐在门槛上看着,觉得时间被拉得很长,长到足以把一件小事想上很多遍。';

const TextEllipsisImperative = () => {
  const manualRef = useRef<TextEllipsisRef>(null);

  return (
    <View className="bg-background p-4">
      <View className="mb-4">
        <TextEllipsis
          collapseText=" 收起"
          content={LONG_TEXT}
          expandText=" 展开"
          ref={manualRef}
          rows={2}
        />
      </View>
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          variant="tonal"
          onPress={() => manualRef.current?.toggle(true)}
        >
          展开
        </Button>
        <Button
          variant="tonal"
          onPress={() => manualRef.current?.toggle(false)}
        >
          收起
        </Button>
        <Button
          variant="outline"
          onPress={() => manualRef.current?.toggle()}
        >
          切换
        </Button>
      </View>
    </View>
  );
};

export { TextEllipsisImperative };

文字与样式

className 追加到根容器上,classNames 按 slot 细粒度覆盖;color / size / weightText 属性照常生效,并且会同步到隐藏的测量文本上(否则量出来的行数与正文对不上)。

slot作用位置
root承载正文与隐藏测量文本的容器
text正文 Text
action内联在末行的展开 / 收起文本
TextEllipsisStyles.tsx
Loading…
import { TextEllipsis } from '@skyroc/native-ui';
import { View } from 'react-native';

const LONG_TEXT =
  '在南方的冬天,屋檐下的水滴会沿着瓦片的边缘缓慢聚拢,最后落进院子里那口积满青苔的水缸,一整个下午都是这样重复的声音。他坐在门槛上看着,觉得时间被拉得很长,长到足以把一件小事想上很多遍。';

const TextEllipsisStyles = () => {
  return (
    <View className="gap-4 bg-background p-4">
      <TextEllipsis
        className="rounded-lg bg-secondary p-3"
        collapseText=" 收起"
        content={LONG_TEXT}
        expandText=" 展开"
        rows={2}
      />
      <TextEllipsis
        classNames={{
          action: 'font-semibold text-warning',
          root: 'rounded-lg border border-border p-3',
          text: 'text-muted-foreground'
        }}
        collapseText=" 收起"
        content={LONG_TEXT}
        expandText=" 展开"
        rows={2}
      />
      {/* 字号变化会重新测量 */}
      <TextEllipsis
        color="info"
        collapseText=" 收起"
        content={LONG_TEXT}
        expandText=" 展开"
        rows={2}
        size="lg"
        weight="semibold"
      />
    </View>
  );
};

export { TextEllipsisStyles };

表情文本边界

裁剪按 Unicode 码点进行,二分不会把代理对(emoji)截成半个字符。

TextEllipsisEmoji.tsx
Loading…
import { TextEllipsis } from '@skyroc/native-ui';
import { View } from 'react-native';

const EMOJI_TEXT =
  '🎉🎊✨🌟💫⭐️🌙☀️🌈🍀🌸🌺🌻🌼🌷💐🍎🍊🍋🍌🍉🍇🍓🫐🥝🍒🍑🥭🍍🥥 后面跟着一段普通文字,用来确认二分裁剪不会把表情截成半个字符。';

const TextEllipsisEmoji = () => {
  return (
    <View className="bg-background p-4">
      <TextEllipsis
        collapseText=" 收起"
        content={EMOJI_TEXT}
        expandText=" 展开"
        rows={2}
      />
    </View>
  );
};

export { TextEllipsisEmoji };

API

TextEllipsis

除下表外,TextEllipsis 透传 Text 的属性(color / size / weight / selectable / style 等),其中 childrennumberOfLinesonTextLayoutasChild 由组件接管。

属性说明类型默认值
content*文本内容string-
rows收起态最多显示的行数number1
expandText收起态尾部的操作文本,留空则只做原生省略、不显示展开入口string''
collapseText展开态尾部的操作文本,留空则展开后不提供收起入口string''
dots截断处的省略号,仅在 expandText 非空时生效string'...'
expanded是否展开(受控)boolean-
defaultExpanded初始是否展开(非受控)booleanfalse
onExpandedChange展开状态变化回调(expanded: boolean) => void-
className根容器类名,合并在 classNames.root 之后string-
classNames各 slot 的类名覆盖,见「文字与样式」一节SlotClassNames<TextEllipsisSlots>-
ref命令式控制的 ref,用于 toggleRef<TextEllipsisRef>-

类型

import type { TextEllipsisProps, TextEllipsisRef, TextEllipsisSlots } from '@skyroc/native-ui';

TextEllipsisSlots

可通过 classNames 覆盖的 slot 名称。

'action' | 'root' | 'text'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 TextEllipsisSlots。

Partial<Record<Slots, string>>

TextEllipsisRef

组件实例暴露的命令式方法。

字段类型说明
toggle*(expanded?: boolean) => void切换展开 / 收起;传 true 展开、传 false 收起,不传则取反。