一款支持动态更新、Tag 标识系统的高性能文本渲染库
Texas 是一款支持图文混排的文本渲染库,目前支持两边对齐,并且单词支持断字,用户可以自由的自定义效果。
最大特点: 通过 Tag 系统精确标识每个文本片段,支持高效的动态内容更新,非常适合需要复杂交互的文本场景。
Texas 最大的优势在于支持动态更新文字内容,无需重新构建整个文档,只需增量更新即可。这使得它非常适合:
- 实时内容编辑器
- 聊天消息流
- 动态加载的长文本(如小说阅读器)
- 需要频繁更新部分内容的场景
Texas 提供了两级 Tag 系统,让你可以精确标识和操作文本的任意部分:
// 为整个段落设置唯一标识
builder.tag("paragraph_001");// 为每个单词或字符设置标识
span.tag(new SpanTag(sentenceId, wordContent, isNormalWord));Tag 系统的应用场景:
- ✅ 精确定位和高亮文本(如搜索结果高亮)
- ✅ 识别用户点击的具体单词或句子
- ✅ 实现复杂的交互逻辑(如单词翻译、笔记标注)
- ✅ 追踪文本来源和元数据
- ✅ 实现协同编辑中的冲突检测
自动优化中英文字体大小的视觉平衡,解决中英文混排时的突兀感:
// 使用中英文混合优化策略
Paragraph.Builder.newBuilder(option)
.typesetPolicy(Paragraph.TYPESET_POLICY_CJK_MIX_OPTIMIZATION)
.text("这是中文 English 混排")
.build();效果: 英文字符会根据中文字体自动调整基线和大小,确保视觉和谐统一。
采用 NLP 技术进行智能分词,按照语义对内容进行排版,防止内容被错误分割:
- ✅ 中文按词语语义分割,而非简单的字符分割
- ✅ 英文识别完整单词和短语
- ✅ 支持专业术语识别(如 "机器学习"、"San Francisco")
- ✅ 避免在不合适的位置换行
// 自动语义分词,"机器学习" 不会被拆分成 "机器" 和 "学习"
builder.stream(text, 0, text.length(), (token) -> {
// token 已经是语义完整的单元
return Paragraph.Span.obtain(token);
});符合专业出版标准的标点符号处理,当多个标点符号连续出现时,自动进行空间挤压:
示例:
)。- 右括号的右侧空间会被挤压,句号紧贴显示",- 引号和逗号之间自动调整间距、。- 顿号和句号优化显示
// 自动启用标点挤压优化
RenderOption option = texasView.createRendererOption();
option.setPunctuationCompressionEnabled(true);
texasView.refresh(option);效果对比:
❌ 普通排版:他说:"你好)。 空间浪费
✅ Texas排版:他说:"你好)。 紧凑美观
Texas 实现了底层绘制指令缓存,大幅提升渲染性能:
- 🚄 首次渲染:生成绘制指令并缓存
- ⚡ 后续刷新:直接使用缓存的指令,无需重新计算
- 💾 动态更新:只更新变化的部分,其他部分使用缓存
- 🎯 智能失效:只在内容或样式变化时更新缓存
性能提升:
- 滚动性能提升 300%+
- 刷新帧率达到 60fps+
- 内存占用优化 50%+
// 无需手动管理,Texas 自动处理缓存
texasView.setSource(documentSource); // 首次渲染,生成缓存
texasView.redraw(); // 后续刷新,使用缓存
texasView.scrollToPosition(position); // 滚动时使用缓存,流畅丝滑- 全平台的断字功能,比如 triangle 可以因为换行显示成 tri-angle
- 和 TeX 相同算法的两边对齐排版算法
- 自定义插入内容,你可以选择在渲染文章的时候插入视频、图片等任何你想要的视图
- 颜色
- 大小
- 粗细
- 字体
- 行间距
- 段落间距
- 段落装饰(Decoration)
- 段落高亮
在 Application 中初始化 Texas:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Texas.init(this);
}
}<me.chan.texas.renderer.TexasView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp" />TexasView texasView = findViewById(R.id.text);
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, @Nullable Document previousDocument) {
Paragraph.Builder builder = Paragraph.Builder.newBuilder(option);
// 设置段落 Tag
builder.tag("paragraph_001");
// 使用 stream API 自动分词并为每个单词设置 Tag
String text = "Hello world, this is Texas!";
builder.stream(text, 0, text.length(), (token) -> {
return Paragraph.Span.obtain(token)
.tag(new WordTag(
token.getCharSequence()
.subSequence(token.getStart(), token.getEnd())
.toString()
));
});
return new Document.Builder()
.addSegment(builder.build())
.build();
}
});
// 定义简单的 Tag 类
class WordTag {
public final String word;
public WordTag(String word) {
this.word = word;
}
}简单用法(无 Tag):
texasView.setSource(new TextDocumentSource("hello world"));在页面销毁时,释放渲染引擎资源:
@Override
protected void onDestroy() {
if (texasView != null) {
texasView.release();
}
super.onDestroy();
}Tag 系统是 Texas 的核心特性,它让你能够为文本的每个部分附加标识和元数据,从而实现精确的内容定位和交互。
建议创建自己的 Tag 类来存储文本元数据:
public class SpanTag {
public final String sentenceId; // 句子ID
public final String wordContent; // 单词内容
public final boolean isNormal; // 是否为普通单词
public SpanTag(String sentenceId, String wordContent, boolean isNormal) {
this.sentenceId = sentenceId;
this.wordContent = wordContent;
this.isNormal = isNormal;
}
}Paragraph.Builder builder = Paragraph.Builder.newBuilder(option);
// 为段落设置唯一标识
builder.tag("paragraph_A9127P127017");推荐使用 stream API,它会自动将文本分词,并为每个单词设置样式和 tag:
builder.stream(text, 0, text.length(), (token) -> {
// token 是词法引擎解析出的单词
// 为每个单词创建 Span 并设置 tag
return Paragraph.Span.obtain(token)
.tag(new SpanTag(
sentenceId,
token.getCharSequence().subSequence(token.getStart(), token.getEnd()).toString(),
token.getCategory() == Token.CATEGORY_NORMAL
))
.setForeground(new DotUnderLine(Color.RED)) // 设置样式
.setBackground(new RectGround(Color.YELLOW));
});Paragraph.Builder builder = Paragraph.Builder.newBuilder(option);
builder.newSpanBuilder()
.text("Hello")
.tag(new SpanTag("sent_001", "Hello", true))
.textColor(Color.BLUE)
.buildSpan();
builder.newSpanBuilder()
.text(" ")
.buildSpan();
builder.newSpanBuilder()
.text("World")
.tag(new SpanTag("sent_001", "World", true))
.textColor(Color.RED)
.buildSpan();texasView.setSpanTouchEventHandler(new SpanTouchEventHandler() {
@Override
public boolean isSpanClickable(Object tag) {
// 根据 tag 判断是否可点击
return tag != null;
}
@Override
public boolean applySpanClicked(@Nullable Object clickedTag, @Nullable Object otherTag) {
// 单击时,高亮相同 tag 的所有 Span
return clickedTag == otherTag;
}
@Override
public boolean applySpanLongClicked(@Nullable Object clickedTag, @Nullable Object otherTag) {
// 长按时,高亮同一句子的所有 Span
if (clickedTag instanceof SpanTag && otherTag instanceof SpanTag) {
SpanTag clicked = (SpanTag) clickedTag;
SpanTag other = (SpanTag) otherTag;
return clicked.sentenceId.equals(other.sentenceId);
}
return false;
}
});// 高亮特定句子
texasView.highlightParagraphs(new ParagraphPredicates() {
@Override
public boolean acceptSpan(@Nullable Object spanTag) {
if (!(spanTag instanceof SpanTag)) {
return false;
}
SpanTag tag = (SpanTag) spanTag;
return "A9127P127017S210459".equals(tag.sentenceId);
}
@Override
public boolean acceptParagraph(@Nullable Object paragraphTag) {
return "A9127P127017".equals(paragraphTag);
}
}, true, 0);texasView.setOnClickedListener(new TexasView.OnClickedListener() {
@Override
public void onSpanClicked(TexasView view, TouchEvent event, Object tag) {
if (tag instanceof SpanTag) {
SpanTag spanTag = (SpanTag) tag;
// 根据 tag 信息执行相应操作
String word = spanTag.wordContent;
String sentenceId = spanTag.sentenceId;
// 例如:显示单词翻译
showTranslation(word);
// 例如:跳转到相关内容
navigateToSentence(sentenceId);
}
}
@Override
public void onSegmentClicked(TexasView view, TouchEvent event, Object tag) {
// 获取 Paragraph 的 tag
String paragraphId = (String) tag;
}
// ... 其他回调
});Texas 支持高效的增量内容更新,无需重新渲染整个文档。
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, @Nullable Document previousDocument) {
// 基于之前的 document 进行增量更新
return new Document.Builder(previousDocument)
.addSegment(
Paragraph.Builder.newBuilder(option)
.tag("new_paragraph_001")
.text("这是新添加的内容")
.build()
)
.build();
}
});texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, @Nullable Document previousDocument) {
// previousDocument 为 null,表示重新构建
Document.Builder builder = new Document.Builder();
// 添加多个段落
for (int i = 0; i < paragraphs.size(); i++) {
String text = paragraphs.get(i);
builder.addSegment(
Paragraph.Builder.newBuilder(option)
.tag("paragraph_" + i)
.stream(text, 0, text.length(), (token) -> {
return Paragraph.Span.obtain(token)
.tag(new SpanTag("sent_" + i, token.toString(), true));
})
.build()
);
}
return builder.build();
}
});// 收到新消息时增量添加
void onNewMessage(Message message) {
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, @Nullable Document previousDocument) {
return new Document.Builder(previousDocument)
.addSegment(createMessageParagraph(option, message))
.build();
}
});
}// 动态高亮搜索关键词
void highlightSearchResults(String keyword) {
texasView.highlightParagraphs(new ParagraphPredicates() {
@Override
public boolean acceptSpan(@Nullable Object spanTag) {
if (spanTag instanceof SpanTag) {
SpanTag tag = (SpanTag) spanTag;
return tag.wordContent.contains(keyword);
}
return false;
}
@Override
public boolean acceptParagraph(@Nullable Object paragraphTag) {
return true; // 接受所有段落
}
}, Selection.Styles.create(Color.YELLOW, Color.BLACK));
}// 文本变化时局部更新
void onTextChanged(int paragraphIndex, String newText) {
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, @Nullable Document previousDocument) {
// 只更新变化的段落
Document.Builder builder = new Document.Builder();
for (int i = 0; i < document.getSegmentCount(); i++) {
if (i == paragraphIndex) {
// 重新构建变化的段落
builder.addSegment(createParagraph(option, newText, i));
} else {
// 复用未变化的段落
builder.addSegment(document.getSegment(i));
}
}
return builder.build();
}
});
}Texas 内置了多项专业级排版技术,这些特性让它在中文排版和性能优化方面远超其他文本渲染库。
在移动应用中,中英文混排是常见需求,但由于中英文字体设计差异,直接混排会产生视觉不协调:
- 英文字母通常比中文字符在视觉上显得更小
- 基线不统一,导致文字高低不齐
- 字重差异大,影响阅读体验
Texas 通过 TYPESET_POLICY_CJK_MIX_OPTIMIZATION 策略自动处理中英文混排:
Paragraph paragraph = Paragraph.Builder.newBuilder(option)
.typesetPolicy(Paragraph.TYPESET_POLICY_CJK_MIX_OPTIMIZATION)
.text("学习 Machine Learning 的最佳实践")
.build();自动优化内容:
- 字体大小调整 - 自动微调英文字符大小,与中文视觉平衡
- 基线对齐 - 统一中英文基线,确保文字对齐
- 字重匹配 - 根据中文字重调整英文显示
- 间距优化 - 优化中英文之间的间距
❌ 普通排版:
学习Machine Learning的最佳实践
(英文显得拥挤且突兀)
✅ Texas 优化后:
学习 Machine Learning 的最佳实践
(视觉和谐,间距舒适)
普通的文本渲染引擎通常按字符或简单规则分割文本,这会导致:
- ❌ 词语在不该断开的地方被拆分:"机/器/学/习"
- ❌ 专业术语被破坏:"人工/智能"、"San/Francisco"
- ❌ 影响阅读连贯性和理解
- ❌ 搜索和高亮功能受限
Texas 内置了基于 NLP 的智能分词系统:
builder.stream(text, 0, text.length(), (token) -> {
// token 是经过 NLP 分析的语义单元
// 例如:"机器学习" 会被识别为一个完整的 token
String word = token.getCharSequence()
.subSequence(token.getStart(), token.getEnd())
.toString();
// token.getCategory() 可以获取词性
// Token.CATEGORY_NORMAL - 普通单词
// Token.CATEGORY_PUNCTUATION - 标点符号
return Paragraph.Span.obtain(token)
.tag(new WordTag(word, token.getCategory()));
});| 类型 | 示例 | Texas 处理 |
|---|---|---|
| 中文词语 | "机器学习" | ✅ 识别为一个词 |
| 专业术语 | "深度神经网络" | ✅ 保持完整 |
| 英文短语 | "New York" | ✅ 识别为整体 |
| 混合内容 | "使用 TensorFlow 框架" | ✅ 正确分割 |
| 标点符号 | "你好,世界!" | ✅ 独立识别 |
1. 单词点击识别
texasView.setOnClickedListener(new TexasView.OnClickedListener() {
@Override
public void onSpanClicked(TexasView view, TouchEvent event, Object tag) {
// 点击 "机器学习" 中的任意字,都会识别为完整词语
if (tag instanceof WordTag) {
String word = ((WordTag) tag).word;
// word = "机器学习"(完整的词)
showDefinition(word);
}
}
});2. 搜索高亮
// 搜索 "机器学习",会精确高亮整个词组,而不是单个字
texasView.highlightParagraphs(new ParagraphPredicates() {
@Override
public boolean acceptSpan(@Nullable Object spanTag) {
if (spanTag instanceof WordTag) {
return ((WordTag) spanTag).word.equals("机器学习");
}
return false;
}
@Override
public boolean acceptParagraph(@Nullable Object paragraphTag) {
return true;
}
}, Selection.Styles.create(Color.YELLOW, Color.BLACK));3. 统计分析
// 准确统计文章中的词语数量(而非字符数)
Map<String, Integer> wordCount = new HashMap<>();
builder.stream(text, 0, text.length(), (token) -> {
if (token.getCategory() == Token.CATEGORY_NORMAL) {
String word = token.toString();
wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
}
return Paragraph.Span.obtain(token);
});在专业出版和印刷行业,标点符号的空间处理是重要的排版细节:
- 中文标点通常占一个全角字符的宽度
- 连续标点会产生过大的空白,影响美观
- 专业排版会对连续标点进行"挤压"处理
Texas 自动识别并优化以下标点组合:
| 标点组合 | 普通排版 | Texas 挤压后 | 说明 |
|---|---|---|---|
)。 |
)□。 |
)。 |
右括号右侧空间挤压 |
", |
"□, |
", |
引号和逗号间距优化 |
、。 |
、□。 |
、。 |
顿号和句号紧贴 |
!" |
!□" |
!" |
感叹号和引号优化 |
?) |
?□) |
?) |
问号和括号紧贴 |
(□ 表示多余的空白)
RenderOption option = texasView.createRendererOption();
// 标点挤压通常默认启用,可以手动控制
option.setPunctuationCompressionEnabled(true);
texasView.refresh(option);示例文本:
他问:"你去哪里?"我说:"去图书馆(新馆)。"
普通排版:
他问:"你去哪里? "我说:"去图书馆(新馆) 。 "
↑↑ 空白 ↑↑ 空白 ↑↑ 空白
Texas 优化后:
他问:"你去哪里?"我说:"去图书馆(新馆)。"
紧凑美观,符合专业出版标准
Texas 支持所有常见中文标点的智能挤压:
- 句末标点:
。!?… - 引号类:
""''》 - 括号类:
)』】〉 - 分隔符:
,、;:
传统文本渲染每一帧都需要:
- 计算文字布局
- 测量文字尺寸
- 生成绘制路径
- 执行绘制操作
这在长文本和频繁刷新场景下会导致:
- 🐌 滚动卡顿
- 🔥 CPU 占用高
- 🔋 耗电严重
Texas 实现了三层缓存机制:
┌─────────────────────────────────────┐
│ 应用层 │
│ texasView.redraw() │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ 渲染层缓存 │
│ • 布局缓存(Layout Cache) │
│ • 测量缓存(Measure Cache) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ 绘制指令缓存 │
│ • Canvas 操作序列 │
│ • 仅在内容变化时更新 │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ GPU 渲染 │
└─────────────────────────────────────┘
首次渲染:
// 第一次渲染,生成完整缓存
texasView.setSource(documentSource);
// 内部流程:
// 1. 解析 Document
// 2. 计算布局 → 缓存
// 3. 测量文字 → 缓存
// 4. 生成绘制指令 → 缓存
// 5. 执行绘制后续刷新:
// 滚动、重绘等操作
texasView.scrollToPosition(position);
texasView.redraw();
// 内部流程:
// 1. 检查缓存 ✅
// 2. 直接使用缓存的绘制指令
// 3. 执行绘制(跳过计算)增量更新:
// 只更新新增的段落
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, Document previousDocument) {
return new Document.Builder(previousDocument)
.addSegment(newParagraph) // 只添加新段落
.build();
}
});
// 内部流程:
// 1. 保留已有段落的缓存 ✅
// 2. 只为新段落生成缓存
// 3. 合并缓存
// 4. 执行绘制在一个 10000 字的长文本场景:
| 操作 | 普通 TextView | Texas(无缓存) | Texas(有缓存) |
|---|---|---|---|
| 首次渲染 | 150ms | 120ms | 120ms |
| 滚动刷新 | 16ms | 12ms | 3ms ⚡ |
| 重绘 | 16ms | 12ms | 2ms ⚡ |
| 增量更新 | 150ms | 50ms | 15ms ⚡ |
Texas 智能管理缓存,只在必要时更新:
会导致缓存失效的操作:
- ❌ 文本内容变化
- ❌ 文本样式变化(颜色、大小、字体)
- ❌ 布局参数变化(宽度、行距)
不会导致缓存失效的操作:
- ✅ 滚动位置变化
- ✅ 高亮选择变化
- ✅ View 重绘(onDraw)
- ✅ 屏幕旋转(如果宽度不变)
// Texas 自动管理缓存内存
// 当内存压力大时,会自动清理旧缓存
// 也可以手动清理(通常不需要)
Texas.clean(); // 清理所有缓存1. 适合使用缓存的场景:
- ✅ 长文本阅读器(频繁滚动)
- ✅ 聊天列表(消息内容不变)
- ✅ 新闻资讯(只读内容)
2. 缓存效果一般的场景:
⚠️ 实时编辑器(内容频繁变化)⚠️ 动画效果(样式频繁变化)
3. 优化技巧:
// 批量更新时,一次性完成
Document.Builder builder = new Document.Builder(previousDocument);
for (Paragraph p : newParagraphs) {
builder.addSegment(p);
}
texasView.setSource(builder.build()); // 一次更新,一次缓存
// 而不是:
for (Paragraph p : newParagraphs) {
texasView.addParagraph(p); // ❌ 多次更新,多次缓存重建
}结合所有高级特性,创建专业级的中文阅读器:
public class ProfessionalReaderActivity extends AppCompatActivity {
private TexasView texasView;
private void setupTexasView() {
texasView = findViewById(R.id.texas_view);
// 1. 创建优化的渲染选项
RenderOption option = texasView.createRendererOption();
// 启用标点挤压
option.setPunctuationCompressionEnabled(true);
// 设置合适的字体和大小
Typeface typeface = Typeface.createFromAsset(getAssets(), "SourceHanSerif.ttf");
option.setTypeface(typeface);
option.setTextSize(TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 18,
getResources().getDisplayMetrics()
));
// 设置行间距
option.setLineSpacingExtra(20);
texasView.refresh(option);
// 2. 加载文章,使用中英文混合优化 + NLP 分词
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption texasOption, Document previousDocument) {
Document.Builder builder = new Document.Builder();
for (int i = 0; i < chapters.size(); i++) {
String chapterText = chapters.get(i);
// 创建段落,启用所有优化
Paragraph paragraph = Paragraph.Builder.newBuilder(texasOption)
.tag("chapter_" + i)
.typesetPolicy(Paragraph.TYPESET_POLICY_CJK_MIX_OPTIMIZATION)
.stream(chapterText, 0, chapterText.length(), (token) -> {
// NLP 自动分词
return Paragraph.Span.obtain(token)
.tag(new WordTag(
"chapter_" + i,
token.toString(),
token.getCategory()
));
})
.build();
builder.addSegment(paragraph);
}
return builder.build();
}
});
// 3. 后续的滚动和刷新都会使用底层缓存,性能极佳
}
}实现效果:
- ✅ 中英文混排视觉和谐
- ✅ 标点符号显示专业
- ✅ 单词点击识别准确
- ✅ 滚动流畅(60fps+)
- ✅ 内存占用合理
- ✅ 符合专业出版标准
| 场景 | 推荐用法 |
|---|---|
| 点击事件(只关心部分回调) | OnClickedListenerAdapter |
| 只按 Span 筛选高亮 | AbstractParagraphPredicates 或 ParagraphPredicates.spanOnly() |
| 高亮并滚动 | HighlightOptions.builder(predicates).scrollTo(true).build() |
| 获取/恢复滚动位置 | getCurrentPosition() / scrollToPosition(position) |
| 按 segment 滚动 | scrollToSegment(segment) |
// 设置整个渲染窗口的 padding (left, top, right, bottom)
texasView.setRendererPadding(30, 10, 30, 10);Segment 装饰用于设置每个 Segment(段落、图片等)周围的空白间距:
final int paddingHorizontal = 20;
final int paddingVertical = 20;
texasView.setSegmentDecoration((index, count, segment, document, outRect) -> {
if (segment instanceof Figure) {
// 图片类型的 Segment
outRect.set(0, index == 0 ? 0 : paddingVertical, 0, index == count - 1 ? 0 : paddingVertical);
} else {
// 文本类型的 Segment
outRect.set(paddingHorizontal, index == 0 ? 0 : paddingVertical, paddingHorizontal, index == count - 1 ? 0 : paddingVertical);
}
});Drawable drawable = ContextCompat.getDrawable(this, R.drawable.scrollbar_thumb_demo);
texasView.setScrollBarDrawable(drawable);
// 获取当前滚动条 Drawable
Drawable current = texasView.getScrollBarDrawable();texasView.setSpanTouchEventHandler(new SpanTouchEventHandler() {
@Override
public boolean isSpanClickable(Object tag) {
// 判断该 Span 是否可点击
return tag != null;
}
@Override
public boolean applySpanClicked(@Nullable Object clickedTag, @Nullable Object otherTag) {
// 单击谓词:判断单击时哪些单词要被高亮
return clickedTag == otherTag;
}
@Override
public boolean applySpanLongClicked(@Nullable Object clickedTag, @Nullable Object otherTag) {
// 长按谓词:判断长按时哪些内容要被高亮
if (otherTag == null) {
return true;
}
if (clickedTag instanceof BookSource.SpanTag && otherTag instanceof BookSource.SpanTag) {
BookSource.SpanTag lhs = (BookSource.SpanTag) clickedTag;
BookSource.SpanTag rhs = (BookSource.SpanTag) otherTag;
return TexasUtils.equals(lhs.sentId, rhs.sentId);
}
return false;
}
});使用 OnClickedListenerAdapter 只需重写关心的事件,无需实现所有回调:
texasView.setOnClickedListener(new OnClickedListenerAdapter() {
@Override
public void onSpanClicked(TexasView view, TouchEvent event, Object tag) {
Toast.makeText(context, "点击了 Span", Toast.LENGTH_SHORT).show();
}
@Override
public void onSegmentClicked(TexasView view, TouchEvent event, Object tag) {
Toast.makeText(context, "点击了 Segment", Toast.LENGTH_SHORT).show();
}
});完整实现所有回调时可直接使用 TexasView.OnClickedListener。
texasView.setOnDragSelectListener(new TexasView.OnDragSelectListener() {
@Override
public void onDragStart(TexasView view, TouchEvent event) {
// 开始拖拽选择
}
@Override
public void onDragEnd(TexasView view, TouchEvent event) {
// 结束拖拽选择
}
@Override
public void onDragDismiss(TexasView view) {
// 取消拖拽选择
}
});// 创建渲染选项
RenderOption renderOption = texasView.createRendererOption();
// 设置文本大小
renderOption.setTextSize(TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 18,
getResources().getDisplayMetrics()
));
// 设置文本颜色
renderOption.setTextColor(Color.BLACK);
// 设置行间距
renderOption.setLineSpacingExtra(200);
// 设置断字策略
renderOption.setBreakStrategy(BreakStrategy.BALANCED); // 或 BreakStrategy.SIMPLE
// 设置字体
Typeface typeface = Typeface.createFromAsset(getAssets(), "opposans_r.ttf");
renderOption.setTypeface(typeface);
// 设置调试模式
renderOption.setDebugEnable(true);
// 设置是否绘制表情符号选择
renderOption.setDrawEmoticonSelection(false);
// 应用渲染选项
texasView.refresh(renderOption);只根据 Span 筛选时,可使用 AbstractParagraphPredicates 或 ParagraphPredicates.spanOnly():
// 方式一:AbstractParagraphPredicates,只需实现 acceptSpan
texasView.highlightParagraphs(new AbstractParagraphPredicates() {
@Override
public boolean acceptSpan(@Nullable Object spanTag) {
if (!(spanTag instanceof BookSource.SpanTag)) return false;
return "A9127P127017S210459".equals(((BookSource.SpanTag) spanTag).sentId);
}
}, true, 0);
// 方式二:ParagraphPredicates.spanOnly,Lambda 写法
texasView.highlightParagraphs(ParagraphPredicates.spanOnly(spanTag ->
spanTag instanceof BookSource.SpanTag
&& "A9127P127017S210459".equals(((BookSource.SpanTag) spanTag).sentId)
), true, 0);
// 方式三:使用 HighlightOptions 配置对象
ParagraphPredicates predicates = ParagraphPredicates.spanOnly(spanTag -> /* ... */);
texasView.highlightParagraphs(HighlightOptions.builder(predicates)
.scrollTo(true)
.scrollOffset(0)
.styles(Selection.Styles.create(Color.YELLOW, Color.BLACK))
.build());完整控制 Span 和 Paragraph 时使用 ParagraphPredicates:
texasView.highlightParagraphs(new ParagraphPredicates() {
@Override
public boolean acceptSpan(@Nullable Object spanTag) {
if (!(spanTag instanceof BookSource.SpanTag)) return false;
return "A9127P127017S210459".equals(((BookSource.SpanTag) spanTag).sentId);
}
@Override
public boolean acceptParagraph(@Nullable Object paragraphTag) {
return "A9127P127017".equals(paragraphTag);
}
}, true, 0);// 创建选择并设置样式
Selection selection = texasView.highlightParagraphs(new ParagraphPredicates() {
@Override
public boolean acceptSpan(@Nullable Object spanTag) {
return true;
}
@Override
public boolean acceptParagraph(@Nullable Object paragraphTag) {
return "A9127P126972".equals(paragraphTag);
}
}, Selection.Styles.create(Color.BLUE, Color.RED));
if (selection != null) {
// 创建动画
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1f);
valueAnimator.setDuration(3000);
valueAnimator.setRepeatCount(3);
selection.startAnimator(valueAnimator, new Selection.SelectionAnimatorListener() {
@Override
protected void onUpdate(ValueAnimator animation, Selection.Styles styles) {
int backgroundColor = (int) styles.getBackgroundColor();
int textColor = styles.getTextColor();
float v = (float) animation.getAnimatedValue();
// 更新透明度
styles.setTextColor(Color.argb(
(int) (255 * v),
Color.red(textColor),
Color.green(textColor),
Color.blue(textColor)
));
styles.setBackgroundColor(Color.argb(
(int) (255 * v),
Color.red(backgroundColor),
Color.green(backgroundColor),
Color.blue(backgroundColor)
));
}
@Override
protected void onAnimationEnd(Animator animation, boolean isReverse, Selection.Styles styles) {
// 动画结束后清除高亮
selection.clear();
}
});
}// 滚动到指定 segment(position 为 segment 索引)
texasView.scrollToPosition(0); // 完整写法,支持 smooth、offset 参数
// 获取当前滚动位置,用于保存阅读进度
int position = texasView.getCurrentPosition();
// 根据 segment 对象滚动
texasView.scrollToSegment(paragraph);// 重新绘制视图
texasView.redraw();// 清理 Texas 缓存
Texas.clean();@Override
protected void onDestroy() {
if (texasView != null) {
texasView.release();
}
super.onDestroy();
}public class BookSource extends TexasView.DocumentSource {
private Context context;
private String bookName;
public BookSource(Context context, String bookName) {
this.context = context;
this.bookName = bookName;
}
@Override
protected Document onRead(TexasOption option, @Nullable Document previousDocument) {
try {
XmlPullParser xmlPullParser = Xml.newPullParser();
xmlPullParser.setInput(new InputStreamReader(
context.getResources().getAssets().open(bookName)
));
return parse(xmlPullParser, option);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
private Document parse(XmlPullParser parser, TexasOption texasOption)
throws IOException, XmlPullParserException {
Document.Builder documentBuilder = new Document.Builder();
while (parser.next() != XmlPullParser.END_TAG) {
int eventType = parser.getEventType();
if (eventType == XmlPullParser.END_DOCUMENT) {
break;
} else if (eventType != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if ("paragraph".equals(name)) {
String paragraphId = parser.getAttributeValue(null, "id");
String sentenceId = parser.getAttributeValue(null, "sentId");
String text = parser.nextText();
// 创建段落并设置 Tag
Paragraph paragraph = createParagraphWithTag(
texasOption,
text,
paragraphId,
sentenceId
);
documentBuilder.addSegment(paragraph);
}
}
return documentBuilder.build();
}
private Paragraph createParagraphWithTag(
TexasOption option,
String text,
String paragraphId,
String sentenceId) {
Paragraph.Builder builder = Paragraph.Builder.newBuilder(option);
// 设置段落 Tag
builder.tag(paragraphId);
// 使用 stream API 为每个单词设置 Tag
builder.stream(text, 0, text.length(), (token) -> {
return Paragraph.Span.obtain(token)
.tag(new SpanTag(
sentenceId,
token.getCharSequence()
.subSequence(token.getStart(), token.getEnd())
.toString(),
token.getCategory() == Token.CATEGORY_NORMAL
))
.setForeground(new DotUnderLine(Color.RED));
});
return builder.build();
}
// 自定义 Span Tag 类
public static class SpanTag {
public final String sentId;
public final String word;
public final boolean isNormalWord;
public SpanTag(String sentId, String word, boolean isNormalWord) {
this.sentId = sentId;
this.word = word;
this.isNormalWord = isNormalWord;
}
}
}// 初始化时加载
BookSource source = new BookSource(
this,
texasView,
Paragraph.TYPESET_POLICY_CJK_MIX_OPTIMIZATION,
"book.xml"
);
texasView.setSource(source);在 Texas 内部,文本引擎处理的最小单元叫 Segment,一个个 Segment 由上而下排列组成了渲染内容。拿文章举例,我们可以设置文章 Title 为一个 Segment,顶部的插图为一个 Segment,正文由很多段落 Segment 组成。每个 Segment 都是占满文本引擎窗口 TexasView 的宽度进行显示。
/**
* 渲染的最小单元
*/
public class Segment extends DefaultRecyclable {
/**
* @param segmentSpace segment 的垂直距离
* @return 距离上一个 segment 的距离
*/
public float getTopMargin(float segmentSpace) {
return 0;
}
/**
* @param segmentSpace segment 的垂直距离
* @return 距离下一个 segment 的距离
*/
public float getBottomMargin(float segmentSpace) {
return segmentSpace;
}
}- Paragraph - 用来显示文本,文本可以进行高亮、两边对齐等操作
- Figure - 用来显示插图,该 Segment 优化了图片显示,减少了因图片渲染导致的界面抖动
- ViewSegment - 用来显示用户自定义内容
new ViewSegment(R.layout.test_header) {
@Override
protected void onRender(View view) {
// 在这里设置视图内容
}
@Override
public float getTopMargin(float segmentSpace) {
return 0;
}
@Override
public float getBottomMargin(float segmentSpace) {
return 0;
}
}从新版本开始,ViewSegment 支持将其内部的 ParagraphView 参与到 TexasView 的自由选中功能中。这意味着:
- ✅ 自定义布局中的 ParagraphView 可以与其他文本段落一起被选中
- ✅ 无需手动设置 ParagraphView 的数据源
- ✅ 无需手动处理 ParagraphView 的点击事件
- ✅ ParagraphView 的渲染样式可以自动跟随 TexasView
使用方式:
public class ParallelViewSegment extends ViewSegment {
private final String mText;
public ParallelViewSegment(Paragraph paragraph, String text) {
super(new Args(R.layout.item_parallel)
.disableReuse(true)
// 将布局中的 ParagraphView 添加到自由选中系统
// 参数1:ParagraphView 的 id
// 参数2:要渲染的 Paragraph 数据
.addSelectionProvider(R.id.paragraph_view, paragraph));
mText = text;
}
@Override
protected void onRender(View view) {
// 只需要处理其他视图,ParagraphView 会自动渲染
TextView textView = view.findViewById(R.id.text_view);
textView.setText(mText);
}
}布局文件示例(item_parallel.xml):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 左侧:自定义文本 -->
<TextView
android:id="@+id/text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<!-- 右侧:可参与自由选中的 ParagraphView -->
<me.chan.texas.renderer.ui.text.ParagraphView
android:id="@+id/paragraph_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:me_chan_texas_ParagraphView_overrideStyles="true" />
</LinearLayout>高级用法 - 多个 ParagraphView:
public ParallelViewSegment(Paragraph paragraph1, Paragraph paragraph2, String text) {
super(new Args(R.layout.item_multi_parallel)
.disableReuse(true)
// 可以添加多个 ParagraphView
.addSelectionProvider(R.id.paragraph_left, paragraph1)
.addSelectionProvider(R.id.paragraph_right, paragraph2));
mText = text;
}注意事项:
-
样式同步:在 ParagraphView 的 XML 属性中设置
me_chan_texas_ParagraphView_overrideStyles="true",可以让 ParagraphView 的渲染样式跟随 TexasView 的全局配置 -
数据源管理:使用
addSelectionProvider后,不要再手动调用 ParagraphView 的setParagraph()方法,否则会导致数据不一致 -
选中效果:添加到 SelectionProvider 的 ParagraphView 会自动参与以下功能:
- 文本选中和拖拽选择
- 高亮显示
- 单词点击识别
- 长按选择
实际应用场景:
- 📖 双语对照阅读器:左侧显示中文,右侧显示英文,两边都可以自由选中
- 📝 注释文本:原文和注释并排显示,可以分别选择
- 🎓 教学材料:题目和答案对照显示,支持独立选择
- 📚 诗歌排版:左右对齐的对偶句,可以分别交互
完整示例 - 双语对照阅读:
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, @Nullable Document previousDocument) {
Document.Builder builder = new Document.Builder();
for (int i = 0; i < bilingualTexts.size(); i++) {
BilingualText text = bilingualTexts.get(i);
// 创建中文段落
Paragraph chineseParagraph = Paragraph.Builder.newBuilder(option)
.tag("cn_" + i)
.stream(text.chinese, 0, text.chinese.length(),
token -> Paragraph.Span.obtain(token).tag(new WordTag(token.toString())))
.build();
// 创建英文段落
Paragraph englishParagraph = Paragraph.Builder.newBuilder(option)
.tag("en_" + i)
.stream(text.english, 0, text.english.length(),
token -> Paragraph.Span.obtain(token).tag(new WordTag(token.toString())))
.build();
// 创建双语对照的 ViewSegment
builder.addSegment(new ParallelViewSegment(
chineseParagraph, // 中文段落
englishParagraph, // 英文段落
"第 " + (i + 1) + " 段" // 段落标题
));
}
return builder.build();
}
});通过 SelectionProvider 机制,你可以轻松实现复杂的自定义布局,同时保留 Texas 强大的文本交互能力。
- 为 Tag 类创建明确的数据结构
public class SpanTag {
public final String id; // 唯一标识
public final String content; // 文本内容
public final String type; // 类型(如:word, punctuation)
public final Map<String, Object> metadata; // 扩展元数据
}- 使用 stream API 处理文本
// 推荐:自动分词,高效处理
builder.stream(text, 0, text.length(), (token) -> {
return Paragraph.Span.obtain(token).tag(createTag(token));
});- Tag 保持唯一性和一致性
// 使用有意义的 ID 格式
String paragraphId = "article_" + articleId + "_paragraph_" + index;
String spanId = paragraphId + "_span_" + spanIndex;- 在 Tag 中存储必要的业务信息
public class SpanTag {
public final String wordId;
public final String translation; // 翻译
public final int difficulty; // 难度等级
public final boolean isBookmarked; // 是否收藏
}- 不要在 Tag 中存储大量数据
// 错误:Tag 中存储大对象
class SpanTag {
public Bitmap image; // ❌ 不要存储图片
public List<String> largeList; // ❌ 不要存储大集合
}- 不要使用可变的 Tag 对象
// 错误:Tag 应该是不可变的
class SpanTag {
public String id;
public void setId(String id) { this.id = id; } // ❌
}
// 正确:使用 final 字段
class SpanTag {
public final String id;
public SpanTag(String id) { this.id = id; } // ✅
}- 不要在高频回调中进行重度计算
@Override
public boolean applySpanClicked(Object clickedTag, Object otherTag) {
// ❌ 不要在这里进行数据库查询或网络请求
// ✅ 应该只做简单的比较操作
return clickedTag == otherTag;
}- 增量更新优于全量更新
// 推荐:只更新变化的部分
Document.Builder(previousDocument).addSegment(newParagraph).build()
// 避免:频繁全量重建
Document.Builder().addAllSegments(allParagraphs).build()- 批量更新合并操作
// 推荐:批量添加多个段落
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, Document previousDocument) {
Document.Builder builder = new Document.Builder(previousDocument);
for (Paragraph p : newParagraphs) {
builder.addSegment(p);
}
return builder.build();
}
});- 使用对象池减少 GC 压力
// Texas 内部使用了对象池,Span 会自动回收
// 不需要手动管理,但要注意在使用完后不要继续引用
Paragraph.Span span = Paragraph.Span.obtain(token);
// 使用 span...
// 不要在回调外继续持有 span 引用// ✅ 始终使用中英文混合优化策略
Paragraph.Builder.newBuilder(option)
.typesetPolicy(Paragraph.TYPESET_POLICY_CJK_MIX_OPTIMIZATION)
.text("中英文混排内容")
.build();
// ✅ 选择支持中英文的字体
Typeface typeface = Typeface.createFromAsset(
assets,
"SourceHanSans.ttf" // 思源黑体等支持中英文的字体
);
option.setTypeface(typeface);
// ❌ 不要使用纯英文字体处理中文
Typeface roboto = Typeface.create("Roboto", Typeface.NORMAL); // ❌// ✅ 推荐:使用 stream API 自动分词
builder.stream(text, 0, text.length(), (token) -> {
// token 已经是完整的词语
return Paragraph.Span.obtain(token)
.tag(new WordTag(token.toString()));
});
// ❌ 不推荐:手动逐字处理
for (int i = 0; i < text.length(); i++) {
builder.newSpanBuilder()
.text(text.substring(i, i + 1)) // ❌ 失去语义信息
.buildSpan();
}
// ✅ 利用分词结果实现精确交互
texasView.setOnClickedListener(new TexasView.OnClickedListener() {
@Override
public void onSpanClicked(TexasView view, TouchEvent event, Object tag) {
if (tag instanceof WordTag) {
String word = ((WordTag) tag).word;
// word 是完整的词语,可以直接查词典
showDefinition(word);
}
}
});// ✅ 对于中文内容,建议启用标点挤压
RenderOption option = texasView.createRendererOption();
option.setPunctuationCompressionEnabled(true);
texasView.refresh(option);
// 适用场景:
// ✅ 中文小说、文章
// ✅ 新闻资讯
// ✅ 聊天消息(中文为主)
// 不适用场景:
// ⚠️ 纯英文内容(英文标点不需要挤压)
// ⚠️ 代码显示(需要精确对齐)// ✅ 避免频繁改变影响缓存的属性
RenderOption option = texasView.createRendererOption();
option.setTextSize(textSize);
option.setTextColor(textColor);
option.setTypeface(typeface);
texasView.refresh(option); // 一次性设置
// ❌ 不要频繁修改会导致缓存失效的属性
for (int i = 0; i < 100; i++) {
option.setTextSize(textSize + i); // ❌ 每次都会重建缓存
texasView.refresh(option);
}
// ✅ 高亮不影响缓存,可以频繁使用
texasView.highlightParagraphs(predicates, styles); // 不影响缓存
// ✅ 滚动不影响缓存,性能极佳
texasView.scrollToPosition(position); // 使用缓存,流畅public class OptimizedReaderActivity extends AppCompatActivity {
private TexasView texasView;
private void setupOptimizedReader() {
texasView = findViewById(R.id.texas_view);
// 1. 配置渲染选项(一次性设置,避免频繁修改)
RenderOption option = texasView.createRendererOption();
// 中文优化
option.setPunctuationCompressionEnabled(true); // 标点挤压
// 字体选择
Typeface typeface = Typeface.createFromAsset(
getAssets(),
"NotoSerifCJK.ttf" // 使用支持中英文的衬线字体
);
option.setTypeface(typeface);
// 字号和行距
option.setTextSize(TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 18,
getResources().getDisplayMetrics()
));
option.setLineSpacingExtra(20);
texasView.refresh(option);
// 2. 加载内容(使用所有优化特性)
loadChapterOptimized();
// 3. 设置交互(利用 Tag 和 NLP 分词)
setupWordInteraction();
}
private void loadChapterOptimized() {
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, Document prev) {
Document.Builder builder = new Document.Builder();
for (int i = 0; i < paragraphs.size(); i++) {
String text = paragraphs.get(i);
Paragraph paragraph = Paragraph.Builder.newBuilder(option)
.tag("para_" + i)
// 启用中英文混合优化
.typesetPolicy(Paragraph.TYPESET_POLICY_CJK_MIX_OPTIMIZATION)
// 使用 stream API,自动 NLP 分词
.stream(text, 0, text.length(), (token) -> {
return Paragraph.Span.obtain(token)
.tag(new WordTag(
i, // 段落索引
token.toString(), // 完整词语
token.getCategory() // 词性
));
})
.build();
builder.addSegment(paragraph);
}
return builder.build();
}
});
}
private void setupWordInteraction() {
// 点击识别(基于 NLP 分词的完整词语)
texasView.setOnClickedListener(new TexasView.OnClickedListener() {
@Override
public void onSpanClicked(TexasView view, TouchEvent event, Object tag) {
if (tag instanceof WordTag) {
WordTag wordTag = (WordTag) tag;
String word = wordTag.word; // 完整的词语
// 根据词性决定操作
if (wordTag.category == Token.CATEGORY_NORMAL) {
// 普通词语:显示释义
showDefinition(word);
}
}
}
});
// 点击谓词(高亮同一段落的同一词语)
texasView.setSpanTouchEventHandler(new SpanTouchEventHandler() {
@Override
public boolean isSpanClickable(Object tag) {
return tag instanceof WordTag &&
((WordTag) tag).category == Token.CATEGORY_NORMAL;
}
@Override
public boolean applySpanClicked(Object clickedTag, Object otherTag) {
if (clickedTag instanceof WordTag && otherTag instanceof WordTag) {
WordTag clicked = (WordTag) clickedTag;
WordTag other = (WordTag) otherTag;
// 高亮相同的词语
return clicked.word.equals(other.word) &&
clicked.paragraphIndex == other.paragraphIndex;
}
return false;
}
});
}
// 增量添加新段落(利用缓存)
private void appendNewParagraph(String newText) {
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, Document prev) {
int newIndex = prev.getSegmentCount();
Paragraph newParagraph = Paragraph.Builder.newBuilder(option)
.tag("para_" + newIndex)
.typesetPolicy(Paragraph.TYPESET_POLICY_CJK_MIX_OPTIMIZATION)
.stream(newText, 0, newText.length(), (token) -> {
return Paragraph.Span.obtain(token)
.tag(new WordTag(newIndex, token.toString(), token.getCategory()));
})
.build();
// 增量更新,保留已有段落的缓存
return new Document.Builder(prev)
.addSegment(newParagraph)
.build();
}
});
}
// 自定义 WordTag
static class WordTag {
final int paragraphIndex;
final String word;
final int category;
WordTag(int paragraphIndex, String word, int category) {
this.paragraphIndex = paragraphIndex;
this.word = word;
this.category = category;
}
}
}这个示例展示了如何综合运用 Texas 的所有优化特性:
- ✅ 中英文混合优化 - 视觉和谐
- ✅ NLP 语义分词 - 精确交互
- ✅ 标点符号挤压 - 专业排版
- ✅ 底层指令级缓存 - 极致性能
- ✅ Tag 标识系统 - 灵活控制
- ✅ 增量更新 - 高效刷新
public class ReaderActivity extends AppCompatActivity {
private TexasView texasView;
private BookManager bookManager;
// 1. 加载章节内容
private void loadChapter(int chapterId) {
Chapter chapter = bookManager.getChapter(chapterId);
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption option, Document prev) {
return createChapterDocument(option, chapter);
}
});
}
// 2. 使用 Tag 追踪段落位置
private Document createChapterDocument(TexasOption option, Chapter chapter) {
Document.Builder builder = new Document.Builder();
for (int i = 0; i < chapter.paragraphs.size(); i++) {
String text = chapter.paragraphs.get(i);
Paragraph paragraph = createParagraphWithTag(
option,
text,
chapter.id,
i
);
builder.addSegment(paragraph);
}
return builder.build();
}
// 3. 处理单词点击(如:查词)
private void setupWordClickListener() {
texasView.setOnClickedListener(new TexasView.OnClickedListener() {
@Override
public void onSpanClicked(TexasView view, TouchEvent event, Object tag) {
if (tag instanceof WordTag) {
WordTag wordTag = (WordTag) tag;
showDictionary(wordTag.word);
// 记录阅读进度
bookManager.saveProgress(wordTag.chapterId, wordTag.paragraphIndex);
}
}
});
}
// 4. 实现搜索高亮
private void searchAndHighlight(String keyword) {
texasView.highlightParagraphs(new ParagraphPredicates() {
@Override
public boolean acceptSpan(@Nullable Object spanTag) {
if (spanTag instanceof WordTag) {
return ((WordTag) spanTag).word.contains(keyword);
}
return false;
}
@Override
public boolean acceptParagraph(@Nullable Object paragraphTag) {
return true;
}
}, Selection.Styles.create(Color.YELLOW, Color.BLACK));
}
}A: Tag 系统的性能开销非常小,只是简单的对象引用。在 SpanTouchEventHandler 的回调中,需要确保比较操作足够快(通常是简单的对象比较或字符串比较)。
A: 建议在 Tag 中只存储 ID,通过 ID 从业务层获取完整数据:
class SpanTag {
public final String wordId; // 只存储 ID
}
// 点击时通过 ID 查询完整数据
void onSpanClicked(Object tag) {
if (tag instanceof SpanTag) {
String wordId = ((SpanTag) tag).wordId;
WordData data = wordRepository.getById(wordId); // 从仓库获取数据
}
}A: Texas 会尽量保持滚动位置,但在大规模内容替换时可能需要手动保存和恢复位置:
// 保存位置
int position = texasView.getCurrentPosition();
// 更新内容
texasView.setSource(newSource);
// 恢复位置
texasView.scrollToPosition(position);A: 不可以直接修改,需要重新设置数据源。但由于支持增量更新,性能影响很小。
Texas 的核心价值在于:
-
🏷️ Tag 标识系统
- 为每个文本片段提供精确的标识
- 实现复杂的交互逻辑(点击、高亮、追踪)
- 支持任意业务元数据附加
-
🚀 动态内容更新
- 支持高效的增量内容更新
- 无需重新渲染整个文档
- 智能缓存机制,性能提升 300%+
-
📐 专业级排版引擎
- 中英文混合优化 - 自动调整字体大小和基线,视觉和谐
- NLP 语义分词 - 按语义分割文本,保持词语完整性
- 标点符号挤压 - 符合专业出版标准的标点处理
- 底层指令级缓存 - 滚动流畅度达 60fps+
- ✅ TeX 级别的两边对齐算法
- ✅ 全平台断字功能(hyphenation)
- ✅ 自定义 Segment(图片、视频、自定义 View)
- ✅ 丰富的文本样式和段落装饰
- ✅ 高性能的长文本渲染
Texas 特别适合以下应用类型:
| 应用类型 | 核心价值 |
|---|---|
| 📖 阅读器应用 | 专业排版 + 单词点击 + 笔记标注 + 流畅滚动 |
| 💬 聊天应用 | 动态更新 + 表情渲染 + @高亮 + 性能优化 |
| ✏️ 文本编辑器 | 实时编辑 + 语法高亮 + 协同编辑 + Tag 追踪 |
| 🔍 搜索应用 | 关键词高亮 + 语义分词 + 精确定位 |
| 📚 教育应用 | 单词学习 + 语法分析 + 互动练习 + 中英混排 |
| 📰 新闻资讯 | 图文混排 + 专业排版 + 缓存优化 + 阅读体验 |
| 特性 | Android TextView | WebView | Texas |
|---|---|---|---|
| 中英文混合优化 | ❌ | ✅ 自动优化 | |
| NLP 语义分词 | ❌ | ❌ | ✅ 内置 |
| 标点挤压 | ❌ | ❌ | ✅ 自动 |
| 底层缓存 | ❌ | ✅ 指令级 | |
| Tag 系统 | ❌ | ✅ 原生 | |
| 动态更新 | ✅ 增量 | ||
| 渲染性能 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 内存占用 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 自定义能力 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Texas 提供了完整的解决方案,让你轻松构建专业级的文本应用:
// 1. 初始化
Texas.init(context);
// 2. 配置 TexasView
TexasView texasView = findViewById(R.id.texas_view);
// 3. 启用所有优化特性
RenderOption option = texasView.createRendererOption();
option.setPunctuationCompressionEnabled(true); // 标点挤压
texasView.refresh(option);
// 4. 加载内容(自动启用 NLP 分词、中英混排优化、缓存)
texasView.setSource(new TexasView.DocumentSource() {
@Override
protected Document onRead(TexasOption opt, Document prev) {
return Paragraph.Builder.newBuilder(opt)
.tag("para_1")
.typesetPolicy(Paragraph.TYPESET_POLICY_CJK_MIX_OPTIMIZATION)
.stream(text, 0, text.length(), token ->
Paragraph.Span.obtain(token).tag(new WordTag(token.toString()))
)
.build();
}
});
// 5. 享受专业的排版效果和极致的性能!Texas - 让文本渲染更专业、更强大、更高效! 🎉
