本文主要是介绍Flutter实现文字镂空效果的详细步骤,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《Flutter实现文字镂空效果的详细步骤》:本文主要介绍如何使用Flutter实现文字镂空效果,包括创建基础应用结构、实现自定义绘制器、构建UI界面以及实现颜色选择按钮等步骤,并详细解析了混合模...
引言
哈哈,2019年初我刚入职时,遇到了一个特别的需求:学校的卡片上要有个分类标签,文字部分还得镂空。当时我刚开始接触Flutter,对很多功能都不熟悉,这个需求就一直没能实现,成了我的一个小执念。现在我早已不在那儿工作了,可这两天闲来无事,突然想起了这个事。趁着五一假期,我开始琢磨画笔功能,终于把当年实现不了的功能给实现了。
Tip: 这时候可能会有人说:啊,这道题我会,用
ShaderMask
配置blendMode: BlendMode.srcOut
就能实现,但实际上这个组件不能设置圆角,内边距等相关内容,如果这时候添加一个Container
那么镂空效果也只能看到Container
的颜色,而不能看到最底部的图片
实现原理
文字镂空效果的核心是使用Canvas和自定义绘制(CustomPainter)来创建一个矩形,然后从中"切出"文字形状。我们将使用Flutter的BlendMode.dstOut
混合模式来实现这一效果。
开始实现
步骤1:创建基础应用结构
首先,我们需要设置基本的应用结构:
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设计。
步骤2:创建主屏幕
接下来,我们创建主屏幕,这是一个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
:背景颜色
步骤3:实现自定义绘制器
这是实现镂空效果的核心部分 - 自定义绘制器:
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(canvphpas, 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
方法优化重绘性能
步骤4:构建UI界面
现在,让我们实现主界面,包括预览区域和控制面板:
@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(zvulY 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), ], ), ], ), ), ], ), ); }
步骤5:实现颜色选择按钮
最后,我们实现颜色选择按钮的构建方法:
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, ), ), ), ); }
关键技术点解析
1. 混合模式(BlendMode)的应用
在这个效果中,最关键的技术是使用BlendMode.dstOut
混合模式。这个混合模式会从目标图像(矩形)中"减去"源图像(文字),从而创建出文字形状的"洞"。
final Paint cutoutPaint = Paint() ..color = Colors.white ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut;
2. Canvas图层(Layer)的使用
我们使用canvas.saveLayer()
和canvas.restore()
来创建和管理图层,这是实现复杂绘制效果的关键:
canvas.saveLayer(rect.inflate(20), Paint()); // 绘制矩形 canvas.saveLayer(rect.inflate(20), cutoutPaint); // 绘制文字 canvas.restore(); canvas.restore();
3. 文字居中处理
为了让文字在矩形中居中显示,我们需要计算正确的位置:
final double xCenter = (size.width - textPainter.width) / 2; final double yCenter = (size.height - textPainter.height) / 2;
code
为了方便大家查阅,下面贴出完整代码
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) http://www.chinasem.cn{ 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; python }); }, 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; RectangleTextCutopythonutPainter({ 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文字镂空效果的资料请关注China编程(www.chinasem.cn)其它相关文章!
这篇关于Flutter实现文字镂空效果的详细步骤的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!