iOS UIButton 的 imageEdgeInsets 和 titleEdgeInsets

coverImage.jpg

前言

我们在日常开发中经常用到 UIButton,有时候需要 icon 和文字一起展示,所以需要做一些定制,比如以下几种情况:

  • 图片在上,文字在下;
  • 图片在下,文字在上;
  • 图片在左,文字在右;
  • 图片在右,文字在左;

其实就是改变 UIButton 内部的 imageViewtitleLabel 的位置而已,默认的位置是:imageView 在左 titleLabel 在右且紧密相连,在水平方向: imageViewtitleLabel 整体居中,在垂直方向:垂直居中,如下图:

默认布局.png

这种布局很多时候并不是我们想要的,可能我们还需要imageViewtitleLabel之间加点间距,再或者改变一下两者的位置等等,这时候就用到了 UIButtonimageEdgeInsetstitleEdgeInsets 两个属性了,但是经过一番折腾,很多小伙伴还是不明白这里面的原理和套路,尽管网上有关这俩属性的资料很多,都是教大家如何设置,但很多资料并没有说清楚真正的本质,今天就给大家讲讲imageEdgeInsetstitleEdgeInsets 两个属性的本质,希望大家看完这篇文章能有所收获。

关键知识点

  1. UIEdgeInsetsMake所对应的4个参数分别是上、左、下、右
UIEdgeInsetsMake(CGFloat top, CGFloat left, CGFloat bottom, CGFloat right)
  1. UIButton 的属性 titleEdgeInsetsimageEdgeInsets顾名思义其实就是 titleLabelimageView 的内边距,可以理解为向内压缩,当然还少了一个前提:如果是正值则为向内压缩,如果是负值,则为向外扩张。为了便于理解,大家也可以可参考 CSS 的 padding) 看图说话:

    edgeInsets说明.png

    控件的有效活动区域(名字自己起的):此区域就是titleLabelimageView所能展示的区域,它们各自有各自的区域,是互不干扰的,这点要注意一下。如果区域容纳不了真实的内容,就会被压缩,具体如何压缩后面会具体讲解
    默认情况下: titleEdgeInsetsimageEdgeInsets各个边的值都是0,此时,黄色区域覆盖蓝色区域;

  2. UIControl 的属性 contentVerticalAlignmentcontentHorizontalAlignment,这两个属性都是枚举类型的,定义了 contentUIControl 内部是如何定位布局的,分为垂直方向和水平方向,此处的定位就是相对于上面👆所说的控件的有效活动区域,即在有效活动区域内,控件的垂直方向和水平方向对齐方式。(此处还是要注意,这个对齐也是指每个控件在自己单独的有效活动区域的对齐,并非是整体。举个例子:titleLabel 在自己的有效活动区域内居中对齐,imageView 在自己的有效活动区域内居中对齐,两个区域可能有交集,也可能无交集,互不干扰,并不是 titleLabelimageView 整体在 UIButton 内居中对齐)具体枚举值如下:

typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
    UIControlContentVerticalAlignmentCenter  = 0,
    UIControlContentVerticalAlignmentTop     = 1,
    UIControlContentVerticalAlignmentBottom  = 2,
    UIControlContentVerticalAlignmentFill    = 3,
};

typedef NS_ENUM(NSInteger, UIControlContentHorizontalAlignment) {
    UIControlContentHorizontalAlignmentCenter = 0,
    UIControlContentHorizontalAlignmentLeft   = 1,
    UIControlContentHorizontalAlignmentRight  = 2,
    UIControlContentHorizontalAlignmentFill   = 3,
    UIControlContentHorizontalAlignmentLeading  API_AVAILABLE(ios(11.0), tvos(11.0)) = 4,
    UIControlContentHorizontalAlignmentTrailing API_AVAILABLE(ios(11.0), tvos(11.0)) = 5,
};

初始默认值:中心对齐(垂直方向居中,水平方向居中)

 button.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
 button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
  1. 找出 titleLabelimageView 各自的初始有效活动区域,即 button.imageEdgeInsets = UIEdgeInsetsZerobutton.titleEdgeInsets = UIEdgeInsetsZero 时它们各自的有效活动区域,为什么要找到它们的初始有效活动区域呢,因为我们设置的 imageEdgeInsetstitleEdgeInsets 都是针对初始有效活动区域做的操作。

  2. AutoLayoutContent Hugging Priority(抗拉伸优先级) 和 Content Compression Resistance Priority(抗压缩优先级),从字面意思可以看出 Content Hugging Priority 是内容拥抱优先级(Hugging:拥抱,抱紧),可以理解为内容与承载内容的视图紧凑的抱紧,比如说 label 里的文字内容是充满整个 label 的; Content Compression Resistance Priority 是内容压缩阻力优先级。这两个属性的值都是越大,越抗拉伸和抗压缩,属于 float 类型的,取值范围是1--1000。不过还有4种系统类型的值如下:

typedef float UILayoutPriority NS_TYPED_EXTENSIBLE_ENUM;
// A required constraint.  Do not exceed this.
static const UILayoutPriority UILayoutPriorityRequired NS_AVAILABLE_IOS(6_0) = 1000; 
// This is the priority level with which a button resists compressing its content.
static const UILayoutPriority UILayoutPriorityDefaultHigh NS_AVAILABLE_IOS(6_0) = 750; 
// This is the priority level at which a button hugs its contents horizontally.
static const UILayoutPriority UILayoutPriorityDefaultLow NS_AVAILABLE_IOS(6_0) = 250; 
// When you send -[UIView systemLayoutSizeFittingSize:], the size fitting most closely to the target size (the argument) is computed.  UILayoutPriorityFittingSizeLevel is the priority level with which the view wants to conform to the target size in that computation.  It's quite low.  It is generally not appropriate to make a constraint at exactly this priority.  You want to be higher or lower.
static const UILayoutPriority UILayoutPriorityFittingSizeLevel NS_AVAILABLE_IOS(6_0) = 50; 

获取和设置

- (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);

- (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);

以上这些知识点是大家需要了解的,弄懂这些,对后面的操作会有很大的帮助。接下来我们研究 UIButtonimageEdgeInsetstitleEdgeInsets 是如何向内压缩和向外扩展的。

如何找出titleLabelimageView 各自的初始有效活动区域

提示:利用 UIControl 的属性 contentVerticalAlignmentcontentHorizontalAlignment
UIButton 继承自 UIControl ,所以我们可以通过设置UIButton 的这两个属性,得到titleLabelimageView 各自的初始有效活动区域,前面我们说过,这两个属性是针对它们各自的有效活动区域而言的,并不是UIButton 本身,所以我们在不改变imageEdgeInsetstitleEdgeInsets 时,分别设置它们在有效区域内左上对齐和右下对齐,就可以知道它们的有效区域范围了,如果理解不了,可以单独设置4次,分别是居上对齐、居左对齐、居下对齐、居右对齐,也可以得到相同的结果;还是数形结合更直观一点,我请大家看图,你们自备茶🍵吧!😆😆😆

各种对齐方式对比.png

通过上图,大家有没有发现,不管是什么对齐方式, imageView 的活动区域始终是在 {{0, 0}, {43, 40}} ({{x, y}, {width, height}})这个区域内进行的,同时也是对齐方式为 Fill 模式下各自的 Frame (由于精度问题,会有小于0.5的误差在内,属于正常情况,不影响结果)。而 titleLabel 的活动区域始终是在 {{30, 0}, {70, 40}} 这个区域内进行的,也就是 button 的宽度减去了 imageView 的宽度的范围,但是在对齐方式为 Fill 模式下, titleLabel 的活动区域并非是上图中绿色背景区域那些,真实值应该是再加上右边与 button 的间距。

