开发者

Flutter实现文字镂空效果的详细步骤

开发者 https://www.devze.com 2025-05-08 11:33 出处:网络 作者: 淡写成灰
目录引言实现原理开始实现步骤1:创建基础应用结构步骤2:创建主屏幕步骤3:实现自定义绘制器步骤4:构建UI界面步骤5:实现颜色选择按钮关键技术点解析1. 混合模式(BlendMode)的应用 2. Canvas图层(Layer)的
目录
  • 引言
  • 实现原理
  • 开始实现
    • 步骤1:创建基础应用结构
    • 步骤2:创建主屏幕
    • 步骤3:实现自定义绘制器
    • 步骤4:构建UI界面
    • 步骤5:实现颜色选择按钮
  • 关键技术点解析
    • 1. 混合模式(BlendMode)的应用 
    • 2. Canvas图层(Layer)的使用
    • 3. 文字居中处理
  • code

    引言

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

    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;
      }
    }
    

    这个自定义绘制器的工作原理是:

    • 创建一个圆角矩形
    • 使用saveLayerBlendMode.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.devze.com{
        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文字镂空效果的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号