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

2025-05-03 18:50

本文主要是介绍Flutter实现文字镂空效果的详细步骤,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Flutter实现文字镂空效果的详细步骤》:本文主要介绍如何使用Flutter实现文字镂空效果,包括创建基础应用结构、实现自定义绘制器、构建UI界面以及实现颜色选择按钮等步骤,并详细解析了混合模...

引言

哈哈,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.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实现文字镂空效果的详细步骤的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154470

相关文章

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll