iOS图片拉伸(resizableImage)

返回一张受保护且被拉伸的图片

应用场景:聊天窗口的气泡

方法一(弃用):

iOS 5.0以前使用(弃用)这个方法会自动计算出偏向中间的一个1*1的方格也就是被拉伸的地方(默认使用拉伸),一般传入的值为图片大小的一半.

[image stretchableImageWithLeftCapWidth:imageHeight *0.5 topCapHeight:imageHeight *0.5 ];

方法二(常用):

将图片没有保护的部分进行拉伸。
上下左右的值定义了受保护区域,能被拉伸的地方是中间区域,一般我们都设成中心点为了安全。

// 1. 直接传入保护的范围,没有设置图拉伸的模式,默认为UIImageResizingModeTile(瓦片式)就是将图片以原来的大小就行平铺显示
[image resizableImageWithCapInsets:UIEdgeInsetsMake(<#CGFloat top#>, <#CGFloat left#>, <#CGFloat bottom#>, <#CGFloat right#>)];
// 2. 设置图片拉伸的模式
// UIImageResizingModeTile平铺 UIImageResizingModeStretch拉伸
[image resizableImageWithCapInsets:UIEdgeInsetsMake(imageheight * 0.5, imagewidth * 0.5, imageheight * 0.5 -1, imagewidth * 0.5 - 1) resizingMode:UIImageResizingModeTile];

传入的第一个参数capInsetsUIEdgeInsets类型的数据: UIEdgeInsetsMake(<#CGFloat top#>, <#CGFloat left#>, <#CGFloat bottom#>, <#CGFloat right#>)
这个参数是一个结构体,定义如下
typedef struct { CGFloat top, left , bottom, right ; } UIEdgeInsets;
该参数的意思是被保护的区域到原始图像外轮廓的上部,左部,底部,右部的直线距离
比如 UIEdgeInsetsMake(42, 0, 0, 0) 指的是拉伸区,详细点解释就是,从顶部一直到y=42部分是保护区,从y=42一直到底部是拉伸区

image.png

image.png

对于拉伸的方式,有以下两种:

typedef NS_ENUM(NSInteger, UIImageResizingMode) {
UIImageResizingModeTile,(瓦片)
UIImageResizingModeStretch,(伸展)
 };
  • IUIImageResizingModeStretch:拉伸模式,通过拉伸UIEdgeInsets指定的矩形区域来填充图片
  • UIImageResizingModeTile:平铺模式(瓦片),通过重复显示UIEdgeInsets指定的矩形区域来填充图片
     
    来做四个测试,假如我们的原始图像尺寸为60*128
    原始图像

     
    在一个 180 * 384 的窗口进行测试图像
     
    测试的界面设计如图
    正上方为原始图像窗口,用于显示原始图像的效果
    左下方为测试图像窗口,用于显示测试状况的效果
    右下方为对比图像窗口,用于显示默认状况的效果
    测试软件的界面设计

     
    当我们设置capInsets为以下四种值的时候有什么样子的效果:
  • 1:capInsets参数为UIEdgeInsetsMake(0, 0, 0, 0)
  • 2:capInsets参数为UIEdgeInsetsMake(42, 0, 0, 0)
  • 3:capInsets参数为UIEdgeInsetsMake(0, 20, 0, 0)
  • 4:capInsets参数为UIEdgeInsetsMake(42, 20, 42, 20)

拉伸模式

resizingMode参数为UIImageResizingModeStretch

1.capInsets参数为UIEdgeInsetsMake(0, 0, 0, 0)时

当我们向拉伸方法传入该组参数时,代表我们未对原始图像的任何区域进行保护.其拉伸效果如图
在该种情况下,我们发现原始图像按比例放大了3倍,因此我们将该情况当做拉伸模式下的默认状况
 在之后的实验中,我们将该种状况当做参考对象,显示在界面的右下角

Stretch实验结果1.png

2.capInsets参数为UIEdgeInsetsMake(42, 0, 0, 0)时

当我们向拉伸方法传入该组参数时,代表我们对原始图像上部的三分之一进行保护(即红色方块区域).其拉伸效果如图
在该种情况下,我们可以发现拉伸后的图像中:

  • 原始图像中受保护的区域(即红色方块区域)在Y轴方向保持了原比例,但在X轴方向进行了拉伸
  • 原始图像中未受保护的区域,直接按比例进行了拉伸


    Stretch实验结果2.png

3.capInsets参数为UIEdgeInsetsMake(0,20, 0, 0)时

当我们向拉伸方法传入该组参数时,代表我们对原始图像左部的三分之一进行保护(即红色方块区域).其拉伸效果如图
在该种情况下,我们可以发现拉伸后的图像中:

  • 原始图像中受保护的区域(即红色方块区域)在X轴方向保持了原比例,但在Y轴方向进行了拉伸
  • 原始图像中未受保护的区域,直接按比例进行了拉伸


    Stretch实验结果3.png

4.capInsets参数为UIEdgeInsetsMake(42, 20, 42, 20)时

当我们向拉伸方法传入该组参数时,代表我们对原始图像除数字5以外的区域进行保护(即两个红色方块围起来的区域).其拉伸效果如图
在该种情况下,我们可以发现拉伸后的图像中:

  • 在X轴上,由于1被左边和上边的设置保护,3被右边和上边的设置保护,所以只能用中间的2来拉伸,同理最底下的7,8,9
  • 在Y轴上,由于1被左边和上边的设置保护,7被左边和下边的设置保护,所以只能用中间的4来拉伸,同理最底下的3,6,9
  • 由于5没有被保护,所以在整个剩余的空间中,用5进行拉伸填充


    Stretch实验结果4.png

选择平铺模式

resizingMode参数为UIImageResizingModeTile

1.capInsets参数为UIEdgeInsetsMake(0, 0, 0, 0)时

当我们向拉伸方法传入该组参数时,代表我们未对原始图像的任何区域进行保护.其平铺效果如图
在该种情况下,我们发现原始图像按比例填充了相框,因此我们将该情况当做拉伸模式下的默认状况
在之后的实验中,我们将该种状况当做参考对象,显示在界面的右下角

Tile测试1.png

2.capInsets参数为UIEdgeInsetsMake(42, 0, 0, 0)时

当我们向拉伸方法传入该组参数时,代表我们对原始图像上部的三分之一进行保护(即红色方块区域).其平铺效果如图
在该种情况下,我们可以发现拉伸后的图像中:

  • 原始图像中受保护的区域(即红色方块区域)在Y轴方向保持了原比例,但在X轴方向进行了平铺填充
  • 原始图像中未受保护的区域,直接按比例进行了平铺,但不包含被保护的区域(注意观察蓝色箭头所指的区域)


    Tile测试2.png

3.capInsets参数为UIEdgeInsetsMake(0,20, 0, 0)时

当我们向拉伸方法传入该组参数时,代表我们对原始图像左部的三分之一进行保护(即红色方块区域).其平铺效果如图4.2.3
在该种情况下,我们可以发现拉伸后的图像中:

  • 原始图像中受保护的区域(即红色方块区域)在X轴方向保持了原比例,但在Y轴方向进行了平铺填充
  • 原始图像中未受保护的区域,直接按比例进行了平铺,但不包含被保护的区域(注意观察蓝色箭头所指的区域)
     


    Tile测试3.png

4.capInsets参数为UIEdgeInsetsMake(42, 20, 42, 20)时

当我们向拉伸方法传入该组参数时,代表我们对原始图像除数字5以外的区域进行保护(即两个红色方块围起来的区域).其拉伸效果如图
在该种情况下,我们可以发现拉伸后的图像中:

  • 在X轴上,由于1被左边和上边的设置保护,3被右边和上边的设置保护,所以只能用中间的2来平铺,同理最底下的7,8,9
  • 在Y轴上,由于1被左边和上边的设置保护,7被左边和下边的设置保护,所以只能用中间的4来平铺,同理最底下的3,6,9
  • 由于5没有被保护,所以在整个剩余的空间中,用5进行平铺填充


    Tile测试4.png

结论和建议

通过8组实验数据可以观察出拉伸方法在平铺模式和拉伸模式下的变化过程和主要区别,由此我们可知:

  • 对原始图形使用拉伸方法且在四周增加保护区域后,能保证原始图形的四个角不失真,但其余部分的变化细节则有不同
  • 如果原始图像的外轮廓不平整的话,使用拉伸方式会让外轮廓的不平整度放大,使用平铺方式应该能减小这种情况

测试代码:

使用了Storyboard搭建软件界面

 #import "ViewController.h"
 
 @interface ViewController ()
 @property (weak, nonatomic) IBOutlet UIImageView *imageView1;
 @property (weak, nonatomic) IBOutlet UIImageView *imageView2;
 @property (weak, nonatomic) IBOutlet UIImageView *imageView3;
 @end
 
 @implementation ViewController
 
 - (void)viewDidLoad {
 [super viewDidLoad];
 
 //--imageView1的相关内容------------------------------------------------------------------------------------
 
 //相框大小为60 * 128    图片尺寸为60 * 128
 //读取图片
 UIImage *testImage1 = [UIImage imageNamed:@"123456789"];
 
 testImage1 = [testImage1 resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0) resizingMode:UIImageResizingModeStretch];
 self.imageView1.image = testImage1;
 
 //---imageView2的相关内容-----------------------------------------------------------------------------------
 
 //相框为180 * 384    图片尺寸为60 * 128    相框大小为原始图片的3倍
 //读取图片
 UIImage *testImage2 = [UIImage imageNamed:@"123456789"];
 /***********************************************/
 //方法1 resizableImageWithCapInsets:默认是平铺
 //方法2 resizableImageWithCapInsets: resizingMode: 方法
 //  UIImageResizingModeTile,     平铺
 //平铺的概念是保证原图像大小不变,将新图像填充满
 //testImage2 = [testImage2 resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0) resizingMode:UIImageResizingModeTile];
 
 //将上部的三分之一"保护",然后进行显示
 //这代表新图像中,上部的三分之一和原图像一样,而其余部分的填充不会使用原图像上部的三分之一
 // testImage2 = [testImage2 resizableImageWithCapInsets:UIEdgeInsetsMake(42, 0, 0, 0) resizingMode:UIImageResizingModeTile];
 
 //将左部的三分之一"保护",然后进行显示
 //这代表新图像中,上部的三分之一和原图像一样,而其余部分的填充不会使用原图像上部的三分之一
 // testImage2 = [testImage2 resizableImageWithCapInsets:UIEdgeInsetsMake(0,20, 0, 0) resizingMode:UIImageResizingModeTile];
 
 //将四周进行保护后
 //在X轴上,由于1被左边的设置保护,3被右边的设置保护,所以中间只能用2来平铺,同理,7和9之间的8
 //在Y轴上,由于1被上边的设置保护,7被下边的设置保护,所以中间只能用4来平铺,同理,3和9之间的6
 //由于5没有被保护,所以在整个空间中,用5进行平铺来填充剩余的区域
 // testImage2 = [testImage2 resizableImageWithCapInsets:UIEdgeInsetsMake(42, 20, 42, 20) resizingMode:UIImageResizingModeTile];
 
 /***********************************************/
 //resizableImageWithCapInsets: resizingMode: 方法
 //  UIImageResizingModeStretch,  拉伸
 //  拉伸的概念是直接按比例将图片放大到与相框尺寸相同的图像,
 //testImage2 = [testImage2 resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0) resizingMode:UIImageResizingModeStretch];
 
 //将上部的三分之一"保护",然后进行显示
 //保证原图像上部的三分之一在Y轴上不被拉伸,其余部分按剩余比例拉伸
 //testImage2 = [testImage2 resizableImageWithCapInsets:UIEdgeInsetsMake(42, 0, 0, 0) resizingMode:UIImageResizingModeStretch];
 
 //将左部的三分之一"保护",然后进行显示
 //保证原图像左部的三分之一在Y轴上不被拉伸,其余部分按剩余比例拉伸
 //testImage2 = [testImage2 resizableImageWithCapInsets:UIEdgeInsetsMake(0, 20, 0, 0) resizingMode:UIImageResizingModeStretch];
 
 //将四周进行保护后
 //在X轴上,由于1被左边的设置保护,3被右边的设置保护,所以中间只能用2来拉伸,同理,7和9之间的8
 //在Y轴上,由于1被上边的设置保护,7被下边的设置保护,所以中间只能用4来拉伸,同理,3和9之间的6
 //由于5没有被保护,所以在整个空间中,用5进行拉伸来填充剩余的区域
 //testImage2 = [testImage2 resizableImageWithCapInsets:UIEdgeInsetsMake(42, 20, 42, 20) resizingMode:UIImageResizingModeStretch];
 
 //将图片添加到相框
 self.imageView2.image = testImage2;
 
 //-----imageView3的相关内容----------------------------------------------------------------------------------
 
 //读取图片
 UIImage *testImage3 = [UIImage imageNamed:@"123456789"];
 
 /***********************************************/
 //resizableImageWithCapInsets默认是平铺
 //resizableImageWithCapInsets默认情况对比图
 //testImage3 = [testImage3 resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0) resizingMode:UIImageResizingModeTile];
 
 /***********************************************/
 //resizableImageWithCapInsets: resizingMode: 方法
 //  UIImageResizingModeTile,     平铺 (已经测试过了,)
 //  UIImageResizingModeStretch,  拉伸
 //testImage3 = [testImage3 resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0) resizingMode:UIImageResizingModeStretch];
 /***********************************************/
 
 //将图片添加到相框
 self.imageView3.image = testImage3;
 // Do any additional setup after loading the view, typically from a nib.
 }
 
 @end

方法三:

Image Slicing 可视化缩放
相当于一个可视化的resizableImageWithCapInsets,可以用于指定在图片缩放时用来填充的像素。我们可以在XcodeAssets.xcassets目录中选择要slicing的图片,点击图片界面右下方的Show Slicing按钮,在想要设定切片的图片上点击Start Slicing,将出现左中右(或者上中下)三条可以拖动的指示线,通过拖动它们来设定实际的缩放范围。
 
他是可视化的resizableImageWithCapInsets,那么它的capInsets在哪里呢?打开图片对应的.json文件,代码如下:

 {
 "images" : [
 {
 "resizing" : {
 "mode" : "9-part",
 "center" : {
 "mode" : "tile",
 "width" : 42,
 "height" : 92
 },
 "cap-insets" : {
 "bottom" : 0,
 "top" : 95,
 "right" : 41,
 "left" : 0
 }
 },

从文件可以看出来topleftbottomright对应的就是上左下右的指示线,看到modetile,就知道Image Slicing默认为平铺缩放,对于widthHeight是做什么的呢?Width代表的是左侧线(或者上方线)和中间线之间的区域,Height代表的是上侧线和中间线之间的区域。
 
做个测试:

image.png

这是一张2倍像素的图片,我把它拖入Assets.xcassets中。选择右下角Slices的类型为Horizontal and Vertical
image.png

左侧ShowSlicing显示如下:
image.png

 
storyboard拖入一个ImageView,上下左右距离边框均为20。则显示如下:
image.png

 
 
简单来记就是纵横各三条线,线之外的四个角不会拉伸。六条线内部交汇处,亮色区域为拉伸区域,灰色蒙版区域被截取掉,不显示。
Snip20171122_5.png

 
 
如果在Xib或者StoryBoard中可以通过View 的Stretching属性来设置。
image.png

UIEdgeInsetsMake使用例子

在创建button的时候经常需要在button上添加图片和按钮,而且一般情况下我们需要的都是图片在上、标题在下的效果(灰色的为button):

image.png

而当我们设置好title和image后发现是图片居左,标题居右的:
image.png

 
当然这不是我们想要的效果,于是通过设置UIEdgeInsetsMake属性来达到我们想要的效果

 typedef struct UIEdgeInsets {
 CGFloat top, left, bottom, right; // specify amount to inset (positive) for each of the edges. values can be negative to 'outset'
 } UIEdgeInsets;

UIEdgeInsetsMake(<#CGFloat top#>, <#CGFloat left#>, <#CGFloat bottom#>, <#CGFloat right#>)要设置的就是四个边距

image.png

 
图中,蓝色标识为可变区域, 绿色标识为不变区域。
上面的图片来自于一叶博客
可以这样理解:top就是距离顶部的距离在默认的基础上又加上设置的距离,拿开头的button来说,在button仅设置image的时候(如果同时设置了image和title效果又是不一样的,下面会详解,稍安勿躁),image默认是居中的,相当于进行了这样的设置:

 [button setImageEdgeInsets:UIEdgeInsetsMake(0, 0, 0, 0)];

如果我们在这个时候设置了:

 [button setImageEdgeInsets:UIEdgeInsetsMake(20, 0, 0, 0)];

就相当于在图片居中的基础上,将图片又下移了20,如果是-20则是在原有的基础上向上移20,其他几个参数同理,正数就是距相应的边的距离增加,负数就是距相应的距离减少。

为button同时设置图片和标题

当我们给button设置图片和title的时候默认是图片居左,标题距右并排排列的
当我们同时添加图片和标题时,图片默认会向左偏移button的titleLabel的宽度,而标题会向右偏移图片的宽度,既然如此,我们就可以设置偏移量来达到我们想要的任何效果

 [button setImageEdgeInsets:UIEdgeInsetsMake(0, 0, 0, -button.titleLabel.intrinsicContentSize.width)];
 [button setTitleEdgeInsets:UIEdgeInsetsMake(0, -button.currentImage.size.width, 0, 0)];

image.png

设置image偏移量的时候并不是设置的-button.titleLabel.bounds.size.width,而是-button.titleLabel.intrinsicContentSize.width,因为在iOS8之后-button.titleLabel.bounds.size.width的值为0,经多方搜索才找到一个替代的方法,这样设置的意义就是将image的偏移量距右边的距离减少了titleLabel的宽度,由于默认是向左便宜了这么多,我们这样设置之后相当于抵消了左移的偏移量,所以图片就居中了,对于title道理是一样的。这一点明白之后剩下的就没什么难题了,继续,title下移,image上移:
 [button setImageEdgeInsets:UIEdgeInsetsMake(-button.titleLabel.intrinsicContentSize.height, 0, 0, -button.titleLabel.intrinsicContentSize.width)];  [button setTitleEdgeInsets:UIEdgeInsetsMake(button.currentImage.size.height, -button.currentImage.size.width, 0, 0)];
image.png

图片和标题好像有点近,好办,再加一点:

 [button setTitleEdgeInsets:UIEdgeInsetsMake(button.currentImage.size.height + 20, -button.currentImage.size.width, 0, 0)];
image.png

代码:

 UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 160, 160)];
 [button setImage:[UIImage imageNamed:@"ffw_32"] forState:UIControlStateNormal];
 [button setTitle:@"按钮" forState:UIControlStateNormal];
 button.titleLabel.font = [UIFont systemFontOfSize:14];
 [button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
 button.center = self.view.center;
 button.backgroundColor = [UIColor grayColor];
 [self.view addSubview:button];
 NSLog(@"%f",-button.titleLabel.bounds.size.width);
 
 
 [button setImageEdgeInsets:UIEdgeInsetsMake(-button.titleLabel.intrinsicContentSize.height, 0, 0, -button.titleLabel.intrinsicContentSize.width)];
 [button setTitleEdgeInsets:UIEdgeInsetsMake(button.currentImage.size.height + 20, -button.currentImage.size.width, 0, 0)];

例子:在代码中使用UIProgressView 设置了Progress Image和Track Image没能正常的工作

Progress Image:



 
Track Image:



 
效果图:

 
解决方法:
新建个子类继承UIProgressView,并添加两个属性分别是:

 @property (nonatomic,strong)UIImage *ggTrackImage;
 @property (nonatomic,strong)UIImage *ggProgressImage;

分别在set方法里对UIProgressView自带的trackImage,和ProgressImge进行赋值,因为iOS的bug就不能直接拿到属性进行赋值了,取出UIProgressViewSubViews,并根据自己的需求对图片进行拉伸,我这里选择的是平滑拉伸,代码如下:

 -(void)setGgTrackImage:(UIImage *)ggTrackImage
 {
 _ggTrackImage=ggTrackImage;
 UIImageView *trackImageView=self.subviews.firstObject;
 CGRect trackProgressFrame=trackImageView.frame;
 trackProgressFrame.size.height=self.frame.size.height;
 trackImageView.frame=trackProgressFrame;
 CGFloat width = _ggTrackImage.size.width/2.0;
 CGFloat height = _ggTrackImage.size.height/2.0;
 UIImage *imgTrack = [_ggTrackImage resizableImageWithCapInsets:UIEdgeInsetsMake(height, width, height, width)];
 trackImageView.image=imgTrack;
 }
 
 -(void)setGgProgressImage:(UIImage *)ggProgressImage
 {
 _ggProgressImage=ggProgressImage;
 CGFloat width = _ggProgressImage.size.width/2.0;
 CGFloat height = _ggProgressImage.size.height/2.0;
 UIImageView *progressImageView=self.subviews.lastObject;
 CGRect ProgressFrame=progressImageView.frame;
 ProgressFrame.size.height=self.frame.size.height;
 progressImageView.frame=ProgressFrame;
 UIImage *imgProgress = [_ggProgressImage resizableImageWithCapInsets:UIEdgeInsetsMake(height, width, height, width)];progressImageView.image=imgProgress;
 }

搞定,将这个类导入其他使用直接进行图片赋值即可

参考博客:

http://www.jianshu.com/p/3907e6116f41
http://www.jianshu.com/p/af2d471f7b9c
http://www.jianshu.com/p/a577023677c1
http://www.jianshu.com/p/4e4801cbda2b
http://www.jianshu.com/p/901f9b98f7c2
http://www.jianshu.com/p/0d3dbc30fad5

推荐阅读更多精彩内容