ReactNative 文字折叠
h5 的单行/多行文字溢出截断实现如下
// 单行
div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// 多行
div {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
}
在 h5 中,多行文字截断的兼容性并不好,而在 RN 中,实现文字截断则简单的多,只需要给 Text 组件添加 numberOfLines 属性即可。
但是通常我们还会遇到如下需求:
文字在不超出 N 行时,全部展示,超出 N 行时截断,并显示 阅读更多 按钮,用于点击展开,展开后 阅读更多 按钮显示为 收起,点击后折叠文字。
如果我们能得知文字初始时是否处于折叠状态,那么就很好办了,通过修改 numberOfLines 的值 0 / N,来变更 展开与折叠 状态。
但是很可惜,RN 并没有提供 api 来告知文字是否处于折叠状态,因此只能另寻他法。
尝试一
参考 github 上的 issue 可以看到这个问题由来已久,有个解法是在 onLayout 时获取 Text 的高度, 对比理论折叠值( N * lineHeight),如果比理论折叠值大,则说明文字处于折叠状态。
上面这个方案我尝试了下,在 ios 上表现良好,但是在 安卓 机器上,onLayout 时获取到的 Text 高度却是折叠后的高度,这意味着上述方法将失效。
尝试二
接着我又注意到一个 api onTextLayout。onTextLayout 在 Text layout 的时候会被调用,可以获取到 lines,这是个对象数组,包含了每行文字的宽高等信息。我打算尝试一下这个 api 具体的表现如何。奇怪的是onTextLayout 虽然可以使用,但是 TS 的申明文件中 Text 组件却没有这个属性。再则在安卓机上 onTextLayout 返回的 lines 为未折叠时的行数,而在 ios 上则为折叠后的行数(最后一行将包括折叠后未能放下的文字)。看来仅通过 onTextLayout 也是无法判断到是否文字处于折叠状态。
尝试三
不过上述的尝试启发了我,如果我们可以得知文字在全部展开时的行数,如果超过 N ,则将 numberOfLines 设置为 N,否则为 0,此时的初始化状态必定是折叠的。那么问题是如何得知文字在全部展开时的行数?如果将文字 numberOfLines 初始值设置为 0,表现将会是一开始全部展开,之后再收起,显然不符合需求。那么我们需要在一个不会被看到的地方暗戳戳地渲染 numberOfLines 为 0 的 Text。这里我是构建了一个 height 为 0 的 ScrollView,在这个 ScrollView 中渲染 numberOfLines 为 0 的 Text,并且通过该 Text 的 onTextLayout 可以获取到文字的行数,并且与 N 进行对比,就可以知道文字是否可折叠。
最终代码实现如下:
/*
该组件的主要用途是测量具有 numberOfLines 属性的 Text 是否会被裁剪,以用于自定义展开更多按钮
原理是在一个高度为 0 的 ScrollView 中进行渲染,获取到渲染后的行数
*/
import React, { Children, isValidElement, cloneElement } from 'react';
import { View, ViewStyle, ScrollView } from 'react-native';
interface IProps {
// 最多行数,超出折叠
limitLineNumber: number;
children: React.ReactElement;
onReady: (truncated: boolean) => void;
style: ViewStyle;
}
const ReadMore = ({ children, limitLineNumber, onReady, style }: IProps) => {
/* 复制出一个不带有 numberOfLines 的 children */
const childrenTemp = Children.map(children, (child) => {
if (isValidElement(child)) {
return cloneElement(child as React.ReactElement<any>, {
numberOfLines: 0,
onTextLayout: (e: any) => {
// onTextLayout事件:ios 与 安卓 在文字折叠情况时会略微不同,体现在 lines 属性上
// ios 是返回折叠后的,而安卓则返回的是折叠之前的
// onTextLayout 在 0.59 的文档上并未体现,且 ts 类型也没有,但实际上是可以使用的
onReady(e.nativeEvent.lines?.length > limitLineNumber);
},
});
}
return child;
});
return (
<View style={style}>
<ScrollView style={{ height: 0 }}>{childrenTemp}</ScrollView>
{children}
</View>
);
};
export default ReadMore;
使用时:
const Demo = () => {
const onReadMoreReady = (truncated: boolean) => {
// 根据是否可折叠,判断展示按钮的文字等状态
};
return <ReadMore onReady={onReadMoreReady} limitLineNumber={2} >
<Text>
any content you need...
</Text>
</ReadMore>
}
⚠️ 以上全部基于 RN 0.59 版本