149人参与 • 2025-05-03 • flutter
哈哈,2019年初我刚入职时,遇到了一个特别的需求:学校的卡片上要有个分类标签,文字部分还得镂空。当时我刚开始接触flutter,对很多功能都不熟悉,这个需求就一直没能实现,成了我的一个小执念。现在我早已不在那儿工作了,可这两天闲来无事,突然想起了这个事。趁着五一假期,我开始琢磨画笔功能,终于把当年实现不了的功能给实现了。

tip: 这时候可能会有人说:啊,这道题我会,用
shadermask配置blendmode: blendmode.srcout就能实现,但实际上这个组件不能设置圆角,内边距等相关内容,如果这时候添加一个container那么镂空效果也只能看到container的颜色,而不能看到最底部的图片
文字镂空效果的核心是使用canvas和自定义绘制(custompainter)来创建一个矩形,然后从中"切出"文字形状。我们将使用flutter的blendmode.dstout混合模式来实现这一效果。
首先,我们需要设置基本的应用结构:
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: 'rectangle text cutout',
theme: themedata(
primaryswatch: colors.teal,
usematerial3: true,
),
home: const rectangledrawingscreen(),
);
}
}
这里我们创建了一个基本的materialapp,并设置了主题颜色为teal(青色),启用了material 3设计。
接下来,我们创建主屏幕,这是一个statefulwidget,因为我们需要管理多个可变状态:
class rectangledrawingscreen extends statefulwidget {
const rectangledrawingscreen({super.key});
@override
state<rectangledrawingscreen> createstate() => _rectangledrawingscreenstate();
}
class _rectangledrawingscreenstate extends state<rectangledrawingscreen> {
// 定义状态变量
double _cornerradius = 20.0;
string _text = "flutter";
double _fontsize = 60.0;
color _rectanglecolor = colors.teal;
color _backgroundcolor = colors.white;
// 构建ui...
}
我们定义了几个关键状态变量:
_cornerradius:矩形的圆角半径_text:要镂空的文字_fontsize:文字大小_rectanglecolor:矩形的颜色_backgroundcolor:背景颜色这是实现镂空效果的核心部分 - 自定义绘制器:
class rectangletextcutoutpainter extends custompainter {
final double cornerradius;
final string text;
final double fontsize;
final color rectanglecolor;
rectangletextcutoutpainter({
required this.cornerradius,
required this.text,
required this.fontsize,
required this.rectanglecolor,
});
@override
void paint(canvas canvas, size size) {
// 创建矩形区域
final rect rect = rect.fromltwh(
20,
20,
size.width - 40,
size.height - 40,
);
// 创建圆角矩形
final rrect roundedrect = rrect.fromrectandradius(
rect,
radius.circular(cornerradius),
);
// 设置文字样式
final textstyle = textstyle(
fontsize: fontsize,
fontweight: fontweight.bold,
);
final textspan = textspan(
text: text,
style: textstyle,
);
// 创建文字绘制器
final textpainter = textpainter(
text: textspan,
textdirection: textdirection.ltr,
);
// 计算文字位置
textpainter.layout(
minwidth: 0,
maxwidth: size.width,
);
final double xcenter = (size.width - textpainter.width) / 2;
final double ycenter = (size.height - textpainter.height) / 2;
// 使用图层和混合模式实现镂空效果
canvas.savelayer(rect.inflate(20), paint());
final paint rectanglepaint = paint()
..color = rectanglecolor
..style = paintingstyle.fill;
canvas.drawrrect(roundedrect, rectanglepaint);
final paint cutoutpaint = paint()
..color = colors.white
..style = paintingstyle.fill
..blendmode = blendmode.dstout;
canvas.savelayer(rect.inflate(20), cutoutpaint);
textpainter.paint(canvas, offset(xcenter, ycenter));
canvas.restore();
canvas.restore();
}
@override
bool shouldrepaint(covariant rectangletextcutoutpainter olddelegate) {
return olddelegate.cornerradius != cornerradius ||
olddelegate.text != text ||
olddelegate.fontsize != fontsize ||
olddelegate.rectanglecolor != rectanglecolor;
}
}
这个自定义绘制器的工作原理是:
savelayer和blendmode.dstout创建一个混合图层shouldrepaint方法优化重绘性能现在,让我们实现主界面,包括预览区域和控制面板:
@override
widget build(buildcontext context) {
return scaffold(
appbar: appbar(
title: const text('rectangle text cutout'),
backgroundcolor: colors.teal.shade100,
),
body: column(
children: [
// 预览区域
expanded(
child: container(
color: colors.grey[200],
child: center(
child: stack(
children: [
// 背景图片
positioned.fill(
child: image.network(
"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d11fee3a97464bca82c9291435cc2a89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6y6yer5oqa5pyv56s-5yy6ieag5reh5yaz5oiq54gw:q75.awebp?rk3s=f64ab15b&x-expires=1746213056&x-signature=ylstmk1m2eveu8bi%2bhdjkvuhe7u%3d",
fit: boxfit.cover,
),
),
// 自定义绘制
custompaint(
size: const size(double.infinity, double.infinity),
painter: rectangletextcutoutpainter(
cornerradius: _cornerradius,
text: _text,
fontsize: _fontsize,
rectanglecolor: _rectanglecolor,
),
),
// 额外的shadermask效果
shadermask(
blendmode: blendmode.srcout,
child: text(
_text,
),
shadercallback: (bounds) =>
lineargradient(colors: [colors.black], stops: [0.0])
.createshader(bounds),
),
],
),
),
),
),
// 控制面板
container(
padding: const edgeinsets.all(16),
color: colors.grey[200],
child: column(
crossaxisalignment: crossaxisalignment.start,
children: [
// 圆角控制
const text('corner radius:', style: textstyle(fontweight: fontweight.bold)),
slider(
value: _cornerradius,
min: 0,
max: 100,
divisions: 100,
label: _cornerradius.round().tostring(),
activecolor: colors.teal,
onchanged: (value) {
setstate(() {
_cornerradius = value;
});
},
),
// 字体大小控制
const sizedbox(height: 10),
const text('font size:', style: textstyle(fontweight: fontweight.bold)),
slider(
value: _fontsize,
min: 20,
max: 120,
divisions: 100,
label: _fontsize.round().tostring(),
activecolor: colors.teal,
onchanged: (value) {
setstate(() {
_fontsize = value;
});
},
),
// 文字输入
const sizedbox(height: 10),
textfield(
decoration: const inputdecoration(
labeltext: 'text to cut out',
border: outlineinputborder(),
focusedborder: outlineinputborder(
borderside: borderside(color: colors.teal),
),
),
onchanged: (value) {
setstate(() {
_text = value;
});
},
controller: texteditingcontroller(text: _text),
),
// 矩形颜色选择
const sizedbox(height: 16),
row(
children: [
const text('rectangle color: ', style: textstyle(fontweight: fontweight.bold)),
const sizedbox(width: 10),
_buildcolorbutton(colors.teal),
_buildcolorbutton(colors.blue),
_buildcolorbutton(colors.red),
_buildcolorbutton(colors.purple),
_buildcolorbutton(colors.orange),
],
),
// 背景颜色选择
const sizedbox(height: 16),
row(
children: [
const text('background color: ', style: textstyle(fontweight: fontweight.bold)),
const sizedbox(width: 10),
_buildbackgroundcolorbutton(colors.white),
_buildbackgroundcolorbutton(colors.grey.shade300),
_buildbackgroundcolorbutton(colors.yellow.shade100),
_buildbackgroundcolorbutton(colors.blue.shade100),
_buildbackgroundcolorbutton(colors.pink.shade100),
],
),
],
),
),
],
),
);
}
最后,我们实现颜色选择按钮的构建方法:
widget _buildcolorbutton(color color) {
return gesturedetector(
ontap: () {
setstate(() {
_rectanglecolor = color;
});
},
child: container(
margin: const edgeinsets.only(right: 8),
width: 30,
height: 30,
decoration: boxdecoration(
color: color,
shape: boxshape.circle,
border: border.all(
color: _rectanglecolor == color ? colors.black : colors.transparent,
width: 2,
),
),
),
);
}
widget _buildbackgroundcolorbutton(color color) {
return gesturedetector(
ontap: () {
setstate(() {
_backgroundcolor = color;
});
},
child: container(
margin: const edgeinsets.only(right: 8),
width: 30,
height: 30,
decoration: boxdecoration(
color: color,
shape: boxshape.circle,
border: border.all(
color: _backgroundcolor == color ? colors.black : colors.transparent,
width: 2,
),
),
),
);
}
在这个效果中,最关键的技术是使用blendmode.dstout混合模式。这个混合模式会从目标图像(矩形)中"减去"源图像(文字),从而创建出文字形状的"洞"。
final paint cutoutpaint = paint() ..color = colors.white ..style = paintingstyle.fill ..blendmode = blendmode.dstout;
我们使用canvas.savelayer()和canvas.restore()来创建和管理图层,这是实现复杂绘制效果的关键:
canvas.savelayer(rect.inflate(20), paint()); // 绘制矩形 canvas.savelayer(rect.inflate(20), cutoutpaint); // 绘制文字 canvas.restore(); canvas.restore();
为了让文字在矩形中居中显示,我们需要计算正确的位置:
final double xcenter = (size.width - textpainter.width) / 2; final double ycenter = (size.height - textpainter.height) / 2;
为了方便大家查阅,下面贴出完整代码
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: 'rectangle text cutout',
theme: themedata(
primaryswatch: colors.teal,
usematerial3: true,
),
home: const rectangledrawingscreen(),
);
}
}
class rectangledrawingscreen extends statefulwidget {
const rectangledrawingscreen({super.key});
@override
state<rectangledrawingscreen> createstate() => _rectangledrawingscreenstate();
}
class _rectangledrawingscreenstate extends state<rectangledrawingscreen> {
double _cornerradius = 20.0;
string _text = "flutter";
double _fontsize = 60.0;
color _rectanglecolor = colors.teal;
color _backgroundcolor = colors.white;
@override
widget build(buildcontext context) {
return scaffold(
appbar: appbar(
title: const text('rectangle text cutout'),
backgroundcolor: colors.teal.shade100,
),
body: column(
children: [
expanded(
child: container(
color: colors.grey[200],
child: center(
child: stack(
children: [
positioned.fill(
child: image.network(
"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d11fee3a97464bca82c9291435cc2a89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6y6yer5oqa5pyv56s-5yy6ieag5reh5yaz5oiq54gw:q75.awebp?rk3s=f64ab15b&x-expires=1746213056&x-signature=ylstmk1m2eveu8bi%2bhdjkvuhe7u%3d",
fit: boxfit.cover,
),
),
custompaint(
size: const size(double.infinity, double.infinity),
painter: rectangletextcutoutpainter(
cornerradius: _cornerradius,
text: _text,
fontsize: _fontsize,
rectanglecolor: _rectanglecolor,
),
),
shadermask(
blendmode: blendmode.srcout,
child: text(
_text,
),
shadercallback: (bounds) =>
lineargradient(colors: [colors.black], stops: [0.0])
.createshader(bounds),
),
],
),
),
),
),
container(
padding: const edgeinsets.all(16),
color: colors.grey[200],
child: column(
crossaxisalignment: crossaxisalignment.start,
children: [
const text('corner radius:', style: textstyle(fontweight: fontweight.bold)),
slider(
value: _cornerradius,
min: 0,
max: 100,
divisions: 100,
label: _cornerradius.round().tostring(),
activecolor: colors.teal,
onchanged: (value) {
setstate(() {
_cornerradius = value;
});
},
),
const sizedbox(height: 10),
const text('font size:', style: textstyle(fontweight: fontweight.bold)),
slider(
value: _fontsize,
min: 20,
max: 120,
divisions: 100,
label: _fontsize.round().tostring(),
activecolor: colors.teal,
onchanged: (value) {
setstate(() {
_fontsize = value;
});
},
),
const sizedbox(height: 10),
textfield(
decoration: const inputdecoration(
labeltext: 'text to cut out',
border: outlineinputborder(),
focusedborder: outlineinputborder(
borderside: borderside(color: colors.teal),
),
),
onchanged: (value) {
setstate(() {
_text = value;
});
},
controller: texteditingcontroller(text: _text),
),
const sizedbox(height: 16),
row(
children: [
const text('rectangle color: ', style: textstyle(fontweight: fontweight.bold)),
const sizedbox(width: 10),
_buildcolorbutton(colors.teal),
_buildcolorbutton(colors.blue),
_buildcolorbutton(colors.red),
_buildcolorbutton(colors.purple),
_buildcolorbutton(colors.orange),
],
),
const sizedbox(height: 16),
row(
children: [
const text('background color: ', style: textstyle(fontweight: fontweight.bold)),
const sizedbox(width: 10),
_buildbackgroundcolorbutton(colors.white),
_buildbackgroundcolorbutton(colors.grey.shade300),
_buildbackgroundcolorbutton(colors.yellow.shade100),
_buildbackgroundcolorbutton(colors.blue.shade100),
_buildbackgroundcolorbutton(colors.pink.shade100),
],
),
],
),
),
],
),
);
}
widget _buildcolorbutton(color color) {
return gesturedetector(
ontap: () {
setstate(() {
_rectanglecolor = color;
});
},
child: container(
margin: const edgeinsets.only(right: 8),
width: 30,
height: 30,
decoration: boxdecoration(
color: color,
shape: boxshape.circle,
border: border.all(
color: _rectanglecolor == color ? colors.black : colors.transparent,
width: 2,
),
),
),
);
}
widget _buildbackgroundcolorbutton(color color) {
return gesturedetector(
ontap: () {
setstate(() {
_backgroundcolor = color;
});
},
child: container(
margin: const edgeinsets.only(right: 8),
width: 30,
height: 30,
decoration: boxdecoration(
color: color,
shape: boxshape.circle,
border: border.all(
color: _backgroundcolor == color ? colors.black : colors.transparent,
width: 2,
),
),
),
);
}
}
class rectangletextcutoutpainter extends custompainter {
final double cornerradius;
final string text;
final double fontsize;
final color rectanglecolor;
rectangletextcutoutpainter({
required this.cornerradius,
required this.text,
required this.fontsize,
required this.rectanglecolor,
});
@override
void paint(canvas canvas, size size) {
final rect rect = rect.fromltwh(
20,
20,
size.width - 40,
size.height - 40,
);
final rrect roundedrect = rrect.fromrectandradius(
rect,
radius.circular(cornerradius),
);
final textstyle = textstyle(
fontsize: fontsize,
fontweight: fontweight.bold,
);
final textspan = textspan(
text: text,
style: textstyle,
);
final textpainter = textpainter(
text: textspan,
textdirection: textdirection.ltr,
);
textpainter.layout(
minwidth: 0,
maxwidth: size.width,
);
final double xcenter = (size.width - textpainter.width) / 2;
final double ycenter = (size.height - textpainter.height) / 2;
canvas.savelayer(rect.inflate(20), paint());
final paint rectanglepaint = paint()
..color = rectanglecolor
..style = paintingstyle.fill;
canvas.drawrrect(roundedrect, rectanglepaint);
final paint cutoutpaint = paint()
..color = colors.white
..style = paintingstyle.fill
..blendmode = blendmode.dstout;
canvas.savelayer(rect.inflate(20), cutoutpaint);
textpainter.paint(canvas, offset(xcenter, ycenter));
canvas.restore();
canvas.restore();
}
@override
bool shouldrepaint(covariant rectangletextcutoutpainter olddelegate) {
return olddelegate.cornerradius != cornerradius ||
olddelegate.text != text ||
olddelegate.fontsize != fontsize ||
olddelegate.rectanglecolor != rectanglecolor;
}
}
以上就是flutter实现文字镂空效果的详细步骤的详细内容,更多关于flutter文字镂空效果的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论