仔细的小伙伴们可能也发现了,imageViewtitleLabel 的初始活动区域的上边界和下边界正好是 button 的上下边界,但是左右边界好像确定不下来啊,再看上面的图,前两个对齐方式:
左上对齐的时候imageView 的左边界是 button 左边界, titleLabel 的左边界是 imageView 的宽度;
右下对齐的时候titleLabel 的右边界是 button 右边界, imageView 的右边界是 button 的宽度减掉 titleLabel 的宽度;

小结(划重点)

// 有效活动区域各边界的初始位置
CGFloat buttonWidth = button.bounds.size.width;
CGFloat imageWidth = CGRectGetWidth(button.imageView.frame);
CGFloat labelWidth = CGRectGetWidth(button.titleLabel.frame);

imageView :top:0  left:0           bottom:0  right:buttonWidth-labelWidth
titleLabel:top:0  left:imageWidth  bottom:0  right:buttonWidth

下面我用图给大家讲解下找imageView 的右边界和titleLabel 的左边界,其它边界上面已经说得很清楚了,这里就不再赘述了。
图解:

imageView的有效活动区域.png

titleLabel的有效活动区域.png
两者的有效活动区域.png
默认情况下,水平和垂直方向都居中的展示.png

以上图应该可以让大家完全明白了吧!

如何设置正确 imageEdgeInsets 和 titleEdgeInsets

上面我们已经找到了 imageView 和 titleLabel 有效活动区域的初始边界,接下来我们就通过设置 imageEdgeInsets 和 titleEdgeInsets 来满足我们的需求。

  • 图片在上,文字在下;
  • 图片在下,文字在上;
  • 图片在左,文字在右;(默认情况)
  • 图片在右,文字在左;

imageEdgeInsetstitleEdgeInsetsUIEdgeInsets 类型的,上面关键知识点里面已经说了,

  1. UIEdgeInsetsMake所对应的4个参数分别是上、左、下、右
UIEdgeInsetsMake(CGFloat top, CGFloat left, CGFloat bottom, CGFloat right)
  1. imageEdgeInsetstitleEdgeInsets的各个参数值的正负决定是向内压缩还是向外扩展(如果是正值则为向内压缩,如果是负值,则为向外扩张。

就拿 imageEdgeInsets 举例说明一下:
对于 imageEdgeInsets.top

  • 为 0 时,imageView 有效活动区域的上边界不动
  • 为正值时,imageView 有效活动区域的上边界向内收缩,即下移
  • 为负值时,imageView 有效活动区域的上边界向外扩张,即上移

对于 imageEdgeInsets.left

  • 为 0 时,imageView 有效活动区域的左边界不动
  • 为正值时,imageView 有效活动区域的左边界向内收缩,即右移
  • 为负值时,imageView 有效活动区域的左边界向外扩张,即左移

对于 imageEdgeInsets.bottom

  • 为 0 时,imageView 有效活动区域的下边界不动
  • 为正值时,imageView 有效活动区域的下边界向内收缩,即上移
  • 为负值时,imageView 有效活动区域的下边界向外扩张,即下移

对于 imageEdgeInsets.right

  • 为 0 时,imageView 有效活动区域的右边界不动
  • 为正值时,imageView 有效活动区域的右边界向内收缩,即左移
  • 为负值时,imageView 有效活动区域的右边界向外扩张,即右移

titleEdgeInsets同理

举例说明

例1:图片在左,文字在右,且间距为6;
分析:默认情况就是图片在左,文字在右,所以我们只需要加间距就行,在对齐方式(水平和垂直方向)都为居中对齐时,我们只需要让 imageView 有效活动区域保持大小不变,整体往左移间距的二分之一,也就是3, titleLabel 有效活动区域保持大小不变,整体往右移间距的二分之一即可实现。

// imageView 与 titleLabel 的间距
CGFloat spacing = 6.f;
button.imageEdgeInsets = UIEdgeInsetsMake(0, -spacing/2, 0, spacing/2);
button.titleEdgeInsets = UIEdgeInsetsMake(0, spacing/2, 0, -spacing/2);
加间距.png

这个图片是用电脑在模拟器上直接截的图,截下来是正常居中的,但是传到这里,貌似看起来 imageView 的顶部间距比下面底部间距大一点点似的,不过大家也可以自己跑个 demo 看看,数据都是没问题的。可以直接通过 Xcode 的 Debug View Hierarchy 功能看视图层次结构和它们的 frame。

Debug View Hierarchy.png

查看imageView的frame.png

问题:上面的方案虽然实现了我们的需求,但是你们有没有发现,对 imageViewtitleLabel 的有效活动区域保持大小不变,整体进行了左移和右移,导致了它们各自的有效活动区域跑出了 button 的外面,可以先后设置水平方向居左和居右对齐,看看会出现什么效果。

不完美的效果.png

尽管实际使用的时候不会这么操作,但是对于追求完美的人来说实在是不能忍啊!

思考:除了上面的方案,还有没有其他方案?
方案一:只左移 imageView 有效活动区域的右边界,和右移 titleLabel 有效活动区域的左边界,间距多少就移多少(在水平对齐方式为居中对齐的时候, imageView 有效活动区域的右边界左移 spacing , imageView 就会整体左移 spacing/2,同理,titleLabel 会右移 spacing/2,整体而言间距就是 spacing)。

    // imageView 与 titleLabel 的间距
    CGFloat spacing = 6.f;
    button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, spacing);
    button.titleEdgeInsets = UIEdgeInsetsMake(0, spacing, 0, 0);

