注册

Flutter 文字环绕

文字环绕


需求


最近接到一个需求,类似于文字环绕,标题最多两行,超出省略,标题后面可以添加标签。效果如下:


Simulator Screen Shot - iPhone 13 - 2022-02-24 at 16.49.54.png


富文本不能控制省略和折行,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;
}

实现


标题文字和标签文字有两种显示情况:



  1. 超出最大行数;
  2. 未超出最大行数;

先假设第一种情况,因为标签前后有间距,所以每个标签前后补一个空格,再把标题和文字拼接创建对应的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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册