TextEllipsis
多行文本折叠,展开 / 收起入口内联在末行
文本省略(TextEllipsis)把长文本折叠成指定行数,并把「展开 / 收起」入口内联在末行的省略号之后。它不是简单地加一个 numberOfLines —— 为了让操作文本真正跟在省略号后面而不是另起一行,组件会用一段隐藏文本量出真实行数,再用二分找出正文该截到第几个字符。
import { TextEllipsis } from '@skyroc/native-ui';基础用法
content 是文本内容,rows 是收起态最多显示的行数(默认 1)。不传 expandText 时不做任何测量,直接交给原生的尾部省略号。
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 };何时使用
- 商品描述、评论、公告这类长文本需要折叠,且要给用户展开入口。
- 只需要单纯截断、不需要展开入口时,直接给
Text传numberOfLines更轻。
展开与收起
expandText 是收起态尾部的操作文本,collapseText 是展开态尾部的操作文本(留空则展开后不提供收起入口)。两者都渲染在正文的同一个 Text 里,所以会紧跟在省略号后面。
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 设置非受控模式的初始展开状态。
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 非空、需要给内联操作文本裁剪出位置时生效;没有操作文本时不做裁剪,省略号由原生接管。
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 时不显示展开入口,也不会多加省略号;空文本同理。
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 变化后重新测量,并回到收起态。
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 变化同样触发重测;长文本换成短文本后展开入口会自动消失。
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 };除 content 与 rows 外,dots、expandText、size、weight、classNames.text 变化也会触发重测 —— 它们都会占掉正文的位置。运行时通过 style 改字号不在这个信号里,需要重测时请把变化一并反映到上述入参上。
受控模式
expanded + onExpandedChange 接管展开状态,文末操作与外部按钮共享同一份状态。
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) 指定状态。
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 / weight 等 Text 属性照常生效,并且会同步到隐藏的测量文本上(否则量出来的行数与正文对不上)。
| slot | 作用位置 |
|---|---|
root | 承载正文与隐藏测量文本的容器 |
text | 正文 Text |
action | 内联在末行的展开 / 收起文本 |
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)截成半个字符。
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 等),其中 children、numberOfLines、onTextLayout、asChild 由组件接管。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| content* | 文本内容 | string | - |
| rows | 收起态最多显示的行数 | number | 1 |
| expandText | 收起态尾部的操作文本,留空则只做原生省略、不显示展开入口 | string | '' |
| collapseText | 展开态尾部的操作文本,留空则展开后不提供收起入口 | string | '' |
| dots | 截断处的省略号,仅在 expandText 非空时生效 | string | '...' |
| expanded | 是否展开(受控) | boolean | - |
| defaultExpanded | 初始是否展开(非受控) | boolean | false |
| onExpandedChange | 展开状态变化回调 | (expanded: boolean) => void | - |
| className | 根容器类名,合并在 classNames.root 之后 | string | - |
| classNames | 各 slot 的类名覆盖,见「文字与样式」一节 | SlotClassNames<TextEllipsisSlots> | - |
| ref | 命令式控制的 ref,用于 toggle | Ref<TextEllipsisRef> | - |
类型
import type { TextEllipsisProps, TextEllipsisRef, TextEllipsisSlots } from '@skyroc/native-ui';TextEllipsisSlots
可通过 classNames 覆盖的 slot 名称。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 TextEllipsisSlots。
TextEllipsisRef
组件实例暴露的命令式方法。
| 字段 | 类型 | 说明 |
|---|---|---|
| toggle* | (expanded?: boolean) => void | 切换展开 / 收起;传 true 展开、传 false 收起,不传则取反。 |