相关阅读
- Flutter RichText支持图片显示和自定义图片效果
- Flutter RichText支持自定义文本溢出效果
- Flutter RichText支持自定义文字背景)
- Flutter RichText支持特殊文字效果
- Flutter Text: 扶我起来
- Flutter 糖果群主跑了!
前言
有一天,产品跟我说,我们的产品的文字太普通了,我想要那种五彩斑斓渐进的效果,随即丢给我一张图。
我说这不是轻松拿捏的事情吗?
古狗一下
打开古狗搜索一番,渐进色文本主要的实现方式有 2
种。
ShaderMask
ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
colors: <Color>[Colors.blue, Colors.red],
).createShader(bounds);
},
child: const Text(
'我会被作用于渐变效果',
style: TextStyle(color: Colors.white),
),
),
接下来我们加上 WidgeSpan
以及一些 emoji
表情看看。
ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
colors: <Color>[Colors.blue, Colors.red],
).createShader(bounds);
},
child: Text.rich(
TextSpan(
children: <InlineSpan>[
WidgetSpan(
child: Container(
width: 20,
height: 20,
color: Colors.red,
),
),
const TextSpan(
text: '🤭我会被作用于渐变效果🤭',
style: TextStyle(color: Colors.green),
),
WidgetSpan(
child: Container(
width: 20,
height: 20,
color: Colors.blue,
),
),
],
),
style: const TextStyle(color: Colors.white),
),
),
ShaderMask
由于是对整体生效,很难满足我们的需求, 比如无法特殊处理WidgeSpan
以及一些emoji
,比如多行的时候,也只能对整体,不能对每一行单独生效。
TextStyle.foreground
Text(
'我会被作用于渐变效果',
style: TextStyle(
foreground: Paint()
..shader = const LinearGradient(
colors: <Color>[Colors.blue, Colors.red],
).createShader(
// 文本的相对位置区域
const Rect.fromLTWH(0, 0, 100, 50),
),
),
),
由于不容易知道文本实际的渲染位置和大小,可以看到无法做到跟 ShaderMask
一样的效果。
接下来我们加上 WidgeSpan
以及一些 emoji
表情看看。
Text.rich(
TextSpan(
children: <InlineSpan>[
WidgetSpan(
child: Container(
width: 20,
height: 20,
color: Colors.red,
),
),
const TextSpan(
text: '🤭我会被作用于渐变效果🤭',
style: TextStyle(color: Colors.green),
),
WidgetSpan(
child: Container(
width: 20,
height: 20,
color: Colors.blue,
),
),
],
),
style: TextStyle(
foreground: Paint()
..shader = const LinearGradient(
colors: <Color>[Colors.blue, Colors.red],
).createShader(
// 文本的相对位置区域
const Rect.fromLTWH(0, 0, 200, 50),
),
),
),
效果尚可,但是
createShader
传入的Rect
需要动态根据情况去设置,这对于用户来说是极其不方便和通用的。
自己写试试
总结下来,直接利用官方的 api
,并不能优雅地处理文本渐进色的场景。
- 通用简单的
api
- 可以处理
WidgeSpan
以及一些emoji
。 - 可以自定义渐进效果的作用区域
其实,不管哪种方式实现,最终还是归于 Canvas
。
于是我用 CustomPainter
做了个小实验。
class _GradientTextPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 创建 TextPainter 来绘制文本
final TextPainter textPainter = TextPainter(
text: const TextSpan(text: '渐进色文本'),
textDirection: TextDirection.ltr,
);
// 设置文本的最大宽度
textPainter.layout(maxWidth: size.width);
// 定义渐变的矩形区域
final Size textSize = textPainter.size;
final Rect shaderRect =
Rect.fromLTWH(0, 0, textSize.width, textSize.height);
// 创建渐变的着色器
final Shader shader = const LinearGradient(
colors: <Color>[Colors.blue, Colors.red],
).createShader(shaderRect);
// 绘制渐变色文本
final Paint paint = Paint()
..shader = shader
..blendMode = BlendMode.srcIn;
canvas.saveLayer(const Offset(0, 0) & size, Paint());
textPainter.paint(canvas, const Offset(0, 0));
canvas.drawRect(shaderRect, paint);
canvas.restore();
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
利用 shader
以及 BlendMode.srcIn
我们很轻松的做出来渐进文字的效果,基于这个原理,我们可以做出来更多的效果来。
ExtendedText
基于上面的探索,ExtendedText 新增了 GradientConfig
参数。
GradientConfig _config = GradientConfig(
gradient: const LinearGradient(
colors: <Color>[Colors.blue, Colors.red],
),
ignoreRegex: GradientConfig.ignoreEmojiRegex,
ignoreWidgetSpan: true,
renderMode: GradientRenderMode.fullText,
blendMode: BlendMode.srcIn,
beforeDrawGradient:
(PaintingContext context, TextPainter textPainter, Offset offset) {
},
);
渐进配置
渐进配置,为 Flutter sdk
中的 Gradient
.
gradient: const LinearGradient(
colors: <Color>[Colors.blue, Colors.red],
),
渐进模式
渐进效果的作用方式
enum GradientRenderMode {
fullText, // 作用在整个文本
line, // 作用区域分为一行一行
selection, // 根据 `TextPainter.getBoxesForSelection` 提供的区域
word, // 根据 `TextPainter.getWordBoundary` 提供的区域
character, // 根据 `CharacterRange` 分割的每一个字符所占用的区域
}
渐进效果从左到右
fulltext | selection |
---|---|
word | character |
---|---|
渐进效果从上到下
fulltext | line |
---|---|
BlendMode
我们是利用 Paint().blendMode
来绘制渐进效果的。默认是 [BlendMode.srcIn] ,提供这个属性是为了以防万一,用户可以自己设置。
不受渐进效果影响的区域
我们想要消去渐进效果的影响,其实之前的文章里面也有讲到过。可以利用 BlendMode.clear 或者 canvas.clipPath / canvas.cli… 来处理,大概的代码如下:
if (ignoreRect != null) {
context.canvas.save();
context.canvas.clipRect(
ignoreRect.shift(offset),
clipOp: ui.ClipOp.difference,
);
}
final ui.Shader shader = _gradientConfig!.gradient.createShader(rect);
final ui.Paint paint = Paint()
..shader = shader
..blendMode = BlendMode.srcIn;
// Draw the gradient within the rectangle.
context.canvas.drawRect(rect, paint);
if (ignoreRect != null) {
context.canvas.restore();
}
ignoreWidgetSpan
对于 WidgetSpan
, 我们可以通过控制绘制先后,来避免 WidgetSpan
的绘制受到渐进效果的影响。即想要避免,就渐进效果的绘制就应该在 WidgetSpan
绘制之前。
// zmtzawqlp
if (_gradientConfig != null && _gradientConfig!.ignoreWidgetSpan) {
drawGradient(context, offset);
}
paintInlineChildren(context, offset);
// zmtzawqlp
if (_gradientConfig != null && !_gradientConfig!.ignoreWidgetSpan) {
drawGradient(context, offset);
}
ignoreRegex
我们需要避免一些特殊字符受到渐进效果的影响,比如 emoji
,那我们只需要把它们找出来,然后利用 clipRect
避免。
final List<TextBox> boxes = <ui.TextBox>[];
if (_gradientConfig != null && _gradientConfig!.ignoreRegex != null) {
_gradientConfig!.ignoreRegex!.allMatches(_textPainter.plainText).forEach(
(RegExpMatch match) {
final int start = match.start;
final int end = match.end;
final TextSelection textSelection =
TextSelection(baseOffset: start, extentOffset: end);
boxes.addAll(_textPainter.getBoxesForSelection(textSelection));
},
);
}
最后将 boxes
一一进行处理即可。
IgnoreGradientSpan
如果有一些特定的内容,不想受到渐进效果的影响,那么我们也只需要把它们找出来,
if (_textPainter.text != null) {
void _findIgnoreGradientSpan(InlineSpan span, int startIndex) {
if (span is IgnoreGradientSpan) {
final int length = span.toPlainText().length;
final TextSelection textSelection = TextSelection(
baseOffset: startIndex, extentOffset: startIndex + length);
boxes.addAll(_textPainter.getBoxesForSelection(textSelection));
// IgnoreGradientSpan and it's children should not be applied to the gradient.
return;
}
if (span is TextSpan && span.children != null) {
int childStartIndex = startIndex;
for (final InlineSpan child in span.children!) {
_findIgnoreGradientSpan(child, childStartIndex);
childStartIndex += child.toPlainText().length;
}
}
}
_findIgnoreGradientSpan(_textPainter.text!, 0);
}
你这样使用它即可
class IgnoreGradientTextSpan extends TextSpan with IgnoreGradientSpan {
IgnoreGradientTextSpan({String? text, List<InlineSpan>? children})
: super(
text: text,
children: children,
);
}
beforeDrawGradient
除了上面特定的处理,也提供了回调给用户,用户可以根据自己的需求,对渐进效果进行处理,比如下面演示的就是只对心型和星星内部的文字进行渐变。
void _beforeDrawGradient(
PaintingContext context,
TextPainter textPainter,
Offset offset,
) {
final Rect rect = offset & textPainter.size;
Path? path;
switch (_drawGradientShape) {
case DrawGradientShape.heart:
path = clipheart(rect);
break;
case DrawGradientShape.star:
path = clipStar(rect);
break;
case DrawGradientShape.none:
}
if (path != null) {
context.canvas.drawPath(
path,
Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 1,
);
context.canvas.clipPath(path);
}
}
结语
extended_text | Flutter package (flutter-io.cn) 该功能在 Flutter 3.10.0
及其以上版本增加支持,即组件版本大于等于 11.0.9
。
通过对文本绘制流程的定制,我们轻松地支持了更加强大的文字渐进效果。 Flutter
较为优秀的结构设计让用户能够很方便地进行各种布局以及绘制的定制,这也是 Flutter
社区能不断壮大发展的一个原因。
爱 Flutter
,爱糖果
,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果
最最后放上 Flutter Candies 全家桶,真香。