专栏首页采云轩[Flutter] UI调试小工具——颜色吸管

[Flutter] UI调试小工具——颜色吸管

作为客户端开发,在应用交付之前,一般都会有 UI 走查这一环节。一方是对颜色不敏感的开发,另一方是对颜色十分敏感的视觉,是否经常出现下列对话:

视觉: 你这个颜色是不是和我设计的不太一样。 开发: 哪里不一样,这个跟设计稿的颜色一模一样。 视觉: 设计稿明明是伸手不见五指的黑,你这个黑的不够纯正。 开发: 你别走,等我看下代码. ......

看代码,不失为一个办法。但是如果你在其他的分支,你需要先 stash 本地代码,再切分支,看代码,找颜色...这个时候,是不是特别想有一个工具,可以立马查看实际显示的颜色。

下面来介绍我是如何制作一个颜色吸管工具,来当场"打脸",当然一般都是"被打脸"。

把大象装到冰箱,需要三步: 1. 打开冰箱,2. 把大象装进去,3. 关上冰箱。那制作一个颜色吸管需要几步呢?

  1. 获取当前屏幕颜色
  2. 选取指定位置
  3. 颜色输出

1. 获取所有像素点的颜色

如何获取当前屏幕的所有像素点的颜色呢,挨个组件去取不太现实。我们可以曲线救国,对当前屏幕截屏,截到的内容就是正在显示的颜色。那么如何截屏呢,Flutter 提供了一个 Widget RepaintBoundary。只需将内容用 RepaintBoundary 包裹起来:

Widget build(BuildContext context) {
  return RepaintBoundary(
    key: _key,
    child: Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(),
    ),
  );
}

在需要截屏的地方,通过 _key 获取到指定 RenderRepaintBoundary ,就可以直接转化为图片,代码如下:

// 根据key获取需要截图的组件
RenderRepaintBoundary boundary = _key.currentContext.findRenderObject();
// 获取当前设备像素比
double pix = window.devicePixelRatio;
// 截屏
var image = await boundary.toImage(pixelRatio: pix);

