UIButton的三个点

修改 UIButton 的 image 和 title 的位置

contentVerticalAlignment 和 contentHorizontalAlignment

它们分别表示 button 的 content 在垂直、水平方向的对齐方式。

默认情况下,把 Button 的 title 和 image 当作整体,进行垂直、水平居中

titleEdgeInsets 和 imageEdgeInsets

Apple文档上表述:分别表示按钮文本、图片周围矩形的插入或起始边距。个人理解:就是指 image、title 的上下左右边缘离开原来的位置(布局后的位置)。默认值是:UIEdgeInsetsZero

修改 titleEdgeInsets、imageEdgeInsets 不会改变 Button 的大小,可能会显示在button的外部。

contentAlignment 对 edgeInsets 的影响

详细可以看看 简书 S型身材的猪,下面直接上验证结果:

1、先进行初始参数设置:

1
2
3
button.size = (80, 80), button.contentAlignment = center; 
image.frame = (8.333333333333336, 30.0, 20.0, 20.0);
title.frame = (28.666666666666664, 31.66666666666667, 43.0, 17.0);

2、修改 imageEdgeInsets.left = 10,那么 image.x == 18.333333333333336 ? 看结果:

1
2
3
btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)
print(btn.imageView?.frame)
// 打印:image.frame = (13.333333333333336, 30.0, 20.0, 20.0)

3、image.x 怎么不是 18.333333333333336 ???

imageEdgeInsetstitleEdgeInsetstop、left、bottom、right都是相对于 button 的 contentRect 而言。而当 button 的 contentEdgeInsets 值为 UIEdgeInsets.zero 时,它们显示的范围为 button.bounds。

由于 button.contentAlignment = center; 计算时是把 title.width + image.width 一起居中显示布局的,然后 imageEdgeInsets.left = 10,所以 title + image 显示的范围为 (70, 80) 来进行居中布局,所以,image.x = (80 - 10 - 20 - 43) / 2 + 10 = 13.5 ≈ 13.333333333333336。

总结:

当 UIButton 的 frame 确定时,imageEdgeInsetstitleEdgeInsets 的值只是确定了 image 和 title 显示的范围,然后再联合 contentVerticalAlignment 和contentHorizontalAlignment 进行对齐。

如果真的需要调整 image 向右移动 10,则需要设置 right = -10,这样 image.x = (80 - 10 + 10 - 20 - 43) / 2 + 10 = 18.5 ≈ 18.333333333333336。

1
btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: -10)

方式一:titleEdgeInsets 和 imageEdgeInsets 调整 button 的 content

btn.contentAlignment = center

1
2
let imageFrame = btn.imageView?.frame ?? CGRect.zero
let titleFrame = btn.titleLabel?.frame ?? CGRect.zero

1.1、默认图片在左,文字在右,添加间距 10

1
2
btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: -5, bottom: 0, right: 5)
btn.titleEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: -5)

1.2、文字在左,图片在右,间距为 10

1
2
3
let space: CGFloat = 5
btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: titleFrame.width + space, bottom: 0, right: -(titleFrame.width + space))
btn.titleEdgeInsets = UIEdgeInsets(top: 0, left: -(imageFrame.width + space), bottom: 0, right: imageFrame.width + space)

1.3、文字在下,图片在上,间距为10

先把文字、图片都居中:

1
2
btn.imageEdgeInsets = UIEdgeInsets(top: 0, left: titleFrame.width / 2, bottom: 0, right: -titleFrame.width / 2)
btn.titleEdgeInsets = UIEdgeInsets(top: 0, left: -imageFrame.width / 2, bottom: 0, right: imageFrame.width / 2)
1
2
3
let space: CGFloat = 10
btn.imageEdgeInsets = UIEdgeInsets(top: -(titleFrame.height + space) / 2, left: titleFrame.width / 2, bottom: (titleFrame.height + space) / 2, right: -titleFrame.width / 2)
btn.titleEdgeInsets = UIEdgeInsets(top: (imageFrame.height + space) / 2, left: -imageFrame.width / 2, bottom: -(imageFrame.height + space) / 2, right: imageFrame.width / 2)

1.4、文字在上,图片在下,间距为10

1
2
3
let space: CGFloat = 10
btn.imageEdgeInsets = UIEdgeInsets(top: (titleFrame.height + space) / 2, left: titleFrame.width / 2, bottom: -(titleFrame.height + space) / 2, right: -titleFrame.width / 2)
btn.titleEdgeInsets = UIEdgeInsets(top: -(imageFrame.height + space) / 2, left: -imageFrame.width / 2, bottom: (imageFrame.height + space) / 2, right: imageFrame.width / 2)

方式二:重写系统的方法

titleRect(contentRect: CGRect) 和 imageRect(contentRect: CGRect) 在设置 title 和 image 时分别会被调用;调用 layoutSubviews 时会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class ZMButton: UIButton {

enum ImageLayout {
case left, right, top, bottom
}

var space: CGFloat = 0.0
var layoutStyle = ImageLayout.left

// 返回 image 在 button 中的 frame
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
let imageRect = super.imageRect(forContentRect: contentRect)
let titleRect = super.titleRect(forContentRect: contentRect)
var x: CGFloat = 0, y: CGFloat = 0
switch layoutStyle {
case .left:
x = imageRect.origin.x
y = imageRect.origin.y
case .right:
x = (contentRect.width - imageRect.width - titleRect.width - space) / 2 + space + titleRect.width
y = (contentRect.height - imageRect.height) / 2
case .top:
x = (contentRect.width - imageRect.width) / 2
y = (contentRect.height - imageRect.height - titleRect.height - space) / 2
default:
x = (contentRect.width - imageRect.width) / 2
y = (contentRect.height - imageRect.height - titleRect.height - space) / 2 + space + titleRect.height
}
return CGRect(x: x, y: y, width: imageRect.width, height: imageRect.height)
}