此方案在水平对齐方式为居左或居右的时候 imageViewtitleLabel 不会超出 button 的边界。

方案二:既然我们知道了各自初始的有效活动区域,通过加间距,我们也可以算出来最终 imageViewtitleLabel 的位置,那我们直接把 imageViewtitleLabel 各自有效活动区域压缩或扩展到与最终 imageViewtitleLabel 的位置(即frame)一致不就行了,针对各自有效活动区域的每条边界做移动操作就可满足,听起来是要做很多的计算啊,不过此方案有一个好处就是,最终结果不会受 button 的对齐属性 contentHorizontalAlignmentcontentVerticalAlignment 影响,因为有效活动区域与它们各自本身的 boundSize 一致了,不论居左,居右,居上,居下,居中还是填充, imageViewtitleLabel 的位置都只有一种情况。

    CGFloat buttonHeight = button.bounds.size.height;
    CGFloat buttonWidth = button.bounds.size.width;
    
    CGFloat imageHeight = CGRectGetHeight(button.imageView.frame);
    CGFloat imageWidth = CGRectGetWidth(button.imageView.frame);
    
    CGFloat labelWidth = CGRectGetWidth(button.titleLabel.frame);
    CGFloat labelHeight = CGRectGetHeight(button.titleLabel.frame);

    // imageView 与 titleLabel 的间距
    CGFloat spacing = 6.f;
    
    // 垂直居中对齐时,imageView 顶部和底部距离 button 的间距
    // 也是 imageView 有效活动区域上边界下移的距离和下边界上移的距离
    CGFloat imageSpaceVertical = (buttonHeight-imageHeight) * 0.5f;
    
    // 垂直居中对齐时,titleLabel 顶部和底部距离 button 的间距
    // 也是 titleLabel 有效活动区域上边界下移的距离和下边界上移的距离
    CGFloat labelSpaceVertical = (buttonHeight-labelHeight) * 0.5f;
    
    // 水平居中对齐时,imageView 和 titleLabel 加间距后,各自距离 button 的间距(两者是等间距的)
    // 也是 imageView 有效活动区域左边界右移的距离   titleLabel 有效活动区域右边界左移的距离
    CGFloat spaceHorizontal = (buttonWidth-imageWidth-labelWidth-spacing) * 0.5f;
    
    // imageView 最终的 frame
    CGRect imageViewFrame = CGRectMake(spaceHorizontal, imageSpaceVertical, imageWidth, imageHeight);
    
    CGFloat titleLabel_X = CGRectGetMaxX(imageViewFrame) + spacing;
    // titleLabel 最终的 frame
    CGRect titleLabelFrame = CGRectMake(titleLabel_X, labelSpaceVertical, labelWidth, labelHeight);
    
    // imageView 有效活动区域右边界左移的距离
    CGFloat imageViewRightEdgeLeftSpace = (buttonWidth-labelWidth) - CGRectGetMaxX(imageViewFrame);
    
    // titleLabel 有效活动区域左边界右移的距离
    CGFloat titleLabelLeftEdgeRightSpace = titleLabel_X - imageWidth;
    
    button.imageEdgeInsets = UIEdgeInsetsMake(imageSpaceVertical, spaceHorizontal, imageSpaceVertical, imageViewRightEdgeLeftSpace);
    button.titleEdgeInsets = UIEdgeInsetsMake(labelSpaceVertical, titleLabelLeftEdgeRightSpace, labelSpaceVertical, spaceHorizontal);

    // 对齐方式:左上
    button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
    button.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;
    
    // 对齐方式:右下
    //button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
    //button.contentVerticalAlignment = UIControlContentVerticalAlignmentBottom;

    // 对齐方式:填充
    //button.contentVerticalAlignment = UIControlContentVerticalAlignmentFill;
    //button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill;