至此,我们就得到了当前屏幕的截图。图片可以看成是一组按照特殊的数据结构,以 png 图片来讲,一个 png 图片是由文件署名和数据块 (chunk) 两部分组成。数据块又由关键数据块 (critical chunk) 和辅助数据块 (ancillary chunk) 两部分组成。这些数据块包含了该图片的所有信息,例如: 图像的宽高,颜色类型,图像深度,实际图像数据,图像位置信息,最后修改信息等。更多内容可以参考这里 (https://dev.gameres.com/Program/Visual/Other/PNGFormat.htm)。

图像数据块 (IDAT) 属于关键数据块,其中保存了图片的实际图像数据,结合颜色类型(常见的有 RGB、YUV 等)也就可以获取到所有像素的指定颜色。至此,第一步结束。

2. 获取指定像素点的颜色

我们如何获得指定像素点的颜色呢,当然是用手选了,想看哪里点哪里,最为方便。这个实现起来也很简单。将前面截屏得到的图片通过 Image.memory() 方法展示出来,不过需要做个数据转换,代码如下:

// 将Image类型转换为Uint8List类型
ByteData byteData = await image.toByteData(format: ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();

将上面的图片加上一个 GestureDetector widget,在 onPanUpdate 或者 onTapUp 方法中可以轻易的获取到当前的 offset 。那么有了图片所有像素的颜色值,有了图片的偏移量,如何获取指定偏移量位置的颜色值呢?这里就需要用到一个著名的图片处理库 image (https://pub.dev/packages/image)。他提供了getPixelSafe()方法,传入 x、y 值就可以获得当前位置的颜色值类型( Uint32 的 AABBGGRR 格式)。??? 代码如下:

Color getColorFromDragUpdateDetails(Offset globalPosition) {
  int x = globalPosition.dx.toInt();
  int y = globalPosition.dy.toInt();
  double pix = window.devicePixelRatio; //获取当前设备像素比
  int pixel32 = this.temp.getPixelSafe((x * pix).toInt(), (y * pix).toInt());
  int argb = _abgrToArgb(pixel32);
  Color pixelColor = Color(argb);
  print('当前坐标: x:$x, y:$y');
  print('--------ARGB:$argb');
  print('--------HEX:${argb.toRadixString(16).toUpperCase()}');
  print('--------A:${pixelColor.alpha} R:${pixelColor.red} G:${pixelColor.green}B:${pixelColor.blue}');
  return pixelColor;
}

image 库的大致原理如下,将不同后缀的图片按照固定的解析方式,取得其中的数据,图片的像素被编码为 4 字节的 Uint32 整数,根据传入的 x、y 值,去取对应位置的颜色值就可以了。我们再加一个悬浮窗来显示选中的颜色,最终的展示效果如下:

你以为到这里就完了吗,NO~ NO~ NO~虽然满足了我们最初的功能,但是还很难用,在"纤细"的手指遮挡下,我们根本无法做到像素级选择和移动。要是能对选中的地方做个放大就完美了。

3. 放大选中位置

在 Flutter 中,对图片的操作可以通过 ImageFilter 来实现。ImageFilter 提供了两个构造方法:

// 提供一个可以实现高斯模糊的图片滤镜
ImageFilter.blur({ double sigmaX = 0.0, double sigmaY = 0.0 })
// 通过应用一个矩阵的变换对图片做操作
ImageFilter.matrix(Float64List matrix4, { FilterQuality filterQuality = FilterQuality.low })

我们在这里可以使用 ImageFilter.matrix() 来对图片的的纹理做矩阵变换来实现图片的放大效果。放大效果分两步走:

3.1 获得放大指定位置后的图片矩阵

这个很好理解,我们将上一阶段截屏得到的图片用 GestureDetector 包裹,在 onPanUpdate 时,取到对应位置的坐标,然后对截图进行矩阵变换,获得变换过后的纹理:

// 手指移动时
onPanUpdate: (detail) {
  setState(() {
    // 获取当前选中点的颜色值
    Color pixelColor =
      getColorFromDragUpdateDetails(detail.globalPosition);
    choiceColor = pixelColor;
    choiceColorString = "0x${pixelColor.value.toRadixString(16).toUpperCase()}";
    // 当前选中的点
    _magnifierPosition =
      detail.globalPosition - _size.center(Offset.zero);
    double newX = detail.globalPosition.dx;
    double newY = detail.globalPosition.dy;
    // 矩阵变换
    final Matrix4 newMatrix = Matrix4.identity()
      ..translate(newX, newY)
      ..scale(scale, scale)
      ..translate(-newX, -newY);
    // 保存变换过后的矩阵
    matrix = newMatrix;
  });
}

3.2 创建一个跟随组件 & 应用矩阵

这个是常规操作啦,使用 StackPositioned 就可以实现一个跟随手势的组件,然后创建一个 BackdropFilter 组件,将上面变换过的矩阵应用到 ImageFilter 上。在位置变化时,实时 setState, 触发组件的刷新,就可以做到啦。特别强调的是,由于获取到的矩阵是整张图片变换的完整矩阵,这里需要使用 ClipRRect 组件,将不需要显示的部分裁减掉。

Visibility(
  visible: _visible,
  child: Positioned(
    left: _magnifierPosition.dx,
    top: _magnifierPosition.dy,
    child: ClipRRect(
      borderRadius: BorderRadius.circular(_size.longestSide),
      child: BackdropFilter(
        filter: ImageFilter.matrix(matrix.storage),
        child: CustomPaint(
          painter: painter,
          size: _size,
        ),
      ),
    ),
 ))

最终效果如下所示:

4.遇到的问题

到这里,这篇文章就基本结束了,这里记录一下遇到的一些问题:

4.1 颜色编码

在获取图片颜色时,获取到的实际是 AABBGGRR 颜色类型,而 Flutter 一般使用的是 AARRGGBB 颜色类型,这里还需要做一个转换,具体代码如下:

// AABBGGRR -> AARRGGBB
int _abgrToArgb(int oldValue) {
  int newValue = oldValue;
  newValue = newValue & 0xFF00FF00; //open new space to insert the bits
  newValue = ((oldValue & 0xFF) << 16) | newValue; // change BB
  newValue = ((oldValue & 0x00FF0000) >> 16) | newValue; // change RR
  return newValue;
}
// int类型的值转换为16进制的hex值
String hexColor = argb.toRadixString(16).toUpperCase();

实际更为常见的还有 YUV 类型。YUV 又有好多子类型,例如 YUV420、YUV421 等,读者可以自行了解相关资料。此处再扩展一个问题,如何计算一张图片的实际内存大小? 图片的内存大小是和分辨率和颜色类型有关的,分辨率决定了有多少个像素点,颜色类型决定了一个像素点存储了多大的数据,一般来讲,图片内存大小的计算公式 宽度*高度 *bytesPerPixel / 8。例如一张 1000*1000 分辨率,RGB 颜色类型的图片,通常情况下, 图片自动缩放到 2 的 n 次方大小,RGB 颜色空间下每个颜色分量由 8 位组成,但是通常情况下颜色还有 alpha 通道也是 8 位 也就是传说中的 RGBA,所以总共是 32 位。所以一般图片的计算公式是 w*h*4。该张图片实际占用的大小就是 1024*1024 * 4 / 1024 / 1024 = 4MB。当时实际情况可能会比这个更为复杂,RGBA 类型也还有许多更加节省内存的变种,例如 RGBA8888、RGBA4444 等。图片包含的其他 chunk 也会占用一定的内存大小,此处只是做一个补充,读者可自行学习。

4.2 获取指定位置的颜色

在截图时,我们传入了 double pix = window.devicePixelRatio; 设备像素比。以 iPhone11 为例,pix 的值为 2.0。在后面我们获取到设备的触摸点时,触摸点的位置是以物理尺寸为准,所以去取图片也要将该 pix 值应用进去。

4.3 矩阵变换

此例中,我们要做的是放大图片的指定位置。通过矩阵来表示的话,就是矩阵的平移和缩放的组合。我们需要先将矩阵平移到需要缩放的点, 缩放, 缩放完成后再平移回去。因为缩放默认是以原点坐标为基准,原点坐标默认是左上角的 (0, 0) 位置。所以我们需要缩放的点平移到原点,再缩放,缩放完之后恢复现场。矩阵变化很有意思,此处不再做扩展,读者可以自行挖掘更多玩法.

5.写在最后

纵观全局,没有用到什么特别难或者高深的技术,但是组合出来的这个小工具却很有实用价值。当然在 UI 还原度的提升和 UI 开发效率方面还有很多其他可以做的事情,例如: 检测组件大小、组件的位置、组件层级等多种方式.

在提升 UI 还原度的和开发效率方面,业界一些大厂在这方面已经走得挺远了,例如爱奇艺,他们已经做到了 UI 半自动验收 (https://mp.weixin.qq.com/s/K9p8986Gq1DoQ1fUYivPrg)。大致实现思路是利用 AI 来识别组件边界,然后通过控件匹配算法和间距选择算法来建立开发页面与设计页面的控件之间的一对一关系和间距关系。然后将这些关系一一比对,就能够输出匹配的结果。但是这种方式在精细度和准确度上面肯定不如使用各种工具进行测量,但是胜在效率高。

我觉得未来的 UI 自动化验收一定是 AI 识别为主的自动验收模式和人工测量为主的个性化验收模式相结合。在页面结构清晰,组件不多的页面以自动验收为主,在页面结构复杂的页面以人工验收为主。这样才能做到效率和准确度的最好结合。

最后,用我不知道从哪里看到的一句话来结束吧,共勉~

技术是为了解决业务问题的,只有在实现业务、给人们带来便利的前提下,技术的存在才有意义。

本文分享自微信公众号 - 政采云前端团队(Zoo-Team),作者:北羽

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-03-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 12种Flutter开发工具推荐

    谷歌的 Flutter 跨平台应用开发框架正迅速成为移动跨平台开发人员的最爱。尽管 Flutter 由于谷歌的大力支持正在迅速成熟,但社区仍然没有太多第三方开发...

    深度学习与Python
  • 动手编写你的第一个 Flutter 应用

    我将带领大家尝试编写一个 Flutter 应用,感受一下 Flutter 开发的语法特点和运行效率。

    CSDN技术头条
  • Flutter 实现原理及在马蜂窝的跨平台开发实践

    在马蜂窝旅游 App 很多业务场景里,我们尝试过一些主流的跨平台开发解决方案, 比如 WebView 和 React Native,来提升开发效率和用户体验。但...

    马蜂窝技术
  • 【译】Flutter架构综述

    Flutter是一个跨平台的UI工具包,它的设计目的是允许跨iOS和Android等操作系统的代码重用,同时也允许应用程序直接与底层平台服务对接。其目标是让开发...

    用户1907613
  • Flutter性能优化

    App 流畅性的关键指标有 UI帧率,GPU帧率,我们期望它能达到 60fps,也就是16ms每帧。

    剑行者
  • Flutter 1.22 正式发布

    我们很高兴推出最新版本的Flutter,它广泛支持iOS 14和Android11。Flutter 1.22在以前版本的基础上构建,使开发人员能够从一个代码库为...

    老孟Flutter
  • 基于小程序技术栈的微信客户端跨平台实践

    本文主要内容整理自 GMTC 2019 分享《基于小程序技术栈的微信客户端跨平台实践》  https://gmtc2019.geekbang.org/pres...

    微信终端开发团队
  • Flutter 凉了吗?

    原文:https://www.freecodecamp.org/news/why-i-think-flutter-is-the-future-of-mobile...

    开发者技术前线
  • Flutter EasyLoading - 让全局Toast/Loading更简单

    Flutter是Google在2017年推出的一套开源跨平台UI框架,可以快速地在iOS、Android和Web平台上构建高质量的原生用户界面。Flutter发...

    huangjianke

扫码关注云+社区

领取腾讯云代金券