Flutter 聊天气泡拉伸之点九图(NinePatchDrawable)应用总结

[》跳过拾光记忆]

拾光记忆

1-15.资产管理 Fam、手势触摸、枚举高阶用法、快速实现单选和多选、Diy 滑动轨道、水印功能、Image 高阶用法、矩阵16个参数含义、颜色差异、颜色填充、图像镜像、图像旋转、图像去色等功能的集合

简介: 该篇主要介绍15 篇文章含有功能的目录,可根据自己的需求选择对应的功能介绍查看。
推荐: ⭐️⭐️⭐️⭐️⭐️

16. Flutter 之 IImage 图像反色处理

简介: 该篇主要介绍 Flutter 之 IImage 库中如何实现图像反色功能以及实现原理的介绍。
推荐: ⭐️⭐️⭐️⭐️⭐️

17. Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~(一)

18. Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~(二)

19. Flutter 绘制路径 Path 的全部方法介绍,一篇足矣~(三)

简介: 该篇主要介绍 Flutter 之 图形(Canvas) 绘制路径 (Path)基础功能以及方法实现底层代码的解析。
推荐: ⭐️⭐️⭐️⭐️⭐️

[返回拾光记忆《]

一、简述

随着技术的发展,聊天软件如雨后春笋般不断涌现。为了提升用户的视觉体验,许多软件采用了聊天内容以气泡形式展示,并设计了多种聊天气泡款式供用户选择更换。而当用户输入的文字变化时,气泡能够跟随内容进行相应变化,这一功能真是神奇!那么在Flutter中,我们如何实现这样的效果呢?接下来,我们将介绍聊天气泡的实现过程,并解决其中遇到的问题。

二、实现阐述

聊天气泡目前实现方式大致分为两种:

  1. 气泡形式使用 Canvas 绘制来实现气泡的变化
  2. 借助UI切图,拉伸图片来实现气泡的变化

从上述介绍的两种方式,第 2 种方式实现相比第 1 种方式很容易实现, 而第 2 种方式的核心就是点九图。

三、点九图

点九图 是一种可拉伸的位图,自动调整它的大小,来使图像在充当背景时可以在界面中自适应。 在 Flutter 中点九图表现为将图片切割成九份,如下图所示:

我们可以设置第5块区域的大小和位置也就能确定第 2、4、6、8 四个模块; 如果气泡发生水平变化,那么第 2、5、8 三个模块进行水平拉伸处理;如果气泡垂直发生变化,那么第 4、5、6 三个模块进行垂直拉伸处理;如果气泡垂直和水平都有变化时, 第 2、8 做水平拉伸;第 4、6 做垂直拉伸;第 5 同时水平和垂直拉伸处理。上面介绍的在 Fluttercanvas.drawImageNineDecorationImage:centerSlice 中涉及到。

四、DecorationImage:centerSlice - 气泡

下面是一个气泡实例,如下所示:

flutter 复制代码
class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Container(
          constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600),
          decoration: const BoxDecoration(
            image: DecorationImage(
              centerSlice: Rect.fromLTWH(10, 15, 3, 3),
              image: AssetImage(FamManager.db),
            ),
          ),
          child: const Text(
            'Flutter 聊化聊化聊化聊化聊化化聊化聊化聊化聊化聊化聊化聊化',
            style: TextStyle(color: Colors.red),
          ).insetsSymmetric(vertical: 20, horizontal: 20),
        ),
      ),
    );
  }
}

从上面代码的第 13 行,我们设置点九图的中心区域大小, 如下图所示:

我们运行上面视图,发现显示不出效果,同时还有异常提醒,如下所示:

我们根据异常提示,很容易找到异常的位置,如下图所示:

在上面图片代码中,有 final FittedSizes fittedSizes = applyBoxFit(fit, inputSize / scale, outputSize); 这一行代码,这行代码是获取到的 fittedSizes 决定着 sourceSize 的值。该方法的底层实现如下:

