it编程 > App开发 > flutter

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

22人参与 2025-05-03 flutter

引言

哈哈,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...
}

我们定义了几个关键状态变量:

步骤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(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;
  }
}

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

步骤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(
            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) {
    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文字镂空效果的资料请关注代码网其它相关文章!

(0)
打赏 微信扫一扫 微信扫一扫

您想发表意见!!点此发布评论

推荐阅读

鸿蒙开发搭建flutter适配的开发环境

12-29

如何使用Flutter实现生成二维码

11-22

基于Flutter实现扫描二维码功能

11-22

flutter开发的app项目 打包成web

08-10

Flutter动画进阶:解锁能量函数的魔力,打造流畅交互体验

08-06

笔记:flutter中一些不错的 UI 相关库推荐(不断更新)

08-06

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论