前言
作为一个 iOS 开发者来实现 Flutter 点 9 的拉伸效果开始可能有些不知所措,总是不能调试出想要的效果,所以觉得有必要了解下点 9 图的实现原理。
先看 API
iOS 的 API 是设定一个 UIEdgeInsets
,UIEdgeInsets
的参数是 left``top``right``bottom
,也就是距离图片边缘的距离。
objc
- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets API_AVAILABLE(ios(5.0));
而 Flutter 的 API 则是设置一个中心的矩形区域
dart
/// 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 中的是一个意思吗?
再看源码
Image
、DecorationImage
都有 centerSlice
属性,最终都会执行 paintImage
方法
dart
void paintImage({
...
Rect? centerSlice,
...
})
如果 centerSlice
不为 null ,就会执行 drawImageNine
方法,其中 center
参数即 centerSlice
, dst
则是最终图片拉伸后的大小。
dart
/// 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
centerSlice 设置为 Rect.fromLTWH(30, 30, 30, 30)
,拉伸后的效果如下图所示
可得出以下结论:
- 1、3、7、9 四个边角不会拉伸
- 2、8 会在水平方向拉伸
- 4、6 会在垂直方向拉伸
- 5 会在水平和垂直方向拉伸
centerSlice.fromLTRB
iOS 开发者可能会下意识的的将 Rect.fromLTRB
理解成 UIEdgeInsets
的含义,但这里的 R B 和 UIEdgeInsets
不是同一个概念,这里的 R B 指的是矩形右边和底部相对于坐标轴初始点 (0, 0) 的位置。
dart
/// Construct a rectangle from its left, top, right, and bottom edges.
const Rect.fromLTRB(this.left, this.top, this.right, this.bottom);
以上图的气泡为例,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
dart
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}
dart
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}
dart
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);
}
...
}