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 提供。本篇的实例代码地址如下: 聊天气泡实例仓库

相关推荐
调皮的芋头32 分钟前
iOS各个证书生成细节
人工智能·ios·app·aigc
太空漫步112 小时前
android社畜模拟器
android
神秘_博士2 小时前
自制AirTag,支持安卓/鸿蒙/PC/Home Assistant,无需拥有iPhone
arm开发·python·物联网·flutter·docker·gitee
coooliang2 小时前
【iOS】SwiftUI状态管理
ios·swiftui·swift
陈皮话梅糖@4 小时前
Flutter 网络请求与数据处理:从基础到单例封装
flutter·网络请求
海绵宝宝_4 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
凯文的内存6 小时前
android 定制mtp连接外设的设备名称
android·media·mtp·mtpserver
天若子6 小时前
Android今日头条的屏幕适配方案
android
林的快手8 小时前
伪类选择器
android·前端·css·chrome·ajax·html·json
望佑8 小时前
Tmp detached view should be removed from RecyclerView before it can be recycled
android