虽然看起来计算的偏移量有点多,但是如果理解了这个方案的逻辑,那么今天讲的本质也就理解了,此方案就当一个测试案例吧,以上这几种方案都是在水平和垂直居中对齐方式下进行计算的,大家也可以自己动手练习一下其它方式,更有助于理解。

例2:图片在上,文字在下,紧密相连,不加间距;
分析:上图下字,这种类似于叠罗汉,此时在水平方向,只有 imageViewtitleLabel ,所以为了简化计算,水平方向的对齐方式采用居中,设置各自水平方向的有效活动区域为整个button 的宽度即可,至于垂直方向,则只需计算出最终的 imageViewtitleLabelframe.origin.y值即可。

    // 垂直居中对齐时,imageView 和 titleLabel ,各自距离 button 的间距(两者是等间距的)
    // 也是 imageView 有效活动区域上边界下移的距离   titleLabel 有效活动区域下边界上移的距离
    CGFloat spaceVertical = (buttonHeight-imageHeight-labelHeight) * 0.5f;
    
    // imageView 最终的 frame.origin.y
    CGFloat imageView_Y = spaceVertical;
    // titleLabel 最终的 frame.origin.y
    // 也是 titleLabel 有效活动区域上边界下移的距离
    CGFloat titleLabel_Y = spaceVertical + imageHeight;
    
    // imageView 有效活动区域下边界上移的距离
    CGFloat imageBottomEdgeTopSpace = buttonHeight-titleLabel_Y;
    // imageView 有效活动区域右边界右移的距离
    CGFloat imageRightEdgeRightSpace = labelWidth;
    
    // titleLabel 有效活动区域下边界上移的距离
    CGFloat labelBottomEdgeTopSpace = buttonHeight-(titleLabel_Y+labelHeight);
    // titleLabel 有效活动区域左边界左移的距离
    CGFloat labelLeftEdgeLeftSpace = imageWidth;
    
    button.imageEdgeInsets = UIEdgeInsetsMake(imageView_Y, 0, imageBottomEdgeTopSpace, -imageRightEdgeRightSpace);
    button.titleEdgeInsets = UIEdgeInsetsMake(titleLabel_Y, -labelLeftEdgeLeftSpace, labelBottomEdgeTopSpace, 0);
    
    // 对齐方式:水平方向为居中,垂直方向任意都行,不影响结果
    button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
    button.contentVerticalAlignment = UIControlContentVerticalAlignmentTop;
    //button.contentVerticalAlignment = UIControlContentVerticalAlignmentBottom;
    //button.contentVerticalAlignment = UIControlContentHorizontalAlignmentFill;
    

