Flutter 我就要五彩斑斓渐进的黑

相关阅读

前言

有一天,产品跟我说,我们的产品的文字太普通了,我想要那种五彩斑斓渐进的效果,随即丢给我一张图。

截屏2024-09-06 15.20.14.png

我说这不是轻松拿捏的事情吗?

古狗一下

打开古狗搜索一番,渐进色文本主要的实现方式有 2 种。

ShaderMask

    ShaderMask(
      shaderCallback: (Rect bounds) {
        return const LinearGradient(
          colors: [Colors.blue, Colors.red],
        ).createShader(bounds);
      },
      child: const Text(
        '我会被作用于渐变效果',
        style: TextStyle(color: Colors.white),
      ),
    ),

截屏2024-09-06 14.54.42.png

接下来我们加上 WidgeSpan 以及一些 emoji 表情看看。

    ShaderMask(
      shaderCallback: (Rect bounds) {
        return const LinearGradient(
          colors: [Colors.blue, Colors.red],
        ).createShader(bounds);
      },
      child: Text.rich(
        TextSpan(
          children: [
            WidgetSpan(
              child: Container(
                width: 20,
                height: 20,
                color: Colors.red,
              ),
            ),
            const TextSpan(
              text: '🤭我会被作用于渐变效果🤭',
              style: TextStyle(color: Colors.green),
            ),
            WidgetSpan(
              child: Container(
                width: 20,
                height: 20,
                color: Colors.blue,
              ),
            ),
          ],
        ),
        style: const TextStyle(color: Colors.white),
      ),
    ),

截屏2024-09-06 15.09.05.png

ShaderMask 由于是对整体生效,很难满足我们的需求, 比如无法特殊处理 WidgeSpan 以及一些 emoji,比如多行的时候,也只能对整体,不能对每一行单独生效。

TextStyle.foreground

    Text(
      '我会被作用于渐变效果',
      style: TextStyle(
        foreground: Paint()
          ..shader = const LinearGradient(
            colors: [Colors.blue, Colors.red],
          ).createShader(
            
            const Rect.fromLTWH(0, 0, 100, 50),
          ),
      ),
    ),

由于不容易知道文本实际的渲染位置和大小,可以看到无法做到跟 ShaderMask 一样的效果。

截屏2024-09-06 15.03.58.png

接下来我们加上 WidgeSpan 以及一些 emoji 表情看看。

    Text.rich(
      TextSpan(
        children: [
          WidgetSpan(
            child: Container(
              width: 20,
              height: 20,
              color: Colors.red,
            ),
          ),
          const TextSpan(
            text: '🤭我会被作用于渐变效果🤭',
            style: TextStyle(color: Colors.green),
          ),
          WidgetSpan(
            child: Container(
              width: 20,
              height: 20,
              color: Colors.blue,
            ),
          ),
        ],
      ),
      style: TextStyle(
        foreground: Paint()
          ..shader = const LinearGradient(
            colors: [Colors.blue, Colors.red],
          ).createShader(
            
            const Rect.fromLTWH(0, 0, 200, 50),
          ),
      ),
    ),

截屏2024-09-06 15.11.25.png

效果尚可,但是 createShader 传入的 Rect 需要动态根据情况去设置,这对于用户来说是极其不方便和通用的。

自己写试试

总结下来,直接利用官方的 api ,并不能优雅地处理文本渐进色的场景。

  • 通用简单的 api
  • 可以处理 WidgeSpan 以及一些 emoji
  • 可以自定义渐进效果的作用区域

其实,不管哪种方式实现,最终还是归于 Canvas

于是我用 CustomPainter 做了个小实验。

class _GradientTextPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    
    final TextPainter textPainter = TextPainter(
      text: const TextSpan(text: '渐进色文本'),
      textDirection: TextDirection.ltr,
    );

    
    textPainter.layout(maxWidth: size.width);

    
    final Size textSize = textPainter.size;
    final Rect shaderRect =
        Rect.fromLTWH(0, 0, textSize.width, textSize.height);

    
    final Shader shader = const LinearGradient(
      colors: [Colors.blue, Colors.red],
    ).createShader(shaderRect);

    
    final Paint paint = Paint()
      ..shader = shader
      ..blendMode = BlendMode.srcIn;
    canvas.saveLayer(const Offset(0, 0) & size, Paint());
    textPainter.paint(canvas, const Offset(0, 0));
    canvas.drawRect(shaderRect, paint);
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

截屏2024-09-06 16.58.40.png

利用 shader 以及 BlendMode.srcIn 我们很轻松的做出来渐进文字的效果,基于这个原理,我们可以做出来更多的效果来。

ExtendedText

基于上面的探索,ExtendedText 新增了 GradientConfig 参数。

  GradientConfig _config = GradientConfig(
    gradient: const LinearGradient(
      colors: [Colors.blue, Colors.red],
    ),
    ignoreRegex: GradientConfig.ignoreEmojiRegex,
    ignoreWidgetSpan: true,
    renderMode: GradientRenderMode.fullText,
    blendMode: BlendMode.srcIn,
    beforeDrawGradient:
        (PaintingContext context, TextPainter textPainter, Offset offset) {
      
    },
  );

渐进配置

渐进配置,为 Flutter sdk 中的 Gradient.

    gradient: const LinearGradient(
      colors: [Colors.blue, Colors.red],
    ),

渐进模式

渐进效果的作用方式

enum GradientRenderMode {
  fullText, 
  line, 
  selection, 
  word, 
  character, 
}

github.com/fluttercand…

渐进效果从左到右
fulltext selection
截屏2024-09-06 14.04.10.png 截屏2024-09-06 14.04.43.png
word character
截屏2024-09-06 14.05.55.png 截屏2024-09-06 14.05.15.png
渐进效果从上到下
fulltext line
截屏2024-09-06 14.06.43.png 截屏2024-09-06 14.07.21.png

BlendMode

我们是利用 Paint().blendMode 来绘制渐进效果的。默认是 [BlendMode.srcIn] ,提供这个属性是为了以防万一,用户可以自己设置。

不受渐进效果影响的区域

我们想要消去渐进效果的影响,其实之前的文章里面也有讲到过。可以利用 BlendMode.clear 或者 canvas.clipPath / canvas.cli… 来处理,大概的代码如下:

    if (ignoreRect != null) {
      context.canvas.save();
      context.canvas.clipRect(
        ignoreRect.shift(offset),
        clipOp: ui.ClipOp.difference,
      );      
    }
    final ui.Shader shader = _gradientConfig!.gradient.createShader(rect);
    final ui.Paint paint = Paint()
      ..shader = shader
      ..blendMode = BlendMode.srcIn;

    
    context.canvas.drawRect(rect, paint);
    if (ignoreRect != null) {
      context.canvas.restore();
    }
 
ignoreWidgetSpan

对于 WidgetSpan, 我们可以通过控制绘制先后,来避免 WidgetSpan 的绘制受到渐进效果的影响。即想要避免,就渐进效果的绘制就应该在 WidgetSpan 绘制之前。

    
    if (_gradientConfig != null && _gradientConfig!.ignoreWidgetSpan) {
      drawGradient(context, offset);
    }
    paintInlineChildren(context, offset);
    
    if (_gradientConfig != null && !_gradientConfig!.ignoreWidgetSpan) {
      drawGradient(context, offset);
    }

github.com/fluttercand…

ignoreRegex

我们需要避免一些特殊字符受到渐进效果的影响,比如 emoji ,那我们只需要把它们找出来,然后利用 clipRect 避免。

    final List boxes = [];
    if (_gradientConfig != null && _gradientConfig!.ignoreRegex != null) {
      _gradientConfig!.ignoreRegex!.allMatches(_textPainter.plainText).forEach(
        (RegExpMatch match) {
          final int start = match.start;
          final int end = match.end;
          final TextSelection textSelection =
              TextSelection(baseOffset: start, extentOffset: end);
          boxes.addAll(_textPainter.getBoxesForSelection(textSelection));
        },
      );
    }

最后将 boxes 一一进行处理即可。

IgnoreGradientSpan

如果有一些特定的内容,不想受到渐进效果的影响,那么我们也只需要把它们找出来,

    if (_textPainter.text != null) {
      void _findIgnoreGradientSpan(InlineSpan span, int startIndex) {
        if (span is IgnoreGradientSpan) {
          final int length = span.toPlainText().length;
          final TextSelection textSelection = TextSelection(
              baseOffset: startIndex, extentOffset: startIndex + length);
          boxes.addAll(_textPainter.getBoxesForSelection(textSelection));
          
          return;
        }

        if (span is TextSpan && span.children != null) {
          int childStartIndex = startIndex;
          for (final InlineSpan child in span.children!) {
            _findIgnoreGradientSpan(child, childStartIndex);
            childStartIndex += child.toPlainText().length;
          }
        }
      }

      _findIgnoreGradientSpan(_textPainter.text!, 0);
    }

你这样使用它即可

class IgnoreGradientTextSpan extends TextSpan with IgnoreGradientSpan {
  IgnoreGradientTextSpan({String? text, List? children})
      : super(
          text: text,
          children: children,
        );
}
beforeDrawGradient

除了上面特定的处理,也提供了回调给用户,用户可以根据自己的需求,对渐进效果进行处理,比如下面演示的就是只对心型和星星内部的文字进行渐变。

  void _beforeDrawGradient(
    PaintingContext context,
    TextPainter textPainter,
    Offset offset,
  ) {
    final Rect rect = offset & textPainter.size;
    Path? path;

    switch (_drawGradientShape) {
      case DrawGradientShape.heart:
        path = clipheart(rect);
        break;
      case DrawGradientShape.star:
        path = clipStar(rect);
        break;
      case DrawGradientShape.none:
    }
    if (path != null) {
      context.canvas.drawPath(
        path,
        Paint()
          ..color = Colors.red
          ..style = PaintingStyle.stroke
          ..strokeWidth = 1,
      );
      context.canvas.clipPath(path);
    }
  }

github.com/fluttercand…

截屏2024-09-06 17.42.04.png

结语

extended_text | Flutter package (flutter-io.cn) 该功能在 Flutter 3.10.0 及其以上版本增加支持,即组件版本大于等于 11.0.9

通过对文本绘制流程的定制,我们轻松地支持了更加强大的文字渐进效果。 Flutter 较为优秀的结构设计让用户能够很方便地进行各种布局以及绘制的定制,这也是 Flutter 社区能不断壮大发展的一个原因。

Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果QQ群:181398081

最最后放上 Flutter Candies 全家桶,真香。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=21389,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?