在创建UIButton控件中,常用的Type有system和custom,他们代表了默认与定制。State则代表了按钮不同状态下的效果。在渲染样式上我们可以将按钮分为titleButton、imageButton、(title & image)button,从而让它满足不同样式下的效果变化。
UIEdgeInsets
UIEdgeInsets是一个结构体类型,并且在UIButton的扩展属性中,直接对应了三个属性的类型,当然在其它地方也有用到,这里我们只探讨UIButton控件。
public struct UIEdgeInsets {
public init()
public init(top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat)
public var top: CGFloat // specify amount to inset (positive) for each of the edges. values can be negative to 'outset'
public var left: CGFloat
public var bottom: CGFloat
public var right: CGFloat
}
EdgeInsets可以理解为控件及控件内部的元素,根据设定值,朝指定的方向的偏移后的最终位置,体现在UI上就是改变了控件原本内容的位置,抛开字体、位置、大小等这些强加于控件造成内容改变的诱因,主要包含三个具体要素:
- titleEdgeInsets
- imageEdgeInsets
- contentEdgeInsets
为了可以深入了解到EdgeInsets的规律,我们可以先来设置一个testButton~
lazy var testButton: UIButton = {
return UIButton(type: .custom).link
.stateNormal({ $0.title("跳转Push").color(.black) })
.font(.medium(30))
.cornerRadius(6, masksToBounds: true)
.borderColor(.black)
.borderWidth(1)
.base
}()
view.addSubview(testButton)
testButton.snp.makeConstraints { make in
make.top.equalToSuperview().inset(80)
make.left.equalToSuperview().inset(10)//为了不让size影响我们,没有锁死size
}
单一的titleButton
包含单个title的UIButton控件它的Insets只与两个属性有关:titleEdgeInsets、contentEdgeInsets
通过运行代码,可以得到如下
,通过观察在设置title以后,在原本Button上会初始化一个UIButtonLabel,用于展示title文字内容,而它的位置为center Vertically & left.right.equalToSuperview()。为什么只是相对左右距离为0,而上下并没有,这里猜测Apple是为了美观故意留出了一部分距离出来。
titleEdgeInsets默认是zero的,为了体现出它的作用,可以随便写一个
UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
此时运行代码,发现了一个什么问题?UIButtonLabel的位置改变了,但没有完全变。如果只是这样设置,那么它所能改变的只有左右的边距,不能影响上下边距。所以还需要如下设置:
//上对齐
UIEdgeInsets(top: -6, left: 10, bottom: 6, right: 10)
//下对齐
UIEdgeInsets(top: 6, left: 10, bottom: -6, right: 10)
那么,能不能既上对齐也下对齐,也就是改变UIButtonLabel的高度?
UIButtonLabel的高度只与font的大小有关
左右边距为什么有误差?
这里判断是Apple会根据屏幕大小自动适配左右间距,在我们设置的范围内调整UIButtonLabel最佳的放置位置,主要可能还是为了美观。
<一个有趣的地方:当我们不设置size的时候,UIButtonLabel的相对上下边距永远是6pt>
单一的imageButton
包含单个image的UIButton控件它的Insets只与两个属性有关:imageEdgeInsets、contentEdgeInsets
修改stateNormal中的内容,改为只保留图片的情况
.stateNormal({ $0.image(R.image.aaaabbbbbc()) })
与上面的titleButton一样,原本的UIButton上会初始化一个UIImageView,用于展示图片内容。不同的是,imageView距上左下右初始边距为0,也是为了美观,Apple选择整个填充满ButtonContent。
默认的imageEdgeInsets值是zero。通过设置imageEdgeInsets:
UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
运行代码后,条件约束一切正常,等于指哪打哪。
title&image Button
修改stateNormal中的内容,增加文字和图片
.stateNormal({ $0.image(R.image.aaaabbbbbc()).title("跳转Push").color(.black) })
可以看到,当不设置UIButton size时,imageView以最大程度所要领地,title还是为了美观保持center Vertically & left.right.equalToSuperview(),只不过left参照的对象变成了imageView。
可能为了保证用户阅读习惯,Apple默认是左图右字的格式。
前面说到imageView是贪婪的,并以最大程度的所要领地,当UIButton size固定时且小于图片原始size,那么UIButtonLabel就会被挤压,在界面上看到的效果像被切割了一样,十分的不完整。
为什么imageView会挤压UIButtonLabel?
因为,imageView优先级更高。
如何进行图文排列
理解上面的排列规则,十分重要。这间接告诉我们需要从哪里开始改,而不是乱改一气。
首先常规方向上,UIButton图文排列有四种方式:左图右字(默认)、右图左字、上图下字、下图上字,先从最简单的开始。
左图右字
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: space, bottom: 0, right: -space)
self.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: space)
策略:由于系统默认左图右字,因此这里不需要改动imageEdgeInsets
(1)考虑UIButtonLabel整体横移
(2)考虑在UIButtonLabel移动后增加/减少testButton的内容范围
(3)考虑由谁来移动产生间距
右图左字
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth, bottom: 0, right: imageWidth)
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: labelWidth+space, bottom: 0, right: -labelWidth-space)
self.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: space)
策略:需要交换UIButtonLabel和imageView
(1)考虑UIButtonLabel的整体横移,移动的距离为imageView宽度
(2)考虑imageView的整体横移,移动距离为UIButtonLabel宽度
(3)考虑由谁来移动产生间距
(4)考虑增加/减少间距过程中,testButton动态内容范围与上方元素保持一致
上图下字、下图上字
策略:考虑到不是单维度上的偏移,需要找到一个参考值,来作为x、y轴移动的依据。
(1)考虑优先垂直居中,找到testButton center Horizontally,作为x参照点,命名为x0.
(2)找到imageView center Horizontally,命名为x1
(3)找到UIButtonLabel center Horizontally,命名为x2
(4)计算x1距离x0的偏移量;考虑计算x2距离x0的偏移量;并移动。可参考下列X轴偏移代码
let labelOffsetX = (imageWidth + labelWidth / 2) - (imageWidth + labelWidth) / 2//label中心x偏移的移动距离
let imageOffsetX = (imageWidth + labelWidth) / 2 - imageWidth / 2//image中心x偏移的移动距离
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: -labelOffsetX, bottom: 0, right: labelOffsetX)
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: imageOffsetX, bottom: 0, right: -imageOffsetX)
(5)考虑水平居中,找到testButton center Vertically,作为y参照点,命名为y0
(6)找到imageView center Vertically,命名为y1
(7)找到UIButtonLabel center Vertically,命名为y2
(8)计算y1距离y0的偏移量;考虑计算y2距离y0的偏移量;并移动。可参考下列Y轴偏移代码
let labelOffsetY = labelHeight / 2 + space / 2//label中心x偏移的移动距离
let imageOffsetY = imageHeight / 2 + space / 2//image中心y偏移的移动距离
(8.1)上图下字
self.titleEdgeInsets = UIEdgeInsets(top: labelOffsetY, left: -labelOffsetX, bottom: -labelOffsetY, right: labelOffsetX)
self.imageEdgeInsets = UIEdgeInsets(top: -imageOffsetY, left: imageOffsetX, bottom: imageOffsetY, right: -imageOffsetX)
(8.2)下图上字
self.titleEdgeInsets = UIEdgeInsets(top: -labelOffsetY, left: -labelOffsetX, bottom: labelOffsetY, right: labelOffsetX)
self.imageEdgeInsets = UIEdgeInsets(top: imageOffsetY, left: imageOffsetX, bottom: -imageOffsetY, right: -imageOffsetX)
(9)考虑在位移过程中,调整testButton实际frame
let maxWidth = max(labelWidth, imageWidth)
let maxHeight = max(labelHeight, imageHeight)
let changedWidth = labelWidth + imageWidth - maxWidth
let changedHeight = labelHeight + imageHeight + space - maxHeight
(10.1)上图下字
self.contentEdgeInsets = UIEdgeInsets(top: imageOffsetY, left: -changedWidth/2, bottom: changedHeight-imageOffsetY, right: -changedWidth/2)
(10.2)下图上字
self.contentEdgeInsets = UIEdgeInsets(top: changedHeight-imageOffsetY, left: -changedWidth/2, bottom: imageOffsetY, right: -changedWidth/2)
如何理解Top、Left、Bottom、Right改变数值产生的变化?
EdgeInsets是每个方向上的偏移量,既然是偏移量,那么,既可以向内偏移也可以向外偏移。(所对应的就是:正向内,负向外)
当我们需要改变元素原始位置,也就是说在同一条线上 如left向左移动 right也要向左移动,一个正值一个负值,才能确保不会压缩原始尺寸。以上面的例子为例:
self.titleEdgeInsets = UIEdgeInsets(top: 0, left: space, bottom: 0, right: -space)
left向右移动space距离,right向右移动space距离,这也就是页面上看到的UIButtonLabel向右整体移动了space
为什么要强调contentEdgeInsets?
设置contentEdgeInsets的最大作用是还原,contentEdgeInsets就像一个最适合的磨具,将最新的排列结果以我们需要的样式展示出来,它就是底部的UIButton,我们既不希望它过大造成排列不严格造成布局浪费,同时也不希望它过小显示不全或点击区域过小,因此在排列后调整是非常必要的。
附上代码
public enum ButtonEdgeInsetsStyle {
case imageTopWithTitleBottom // image在上,label在下
case imageLeftWithTitleRight // image在左,label在右
case imageBottomWithTitleTop // image在下,label在上
case imageRightWithTitleLeft // image在右,label在左
}
extension UIButton {
/// 控制Button内容布局
func layoutEdges(style: ButtonEdgeInsetsStyle, space:CGFloat) {
//top、left、bottom、right
//反的
//如果同时改变同一条线上的两个属性 等于做了x或y轴的偏移
// edgeInsets初始值
var imageEdgeInset = UIEdgeInsets.zero
var labelEdgeInset = UIEdgeInsets.zero
var contentEdgeInset = UIEdgeInsets.zero
// ButtonImageView Size
let imageWidth = self.imageView?.intrinsicContentSize.width ?? 0
let imageHeight = self.imageView?.intrinsicContentSize.height ?? 0
// ButtonLabel Size
let labelWidth = self.titleLabel?.intrinsicContentSize.width ?? 0
let labelHeight = self.titleLabel?.intrinsicContentSize.height ?? 0
print(imageWidth)
print(imageHeight)
print(labelWidth)
print(labelHeight)
let labelOffsetX = (imageWidth + labelWidth / 2) - (imageWidth + labelWidth) / 2//label中心x偏移的移动距离
let imageOffsetX = (imageWidth + labelWidth) / 2 - imageWidth / 2//image中心x偏移的移动距离
let labelOffsetY = labelHeight / 2 + space / 2//label中心x偏移的移动距离
let imageOffsetY = imageHeight / 2 + space / 2//image中心y偏移的移动距离
print("imageOffsetX \(imageOffsetX)")
print("imageOffsetY \(imageOffsetY)")
print("labelOffsetX \(labelOffsetX)")
print("labelOffsetY \(labelOffsetY)")
let maxWidth = max(labelWidth, imageWidth)
let maxHeight = max(labelHeight, imageHeight)
let changedWidth = labelWidth + imageWidth - maxWidth
let changedHeight = labelHeight + imageHeight + space - maxHeight
// 赋值
switch style {
case .imageTopWithTitleBottom:
labelEdgeInset = UIEdgeInsets(top: labelOffsetY, left: -labelOffsetX, bottom: -labelOffsetY, right: labelOffsetX)
imageEdgeInset = UIEdgeInsets(top: -imageOffsetY, left: imageOffsetX, bottom: imageOffsetY, right: -imageOffsetX)
contentEdgeInset = UIEdgeInsets(top: imageOffsetY, left: -changedWidth/2, bottom: changedHeight-imageOffsetY, right: -changedWidth/2)
case .imageBottomWithTitleTop:
labelEdgeInset = UIEdgeInsets(top: -labelOffsetY, left: -labelOffsetX, bottom: labelOffsetY, right: labelOffsetX)
imageEdgeInset = UIEdgeInsets(top: imageOffsetY, left: imageOffsetX, bottom: -imageOffsetY, right: -imageOffsetX)
contentEdgeInset = UIEdgeInsets(top: changedHeight-imageOffsetY, left: -changedWidth/2, bottom: imageOffsetY, right: -changedWidth/2)
case .imageLeftWithTitleRight:
// 由于系统默认样式是image左label右
// 因此不用调整image
// 只需要将label位置整体向label的右侧移动space距离
// 最底层content也因为image没有位置移动,只需要将content层右侧增加label移动的space距离
labelEdgeInset = UIEdgeInsets(top: 0, left: space, bottom: 0, right: -space)
contentEdgeInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: space)
case .imageRightWithTitleLeft:
// 由于调换了image与label的位置
// 因此将label整体向左侧移动一个imageWidth的距离,再将image向右移动一个labelWidth的距离
// 但同时需要考虑间距改变带来的效果,也就需要增加一个space距离
// 最底层由于只是改变了image和label的位置,所以只需要增加一个space的距离
labelEdgeInset = UIEdgeInsets(top: 0, left: -imageWidth, bottom: 0, right: imageWidth)
imageEdgeInset = UIEdgeInsets(top: 0, left: labelWidth+space, bottom: 0, right: -labelWidth-space)
contentEdgeInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: space)
}
self.titleEdgeInsets = labelEdgeInset
self.imageEdgeInsets = imageEdgeInset
self.contentEdgeInsets = contentEdgeInset
}