22人参与 • 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 举报,一经查实将立刻删除。
发表评论