Flutter 文字环绕
文字环绕
需求
最近接到一个需求,类似于文字环绕,标题最多两行,超出省略,标题后面可以添加标签。效果如下:
富文本不能控制省略和折行,Flutter 提供了 TextPainter
可以实现。
分析
标签有文字和颜色两个属性,个数不定:
class Tag {
/// 标签文本
final String label;
/// 标签背景颜色
final Color color;
Tag({required this.label, required this.color});
}
标题最大行数可变,可能明天产品要最多显示三行;
文本样式可变;
先创建出来对应的Widget
:
class TagTitle extends StatefulWidget {
const TagTitle(
this.text, {
Key? key,
required this.tags,
this.maxLines = 2,
this.style = const TextStyle(color: Colors.black, fontSize: 16),
}) : super(key: key);
final String text;
final int maxLines;
final TextStyle style;
final List<Tag> tags;
}
实现
标题文字和标签文字有两种显示情况:
- 超出最大行数;
- 未超出最大行数;
先假设第一种情况,因为标签前后有间距,所以每个标签前后补一个空格,再把标题和文字拼接创建对应的TextSpan
:
tagTexts = widget.tags.fold<String>(
' ', (previousValue, element) => '$previousValue${element.label} ');
_allSp = TextSpan(
text: '${widget.text}$tagTexts',
style: widget.style,
);
要绘制标题、省略号、标签、都需要TextSpan
,所以一并创建出来,当然还有最重要的TextPainter
:
// 标签
final tagsSp = TextSpan(
text: tagTexts,
style: widget.style,
);
// 省略号
final ellipsizeTextSp = TextSpan(
text: ellipsizeText,
style: widget.style,
);
// 标题
final textSp = TextSpan(
text: widget.text,
style: widget.style,
);
final textPainter = TextPainter(
text: tagsSp,
textDirection: TextDirection.ltr,
maxLines: widget.maxLines,
)..layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
拿到标签、省略号、标题的尺寸:
final tagsSize = textPainter.size;
textPainter.text = ellipsizeTextSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final ellipsizeSize = textPainter.size;
textPainter.text = textSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final textSize = textPainter.size;
算出标题超出最大长度的位置:
textPainter.text = _allSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final pos = textPainter.getPositionForOffset(Offset(
textSize.width - tagsSize.width - ellipsizeSize.width,
textSize.height,
));
final endIndex = textPainter.getOffsetBefore(pos.offset);
如果超出的话,文字显示区域的宽度减去标签宽度减去省略号宽度,剩下的位置就是标题最大宽度偏移量,根据偏移量得到此位置的文字位置下标。
textPainter.didExceedMaxLines
返回的是是否超出最大长度,也就是一开始分析的两种情况的哪一种,如果超出,就根据上面计算出来的下标截取标题文字,添加省略号,然后添加上标签;否则,直接显示标题文本和标签:
TextSpan textSpan;
if (textPainter.didExceedMaxLines) {
textSpan = TextSpan(
style: widget.style,
text: widget.text.substring(0, endIndex) + ellipsizeText,
children: _getWidgetSpan(),
);
} else {
textSpan = TextSpan(
style: widget.style,
text: widget.text,
children: _getWidgetSpan(),
);
}
return RichText(
text: textSpan,
overflow: TextOverflow.ellipsis,
maxLines: widget.maxLines,
);
标签因为带有背景,所以可以用WidgetSpan
加上标签背景,这里使用CustomPaint
实现:
List<WidgetSpan> _getWidgetSpan() {
return widget.tags
.map((e) => WidgetSpan(
child: CustomPaint(
painter: BgPainter(e.color),
child: Text(
' ' + e.label + ' ',
style: widget.style,
),
),
))
.toList();
}
这个BgPainter
就一个功能,绘制背景色:
class BgPainter extends CustomPainter {
final Paint _painter;
final Color color;
BgPainter(this.color) : _painter = Paint()..color = color;
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(2, 0, size.width - 2, size.height), _painter);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>
oldDelegate != this;
}
使用:
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗斯总统普京和美国总统sadaasdadadada',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
附上完整代码:
main.dart
import 'package:custom/review/tag_title.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗斯总统普京和美国总统sadaasdadadada',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
],
),
);
}
}
Tag_title.dart:
//@dart=2.12
import 'package:flutter/material.dart';
import 'bg_painter.dart';
class TagTitle extends StatefulWidget {
const TagTitle(
this.text, {
Key? key,
required this.tags,
this.maxLines = 2,
this.style = const TextStyle(color: Colors.black, fontSize: 16),
}) : super(key: key);
final String text;
final int maxLines;
final TextStyle style;
final List<Tag> tags;
@override
TagTitleState createState() => TagTitleState();
}
class TagTitleState extends State<TagTitle> {
late final String tagTexts;
late final TextSpan _allSp;
final String ellipsizeText = '...';
@override
void initState() {
super.initState();
tagTexts = widget.tags.fold<String>(
' ', (previousValue, element) => '$previousValue${element.label} ');
_allSp = TextSpan(
text: '${widget.text}$tagTexts',
style: widget.style,
);
}
List<WidgetSpan> _getWidgetSpan() {
return widget.tags
.map((e) => WidgetSpan(
child: CustomPaint(
painter: BgPainter(e.color),
child: Text(
' ' + e.label + ' ',
style: widget.style,
),
),
))
.toList();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
assert(constraints.hasBoundedWidth);
// 标签
final tagsSp = TextSpan(
text: tagTexts,
style: widget.style,
);
// 省略号
final ellipsizeTextSp = TextSpan(
text: ellipsizeText,
style: widget.style,
);
// 标题
final textSp = TextSpan(
text: widget.text,
style: widget.style,
);
final textPainter = TextPainter(
text: tagsSp,
textDirection: TextDirection.ltr,
maxLines: widget.maxLines,
)..layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final tagsSize = textPainter.size;
textPainter.text = ellipsizeTextSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final ellipsizeSize = textPainter.size;
textPainter.text = textSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final textSize = textPainter.size;
textPainter.text = _allSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final pos = textPainter.getPositionForOffset(Offset(
textSize.width - tagsSize.width - ellipsizeSize.width,
textSize.height,
));
final endIndex = textPainter.getOffsetBefore(pos.offset);
TextSpan textSpan;
if (textPainter.didExceedMaxLines) {
textSpan = TextSpan(
style: widget.style,
text: widget.text.substring(0, endIndex) + ellipsizeText,
children: _getWidgetSpan(),
);
} else {
textSpan = TextSpan(
style: widget.style,
text: widget.text,
children: _getWidgetSpan(),
);
}
return RichText(
text: textSpan,
overflow: TextOverflow.ellipsis,
maxLines: widget.maxLines,
);
},
);
}
}
class Tag {
/// 标签文本
final String label;
/// 标签背景颜色
final Color color;
Tag({required this.label, required this.color});
}
bg_painter.dart:
//@dart=2.12
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class BgPainter extends CustomPainter {
final Paint _painter;
final Color color;
BgPainter(this.color) : _painter = Paint()..color = color;
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(2, 0, size.width - 2, size.height), _painter);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>
oldDelegate != this;
}
作者:艾维码
链接:https://juejin.cn/post/7068194837954035725
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。