此方案是固定住了 imageViewtitleLabel 有效活动区域的上边界和下边界,也就是上边界和它们各自最终的 frame.origin.y 一致,下边界和 (frame.origin.y + frame.size.height) ,所以垂直方向的对齐方式不会影响到它们的位置,水平方向采用将有效活动区域放大至 button 的宽度,利用了水平居中对齐。
图片在上,文字在下,很明显,这种布局当前 button 的高度容纳不下,相应的我们增加其高度即可。

思考:大家有没有发现一个问题,我们以上所讲的情况都是 button 的宽度大于 imageViewtitleLabel 的之和的,也就是说,button 始终包含着 imageViewtitleLabel ,大家有没有想过 button 的宽度小于 imageViewtitleLabel 之和的时候还会按我们的预期偏移吗?

填坑

上面让大家思考的问题其实是个坑,当button 的宽度小于 imageViewtitleLabel 之和的时候,此时 titleLabel 会被压缩,也就是文字显示不全,甚至直接显示“...”。如图:

文字不显示.png

此时, button 的宽高都为60,无法完全显示两者,默认优先压缩了 titleLabel 。接下来看看为什么会压缩 titleLabel 而不是 imageView

在上面的关键知识点里的第5条说到了两个优先级, Content Hugging Priority(抗拉伸优先级) 和 Content Compression Resistance Priority(抗压缩优先级)正式因为 titleLabelContent Compression Resistance Priority(抗压缩优先级)小于 imageView 的,所以才会在宽度不够的时候会优先压缩 titleLabel 。我们来看看它们的默认值各是多少,还是通过 Xcode 的 Debug View Hierarchy 功能。

抗压缩阻力对比.png

代码获取:

    // imageView 水平方向的抗压缩阻力
    UILayoutPriority layoutPriority_image = [button.imageView contentCompressionResistancePriorityForAxis:UILayoutConstraintAxisHorizontal];
    // titleLabel 水平方向的抗压缩阻力
    UILayoutPriority layoutPriority_label = [button.titleLabel contentCompressionResistancePriorityForAxis:UILayoutConstraintAxisHorizontal];

上图可以看出:

imageView 的 Content Hugging Priority
Horizontal:250(UILayoutPriorityDefaultLow)
Vertical:250(UILayoutPriorityDefaultLow)

imageView 的 Content Compression Resistance Priority
Horizontal:750(UILayoutPriorityDefaultHigh)
Vertical:750(UILayoutPriorityDefaultHigh)

titleLabel 的 Content Hugging Priority
Horizontal:250(UILayoutPriorityDefaultLow)
Vertical:250(UILayoutPriorityDefaultLow)

titleLabel 的 Content Compression Resistance Priority
Horizontal:749.5
Vertical:749.5

titleLabel 的抗压缩阻力小于 imageView 的抗压缩阻力,所以是压缩了 titleLabel 。
也许有小伙伴在想,如果把 titleLabel 的抗压缩阻力设置为大于 imageView 的抗压缩阻力,那会不会就会优先显示 titleLabel 而压缩 imageView 呢,那不好意思,让你失望了,并不会出现你想要的结果。可能这是为了保护 imageView 不会被压缩变形吧,当然这也只是我的猜测。😆😆😆

接下来我们把上面提到的问题做了吧,请看题。
例:当 button 的宽度小于 imageView 和 titleLabel 宽度之和时,使图片在上,文字在下,不加间距。
分析:既然是叠罗汉放置,那就还是按水平居中的对齐方式,放大它们的有效活动区域至 button 的宽度,让它们自动居中对齐就行,我们只需计算上下边界偏移量就行。

    // 垂直居中对齐时,imageView 和 titleLabel ,各自距离 button 的间距(两者是等间距的)
    // 也是 imageView 有效活动区域上边界下移的距离   titleLabel 有效活动区域下边界上移的距离
    CGFloat spaceVertical = (buttonHeight-imageHeight-labelHeight) * 0.5f;
    
    // imageView 最终的 frame.origin.y
    CGFloat imageView_Y = spaceVertical;
    // titleLabel 最终的 frame.origin.y
    // 也是 titleLabel 有效活动区域上边界下移的距离
    CGFloat titleLabel_Y = spaceVertical + imageHeight;
    
    // imageView 有效活动区域下边界上移的距离
    CGFloat imageBottomEdgeTopSpace = buttonHeight-titleLabel_Y;

    // titleLabel 有效活动区域下边界上移的距离
    CGFloat labelBottomEdgeTopSpace = buttonHeight-(titleLabel_Y+labelHeight);

    button.imageEdgeInsets = UIEdgeInsetsMake(imageView_Y, 0, imageBottomEdgeTopSpace, -labelWidth);
    button.titleEdgeInsets = UIEdgeInsetsMake(titleLabel_Y, -imageWidth, labelBottomEdgeTopSpace, 0);
不按套路出牌.png

What? 这是什么鬼👻,imageView 为啥没居中啊?怎么让和我们想的不一样呢? 结果却让人意想不到啊!

通过多次测试,我发现当 imageView 有效活动区域的右边界恰好右移到接近 button 右边界处的某个位置时,imageView 有效活动区域的右边界会被压缩至 button 宽度的一半,不知道这是什么机制,这种问题之前没出现,就是因为 imageView 的宽度与 button 的宽度正好发生了什么不可告人的秘密㊙️,也可能是精度丢失的问题导致的吧,暂且就这么安慰自己吧,哈哈...

那既然水平居中对于 imageView 是不安全的,那我们就采取绝对定位(也就是让 imageView 的有效活动区域 frame 与它最终的 frame 一致),只不过就是需要多做点计算的事儿。

    // 垂直居中对齐时,imageView 和 titleLabel ,各自距离 button 的间距(两者是等间距的)
    // 也是 imageView 有效活动区域上边界下移的距离   titleLabel 有效活动区域下边界上移的距离
    CGFloat spaceVertical = (buttonHeight-imageHeight-labelHeight) * 0.5f;
    
    // imageView 最终的 frame.origin.y
    CGFloat imageView_Y = spaceVertical;
    // titleLabel 最终的 frame.origin.y
    // 也是 titleLabel 有效活动区域上边界下移的距离
    CGFloat titleLabel_Y = spaceVertical + imageHeight;
    
    // imageView 有效活动区域下边界上移的距离
    CGFloat imageBottomEdgeTopSpace = buttonHeight-titleLabel_Y;
    // imageView 有效活动区域左边界右移的距离
    CGFloat imageLeftEdgeRightSpace = (buttonWidth-imageWidth) * 0.5f;
    // imageView 有效活动区域右边界移动的距离(此处不确定是左移还是右移,因为 titleLabel 被压缩后宽度未知)
    // 所以还是通过算术表达式计算,正值就是左移,负值就是右移
    CGFloat imageRightEdgeLeftSpace = (buttonWidth-labelWidth) - (buttonWidth-imageLeftEdgeRightSpace);
    // 简化一下
    imageRightEdgeLeftSpace = imageLeftEdgeRightSpace-labelWidth;
    
    // titleLabel 有效活动区域下边界上移的距离
    CGFloat labelBottomEdgeTopSpace = buttonHeight-(titleLabel_Y+labelHeight);

    button.imageEdgeInsets = UIEdgeInsetsMake(imageView_Y, imageLeftEdgeRightSpace, imageBottomEdgeTopSpace, imageLeftEdgeRightSpace-labelWidth);
    button.titleEdgeInsets = UIEdgeInsetsMake(titleLabel_Y, -imageWidth, labelBottomEdgeTopSpace, 0);

达到预期效果.png

以上方案是把 imageView 的有效活动区域给绝对定位了,titleLabel 的有效活动区域是放大,采用水平居中对齐的方式,自动居中对齐的。大家可以试试 button 宽度小的时候,两者的有效活动区域都采用绝对定位的方案,看看会有什么意外的收获。😏😏😏答案是尽管居中了,但是 titleLabel 还是被压缩了,没有显示出文字。此处给大家提示一下,在计算 titleLabel 有效活动区域左右边界移动的时候,切忌,不要用 titleLabel 的宽度(即CGRectGetWidth(button.titleLabel.frame))去做计算,因为此时 titleLabel 的宽度是被压缩了的,与真实的 titleString 宽度不符,是小于 titleString 宽度的,所以 titleLabel 最终移动了左右边界后还是之前的那么宽,固然无法完全显示文字。此时做计算的时候一定要去计算 titleLabel.text 的文字宽度。但是 imageView 有效活动区域右边界移动的计算还是使用 titleLabel 的宽度(即CGRectGetWidth(button.titleLabel.frame))去计算。自己试着理解一下,这里可能有点绕,自己最好动手练习一下,就明白了!

    NSDictionary *dic = @{NSFontAttributeName:button.titleLabel.font};
    CGFloat stringWidth = [button.titleLabel.text boundingRectWithSize:CGSizeMake(MAXFLOAT, labelHeight)
                                                               options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
                                                            attributes:dic
                                                               context:nil].size.width;

总结

说了这么多,最后也该做个大总结了,主要说下以下几个重点以及需要注意的地方吧。

  1. imageView 和 titleLabel 有效活动区域的初始位置,所有的偏移都是在这个基础上进行的;
// 有效活动区域各边界的初始位置
CGFloat buttonWidth = button.bounds.size.width;
CGFloat imageWidth = CGRectGetWidth(button.imageView.frame);
CGFloat labelWidth = CGRectGetWidth(button.titleLabel.frame);

imageView :top:0  left:0           bottom:0  right:buttonWidth-labelWidth
titleLabel:top:0  left:imageWidth  bottom:0  right:buttonWidth
  1. button.contentHorizontalAlignmentbutton.contentVerticalAlignment 两个方向的对齐方式都是针对 imageView 和 titleLabel 在各自的有效区域内而言的,且互不影响,但是在填充模式下,即
    // 对齐方式:填充
    button.contentVerticalAlignment = UIControlContentVerticalAlignmentFill;
    button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill;

imageView 会拉伸充满自己的有效活动区域,titleLabel 只会在垂直方向充满自己的有效活动区域,水平方向是与自己的有效活动区域左对齐的,不会充满整个 titleLabel 的有效活动区域。

  1. 当 button 的宽度不够容纳 imageView 和 titleLabel 时, imageView 不合适用水平方向扩大有效区域的右边界至 button 的右边界,然后居中对齐的方式,不安全,但是 titleLabel 不会有问题,可采用此方式。

  2. 当 button 的宽度不够容纳 imageView 和 titleLabel 时, titleLabel 有效活动区域如果采用绝对定位的话,切记分清 labelWidth 和 stringWidth 的区别,计算 imageView 有效活动区域右边界移动时用 labelWidth ,计算 titleLabel 有效活动区域的左右边界移动时用 stringWidth。

  3. 当 button 的宽度不够容纳 imageView 和 titleLabel 时, 始终优先压缩 titleLabel。

  4. 设置 button.imageEdgeInsetsbutton.titleEdgeInsets 时,button.contentHorizontalAlignmentbutton.contentVerticalAlignment 有多种对齐方式可选,自己喜欢哪一种就选哪一种对齐方式,当然还是计算量少的最好,哈哈...

  5. 以上讲的所有边界偏移量的计算有一个很重要的前提就是 button.contentEdgeInsets 为默认值 UIEdgeInsetsZero,否则在做偏移量计算的时候需要加上 button.contentEdgeInsets 各边界的值,当然一般很少会去设置 button.contentEdgeInsets

今天的分享就到此结束🎬,希望大家能有所收获,祝各位帅哥靓女周末愉快!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容