flutter 复制代码
FittedSizes applyBoxFit(BoxFit fit, Size inputSize, Size outputSize) {
  if (inputSize.height <= 0.0 || inputSize.width <= 0.0 || outputSize.height <= 0.0 || outputSize.width <= 0.0) {
    return const FittedSizes(Size.zero, Size.zero);
  }

  Size sourceSize, destinationSize;
  switch (fit) {
    case BoxFit.fill:
      sourceSize = inputSize;
      destinationSize = outputSize;
      break;
      // .... 省略无关代码
  }
  return FittedSizes(sourceSize, destinationSize);
}

我们开始分析该方传入的参数,如下所示:

  • fit

    我们从上图代码的 fit ??= centerSlice == null ? BoxFit.scaleDown : BoxFit.fill; 可知 applyBoxFit 方法中传入的 fit 参数值是 fill, 所以才隐藏其他类型的无关代码。

  • inputSize

    有上面图片中的

    flutter 复制代码
    Size outputSize = rect.size;
      Size inputSize = Size(image.width.toDouble(), image.height.toDouble());
      Offset? sliceBorder;
      if (centerSlice != null) {
        sliceBorder = inputSize / scale - centerSlice.size as Offset;
        outputSize = outputSize - sliceBorder as Size;
        inputSize = inputSize - sliceBorder * scale as Size;
      }

    这些代码可知 inputSize 的大小就是我们设置中心区域的大小(scale =1 时),则 inputSize传入的值的大小是 Size(3,3)

  • outputSize
    outputSize 是我们组件的尺寸减去裁剪边框 sliceBorder 得到的。则 outputSize传入的值的大小是 Size(355,-4)

上面参数值的结果我们可以通过打断点的形式获取。然后我们以获取到 applyBoxFit 方法传入的参数值,则有

flutter 复制代码
if (inputSize.height <= 0.0 || inputSize.width <= 0.0 || outputSize.height <= 0.0 || outputSize.width <= 0.0) {
  return const FittedSizes(Size.zero, Size.zero);
}

代码进行判定,由于 outputSize.height 的高度是 -4, 而返回 FittedSizes(Size.zero, Size.zero);

  • 从上述代码分析,是outputSize 的高度出现负数导致异常。 而 outputSize 的高度计算公式如下: height = rect.height - (image.height / scale - centerSlice.height), 结合上边的例子将数值带入公式, height : 60 - (67 / 1 - 3 ) = -4,而得到验证。

  • height = rect.height - (image.height / scale - centerSlice.height) 公式想让 height 大于零有两种方式。如下所示:

    • 调整 rect.height, 让组件初始高度增加。比如:我们给文本增加 Padding。 示例如下:

      flutter 复制代码
      class MyWidget extends StatelessWidget {
        const MyWidget({super.key});
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(),
            body: Center(
              child: Container(
                constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600),
                decoration: const BoxDecoration(
                  image: DecorationImage(
                    centerSlice: Rect.fromLTWH(10, 15, 3, 3),
                    image: AssetImage(FamManager.db),
                  ),
                ),
                child: const Text(
                  'Flutter 聊化聊化聊化聊化聊化化聊化聊化聊化聊化聊化聊化聊化',
                  style: TextStyle(color: Colors.red),
                ).insetsSymmetric(vertical: 25, horizontal: 20),
              ),
            ),
          );
        }
      }

      上述代码运行视图如下:

      上边视图显示正常,我们粗略计算一下高度: 25 * 2 + 14 * 1.4 - (67/1 - 3) = 5.9, 我们利用断点得到的高度如图所示:

    • 调整图像的 scale 值, 同时也要等比缩放centerSlice的值 ,让裁剪边距变大。实例如下:

      flutter 复制代码
      class MyWidget extends StatelessWidget {
        const MyWidget({super.key});
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(),
            body: Center(
              child: Container(
                constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600),
                decoration: const BoxDecoration(
                  image: DecorationImage(
                    centerSlice: Rect.fromLTWH(10 / 2, 15 / 2, 3 / 2, 3 / 2),
                    image: AssetImage(FamManager.db),
                    scale: 2,
                  ),
                ),
                child: const Text(
                  'Flutter 聊化聊化聊化聊化聊化化聊化聊化聊化聊化聊化聊化聊化',
                  style: TextStyle(color: Colors.red),
                ).insetsSymmetric(vertical: 20, horizontal: 20),
              ),
            ),
          );
        }
      }

      上述代码运行实例如下:

      我们粗略计算一下:20 * 2 + 14 * 1.4 - (67/2 - 3/2) = 27.6, 我们再利用断点获取一下高度如下所示:

五、Canvas::drawImageNine

下面是应用实例如下:

flutter 复制代码
class MyWidgetCanvas extends StatelessWidget {
  const MyWidgetCanvas({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FutureBuilder(
          future: getImageFromAssets(FamManager.db),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting ||
                snapshot.connectionState == ConnectionState.active) {
              return const Text('加载中');
            }
            return CustomPaint(
              painter: ChatBubblePainter(snapshot.data!),
              child: const Text(
                'Flutter 聊化聊化聊化聊化聊化聊化化聊化聊化聊化化聊化聊化聊化化聊聊化',
                style: TextStyle(color: Colors.red),
              ).insetsSymmetric(vertical: 20, horizontal: 20),
            );
          },
        ),
      ),
    );
  }

  Future<ui.Image> getImageFromAssets(String path) async {
    final ImmutableBuffer immutableBuffer = await rootBundle.loadBuffer(path);
    final ui.Codec codec = await ui.instantiateImageCodecFromBuffer(
      immutableBuffer,
    );
    final ui.FrameInfo frameInfo = await codec.getNextFrame();
    return frameInfo.image;
  }
}

class ChatBubblePainter extends CustomPainter {
  const ChatBubblePainter(this.image);
  final ui.Image image;

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawImageNine(image, const Rect.fromLTWH(10 / 2, 15 / 2, 3 / 2, 3 / 2), Offset.zero & size, Paint());
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

上述代码运行的视图结果如下:

从上述可以看到,气泡的实现使用绘制比UI相对繁琐。大家可以根据需求选择实现方式。

六、鼓励与支持

上面介绍了聊天气泡的实现方式以及在使用过程中遇到问题的总结和解决问题的探索。希望该篇文章功能让你对Flutter 中的点九图有更深入的了解。如果你感觉文章写的还可以,那请留下你的收藏与评论。该篇文章使用的资源管理是由 fam 提供;使用的便捷添加边距是由 idkit 提供。本篇的实例代码地址如下: 聊天气泡实例仓库

相关推荐
安和昂1 小时前
【iOS】bug调试技巧
ios·bug·cocoa
恋猫de小郭1 小时前
IntelliJ IDEA 2024.3 K2 模式已发布稳定版,Android Studio Meerkat 预览也正式支持
android·android studio
emperinter1 小时前
WordCloudStudio Now Supports AliPay for Subscriptions !
人工智能·macos·ios·信息可视化·中文分词
AirDroid_cn2 小时前
iPhone或iPad接收的文件怎么找?怎样删除?
ios·iphone·ipad·文件传输
找藉口是失败者的习惯5 小时前
Jetpack Compose 如何布局解析
android·xml·ui
Swift社区8 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
#摩斯先生8 小时前
Swift从0开始学习 对象和类 day3
ios·xcode·swift
没头脑的ht9 小时前
Swift内存访问冲突
开发语言·ios·swift
#摩斯先生9 小时前
Swift从0开始学习 并发性 day4
ios·xcode·swift
没头脑的ht9 小时前
Swift闭包的本质
开发语言·ios·swift