Implementing Gradient Text in Flutter Using ShaderMask, TextStyle.foreground, CustomPainter, and ExtendedText
This article explores multiple techniques for creating gradient text in Flutter—including ShaderMask, TextStyle.foreground, a custom CustomPainter, and the ExtendedText package's GradientConfig—detailing their code implementations, limitations, and how to handle WidgetSpan, emoji, and selective gradient rendering.
Preface
One day a product manager said the app's text looked too plain and wanted a colorful, gradient effect, providing an example image. The author notes that achieving such an effect is not a trivial task.
Exploring Solutions
ShaderMask
ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
colors:
[Colors.blue, Colors.red],
).createShader(bounds);
},
child: const Text(
'I will be affected by the gradient effect',
style: TextStyle(color: Colors.white),
),
);ShaderMask applies a gradient to the whole widget, but it cannot treat WidgetSpan or emoji individually, especially for multi‑line text where the gradient is applied to the entire block.
Adding WidgetSpan and Emoji
ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
colors:
[Colors.blue, Colors.red],
).createShader(bounds);
},
child: Text.rich(
TextSpan(
children:
[
WidgetSpan(
child: Container(
width: 20,
height: 20,
color: Colors.red,
),
),
const TextSpan(
text: '🤭I will be affected by the gradient effect🤭',
style: TextStyle(color: Colors.green),
),
WidgetSpan(
child: Container(
width: 20,
height: 20,
color: Colors.blue,
),
),
],
),
style: const TextStyle(color: Colors.white),
),
);Even with WidgetSpan and emoji, the gradient still covers the whole area, making it impossible to apply different gradients per line or per span.
TextStyle.foreground
Text(
'I will be affected by the gradient effect',
style: TextStyle(
foreground: Paint()
..shader = const LinearGradient(
colors:
[Colors.blue, Colors.red],
).createShader(
// Text's relative position rectangle
const Rect.fromLTWH(0, 0, 100, 50),
),
),
);Using TextStyle.foreground with a shader gives more control, but you must manually calculate the rectangle that matches the rendered text, and it still cannot selectively exclude WidgetSpan or emoji.
CustomPainter Experiment
Because the built‑in APIs are limited, the author creates a CustomPainter to draw gradient text more flexibly.
class _GradientTextPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// Create TextPainter to layout the text
final TextPainter textPainter = TextPainter(
text: const TextSpan(text: 'Gradient Text'),
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: size.width);
// Define the rectangle that matches the text size
final Size textSize = textPainter.size;
final Rect shaderRect = Rect.fromLTWH(0, 0, textSize.width, textSize.height);
// Create the gradient shader
final Shader shader = const LinearGradient(
colors:
[Colors.blue, Colors.red],
).createShader(shaderRect);
// Paint the gradient using BlendMode.srcIn
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) => true;
}By drawing the text first, then painting a gradient rectangle with BlendMode.srcIn , any shape can be used to achieve the desired effect.
ExtendedText Package
The extended_text package adds a GradientConfig parameter to simplify gradient text rendering.
GradientConfig _config = GradientConfig(
gradient: const LinearGradient(
colors:
[Colors.blue, Colors.red],
),
ignoreRegex: GradientConfig.ignoreEmojiRegex,
ignoreWidgetSpan: true,
renderMode: GradientRenderMode.fullText,
blendMode: BlendMode.srcIn,
beforeDrawGradient: (PaintingContext context, TextPainter textPainter, Offset offset) {
// custom logic before drawing gradient
},
);GradientConfig lets you specify the gradient, decide whether to ignore emojis (via ignoreRegex ) or WidgetSpan , choose the render mode (fullText, line, selection, word, character), and set the blend mode.
GradientRenderMode Enum
enum GradientRenderMode {
fullText, // apply to the whole text
line, // apply per line
selection,// apply to a TextSelection region
word, // apply per word
character // apply per character
}Different modes allow fine‑grained control over where the gradient is painted.
Handling WidgetSpan and Emoji
To prevent the gradient from affecting WidgetSpan or emoji, you can either draw the gradient before or after the widget spans based on the ignoreWidgetSpan flag.
// Draw gradient before widget spans
if (_gradientConfig != null && _gradientConfig!.ignoreWidgetSpan) {
drawGradient(context, offset);
}
paintInlineChildren(context, offset);
// Draw gradient after widget spans
if (_gradientConfig != null && !_gradientConfig!.ignoreWidgetSpan) {
drawGradient(context, offset);
}Ignoring Specific Characters with Regex
Emoji or other characters can be excluded from the gradient by providing a regular expression and clipping those regions.
final List
boxes =
[];
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));
});
}Custom Gradient Shapes via Callback
The beforeDrawGradient callback lets developers draw custom shapes (e.g., heart or star) and clip the canvas before applying the gradient.
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:
break;
}
if (path != null) {
context.canvas.drawPath(
path,
Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 1,
);
context.canvas.clipPath(path);
}
}Conclusion
The gradient text capability is supported in Flutter 3.10.0 and later (package version ≥ 11.0.9). By customizing the text‑painting pipeline—whether through ShaderMask , TextStyle.foreground , a CustomPainter , or the ExtendedText package—developers can create rich, flexible gradient effects while handling special spans and emojis as needed.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.