likes
comments
collection
share

如何写对 Flutter centerSlice

作者站长头像
站长
· 阅读数 12

前言

作为一个 iOS 开发者来实现 Flutter 点 9 的拉伸效果开始可能有些不知所措,总是不能调试出想要的效果,所以觉得有必要了解下点 9 图的实现原理。

先看 API

iOS 的 API 是设定一个 UIEdgeInsetsUIEdgeInsets 的参数是 left``top``right``bottom,也就是距离图片边缘的距离。

- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets API_AVAILABLE(ios(5.0));

而 Flutter 的 API 则是设置一个中心的矩形区域

/// The center slice for a nine-patch image.
///
/// The region of the image inside the center slice will be stretched both
/// horizontally and vertically to fit the image into its destination. The
/// region of the image above and below the center slice will be stretched
/// only horizontally and the region of the image to the left and right of
/// the center slice will be stretched only vertically.
final Rect? centerSlice;

开始有些迷惑了,这个和 iOS 中的是一个意思吗?

再看源码

ImageDecorationImage 都有 centerSlice 属性,最终都会执行 paintImage 方法

void paintImage({
  ...
  Rect? centerSlice,
  ...
})

如果 centerSlice 不为 null ,就会执行 drawImageNine 方法,其中 center 参数即 centerSlice, dst则是最终图片拉伸后的大小。

/// Draws the given [Image] into the canvas using the given [Paint].
///
/// The image is drawn in nine portions described by splitting the image by
/// drawing two horizontal lines and two vertical lines, where the `center`
/// argument describes the rectangle formed by the four points where these
/// four lines intersect each other. (This forms a 3-by-3 grid of regions,
/// the center region being described by the `center` argument.)
///
/// The four regions in the corners are drawn, without scaling, in the four
/// corners of the destination rectangle described by `dst`. The remaining
/// five regions are drawn by stretching them to fit such that they exactly
/// cover the destination rectangle while maintaining their relative
/// positions.
void drawImageNine(Image image, Rect center, Rect dst, Paint paint);

从注释中我们可以了解到,绘制点 9 图会根据根据 center 将图片分成 3 x 3 的 9 个区域(这应该就是点 9 图命名的由来),边角的四个区域不会拉伸,其他五个区域则会拉伸来铺满剩余 dst

这样来看其实 centerSlice 和 iOS 中的 capInsets 本质上是一样的,只是参数的定义不同。

实践一下

原图 30 x 30, scale 为 3.0

如何写对 Flutter centerSlice

centerSlice 设置为 Rect.fromLTWH(30, 30, 30, 30),拉伸后的效果如下图所示

如何写对 Flutter centerSlice

可得出以下结论:

  • 1、3、7、9 四个边角不会拉伸
  • 2、8 会在水平方向拉伸
  • 4、6 会在垂直方向拉伸
  • 5 会在水平和垂直方向拉伸

centerSlice.fromLTRB

iOS 开发者可能会下意识的的将 Rect.fromLTRB 理解成 UIEdgeInsets 的含义,但这里的 R B 和 UIEdgeInsets 不是同一个概念,这里的 R B 指的是矩形右边和底部相对于坐标轴初始点 (0, 0) 的位置。

/// Construct a rectangle from its left, top, right, and bottom edges.
const Rect.fromLTRB(this.left, this.top, this.right, this.bottom);

如何写对 Flutter centerSlice

以上图的气泡为例,iOS 是 UIEdgeInsetsMake(10, 20, 10, 4),flutter 是 Rect.fromLTRB(10, 20, 110, 26),如果设置为 Rect.fromLTRB(10, 20, 10, 4), 就会遇到以下错误

centerSlice was used with a BoxFit that does not guarantee that the image is fully visible.

paintImage 方法的源码可以看到是一下 assert

if (centerSlice != null) {
  // We don't have the ability to draw a subset of the image at the same time
  // as we apply a nine-patch stretch.
  assert(sourceSize == inputSize,
      'centerSlice was used with a BoxFit that does not guarantee that the image is fully visible.');
}

其中 inputSize 初始值为图片的像素值,经过计算后变成了 centerSlice 的 size {0, -16}

Offset? sliceBorder;
sliceBorder = inputSize / scale - centerSlice.size as Offset;
inputSize = inputSize - sliceBorder * scale as Size;
/// 等价于以下计算
inputSize = inputSize - (inputSize / scale - centerSlice.size) * scale as Size;
inputSize = inputSize - inputSize + centerSlice.size * scale as Size;
inputSize = centerSlice.size * scale as Size;

sourceSize 来源于 fittedSizes.source,此时是 {0, 0}

final FittedSizes fittedSizes = applyBoxFit(fit, inputSize / scale, outputSize);

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);
  }
  ...
}