// 返回 title 在 buttom 中的 frame
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
let imageRect = super.imageRect(forContentRect: contentRect)
let titleRect = super.titleRect(forContentRect: contentRect)
var x: CGFloat = 0, y: CGFloat = 0

switch layoutStyle {
case .left:
x = titleRect.origin.x
y = titleRect.origin.y
case .right:
x = (contentRect.width - imageRect.width - titleRect.width - space) / 2
y = (contentRect.height - titleRect.height) / 2
case .top:
x = (contentRect.width - titleRect.width) / 2
y = (contentRect.height - imageRect.height - titleRect.height - space) / 2 + space + imageRect.height
default:
x = (contentRect.width - titleRect.width) / 2
y = (contentRect.height - imageRect.height - titleRect.height - space) / 2
}
return CGRect(x: x, y: y, width: titleRect.width, height: titleRect.height)
}
}

总结:

方式一 修改 image 和 title,必须 image 和 title 都有值,button 的 frame 确定

方式二 直接初始化 button 时设置参数即可,如页面已显示,设置后则要进行 btn.layoutSubviews() 更新

UIButton 避免重复点击

方式一:

1
2
3
4
5
6
7
8
9
10
@objc func clickGreenBtn(button: UIButton) {

button.isEnabled = false

// todo...

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
button.isEnabled = true
}
}

方式二:

来自吴品诚的技术博客

1
2
3
4
5
6
7
func getPtr<T>(_ value: inout T) -> String {
let ptr = withUnsafePointer(to: &value) { UnsafeRawPointer($0) }
let address = ptr.load(as: UInt.self)
let ptr2 = UnsafeRawPointer(bitPattern: address)
return ptr2 == nil ? "\(ptr2!)" : "null"
}
`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
extension UIButton {

private static var REPEATTIMEINTERVAL_KEY: Void?
private static var ENABLEDREJECTREPEATTAP_KEY: Void?
private static var EVENTDICTIONARY_KEY: Void?

/// 重复点击时间间隔,默认 0.5s
var repeatTimeInterval: Double {
get {
if let value = objc_getAssociatedObject(self, &UIButton.REPEATTIMEINTERVAL_KEY) as? Double {
return value
}
objc_setAssociatedObject(self, &UIButton.REPEATTIMEINTERVAL_KEY, 0.5, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return 0.5
}
set {
objc_setAssociatedObject(self, &UIButton.REPEATTIMEINTERVAL_KEY, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

/// 是否启用防止重复点击,默认为 true
var enabledRejectRepeatTap: Bool {
get {
if let value = objc_getAssociatedObject(self, &UIButton.ENABLEDREJECTREPEATTAP_KEY) as? Bool {
return value
}
objc_setAssociatedObject(self, &UIButton.ENABLEDREJECTREPEATTAP_KEY, true, .OBJC_ASSOCIATION_ASSIGN)
return true
}
set {
objc_setAssociatedObject(self, &UIButton.ENABLEDREJECTREPEATTAP_KEY, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}

/// key: target-action ,value:触发点击时间
var eventDictionary: Dictionary<String, Double> {
get {
if let value = objc_getAssociatedObject(self, &UIButton.EVENTDICTIONARY_KEY) as? Dictionary<String, Double> {
return value
}
let defaultValue = Dictionary<String, Double>()
objc_setAssociatedObject(self, &UIButton.EVENTDICTIONARY_KEY, defaultValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return defaultValue
}
set {
objc_setAssociatedObject(self, &UIButton.EVENTDICTIONARY_KEY, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

static let swizzleUIButtonOnce: Void = {
let cls = UIButton.self
zm_swizzleMethod(cls, #selector(UIButton.sendAction(_:to:for:)), #selector(UIButton.zm_sendAction(_:to:for:)))
}()

@objc dynamic func zm_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {

guard let type = event?.type, type == UIEvent.EventType.touches, enabledRejectRepeatTap else {
return
}
var act = action, tar = target
let key = getPtr(&act) + getPtr(&tar)
let lasttimeTapTime = eventDictionary[key] ?? 0
let currentTime = NSDate.init().timeIntervalSince1970

if lasttimeTapTime > 0.0 {
let interval = currentTime - lasttimeTapTime
if interval <= self.repeatTimeInterval {
return;
}
}
eventDictionary[key] = currentTime
zm_sendAction(action, to: target, for: event)
}

}

UIButton 扩大点击范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

class ZMButton: UIButton {
// 向四周扩大点击范围
var stretchInsets: UIEdgeInsets = UIEdgeInsets.zero

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var actionBounds = bounds
actionBounds.origin.x -= stretchInsets.left
actionBounds.size.width += stretchInsets.right + stretchInsets.left
actionBounds.origin.y -= stretchInsets.top
actionBounds.size.height += stretchInsets.bottom + stretchInsets.top

print(actionBounds.contains(point), actionBounds, point)
return actionBounds.contains(point)
}
}

学习博客:

意林的小站
简书 S型身材的猪
LQRelayoutButton
雪_晟

文章作者: Czm
文章链接: http://yoursite.com/2020/06/19/UIButton%E7%9A%84%E4%B8%89%E4%B8%AA%E7%82%B9/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm