Objective-C 背景拉伸

背景拉伸的问题

考察如下添加按钮的代码:

- (void)loadView {
  [super loadView];
  UIButton *myBtn = [UIButton buttonWithType:UIButtonTypeCustom];
  [myBtn setContentEdgeInsets:UIEdgeInsetsMake(10, 50, 10, 50)];
  [myBtn setTitle:@"click me" forState:UIControlStateNormal];
  [myBtn setBackgroundImage:[UIImage imageNamed:@"purple_button"]
                   forState:UIControlStateNormal];
  [self.view addSubview:myBtn];
  // ...
}

得到的结果:

圆角被拉伸的效果

圆角被拉伸的效果

这里图片作为背景时,默认被拉伸,造成了图片圆角的变形。

背景平铺

也有不变形的情况,使用图片创建 UIColor 作为背景。但此时背景图片是按原大小平铺开的,代码及效果如下:

UIColor *bgColor =
    [UIColor colorWithPatternImage:[UIImage imageNamed:@"purple_button"]];
[myBtn setBackgroundColor:bgColor];

平铺的背景

平铺的背景

这种适用用来创建纯色背景,比如用一张 1X1 的图片。(为啥不直接使用 UIColor 的纯色?)

精准拉伸

像开头的那种情况,其实只需要拉伸图片的中间部分,保持四个角落即可。

原生 API 中确实也有相应的方法提供支持,那就是 resizableImageWithCapInsets:

通过它可以指定图片四个角落的区域,这四个指定出来的区域在拉伸时不受影响。

保留四个角的示意图

保留四个角的示意图

所以修正后的圆角可以这样来实现:

UIImage *bgImage = [UIImage imageNamed:@"purple_button"];
UIImage *resizedImage =
    [bgImage resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
[myBtn setBackgroundImage:resizedImage forState:UIControlStateNormal];

修正后的效果:

修正后的圆角

修正后的圆角

该方法比较常用的地方还有就是聊天窗口的背景,将聊天气泡四个角保护起来,内部任意拉伸,就可以展示任意聊天文本了。

聊天气泡背景

聊天气泡背景

单个方向上的拉伸

虽然 resizableImageWithCapInsets: 很有用,但也只解决了部分场景。

请看如下的 tab bar 展现:

带弧形的 tab bar 背景

带弧形的 tab bar 背景,图片来自 flutter 文档

因为横向宽度在不同屏幕下是不一样的,很自然地,这里的效果,我们只需要如下图片即可:

tab bar 背景

tab bar 背景

仔细一想,这里的场景需要拉伸的是两边,而不是图片的中间。所以 resizableImageWithCapInsets: 在这里派不上用场,需要自行解决。

我们可以在 UIImage 上扩展一个方法,来解决这里只拉伸两边的需求。

以下实现来自 StackOverflow Klass 的回答

- (UIImage *)pbResizedImageWithWidth:(CGFloat)newWidth andTiledAreaFrom:(CGFloat)from1 to:(CGFloat)to1 andFrom:(CGFloat)from2 to:(CGFloat)to2  {
    NSAssert(self.size.width < newWidth, @"Cannot scale NewWidth %f > self.size.width %f", newWidth, self.size.width);

    CGFloat originalWidth = self.size.width;
    CGFloat tiledAreaWidth = (newWidth - originalWidth)/2;

    UIGraphicsBeginImageContextWithOptions(CGSizeMake(originalWidth + tiledAreaWidth, self.size.height), NO, self.scale);

    UIImage *firstResizable = [self resizableImageWithCapInsets:UIEdgeInsetsMake(0, from1, 0, originalWidth - to1) resizingMode:UIImageResizingModeTile];
    [firstResizable drawInRect:CGRectMake(0, 0, originalWidth + tiledAreaWidth, self.size.height)];

    UIImage *leftPart = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    UIGraphicsBeginImageContextWithOptions(CGSizeMake(newWidth, self.size.height), NO, self.scale);

    UIImage *secondResizable = [leftPart resizableImageWithCapInsets:UIEdgeInsetsMake(0, from2 + tiledAreaWidth, 0, originalWidth - to2) resizingMode:UIImageResizingModeTile];
    [secondResizable drawInRect:CGRectMake(0, 0, newWidth, self.size.height)];

    UIImage *fullImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return fullImage;
}

类似 resizableImageWithCapInsets:,这里扩展的方法也会返回一张拉伸好的新图片。不同之处在于,指定好两边需要拉伸的部分后,得到的是只拉伸两边而保留了中间的图片。

相关资源