UIButtonEdgeInsets图文排列窥探

333 阅读7分钟

在创建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

通过运行代码,可以得到如下

25077763-FE70-4712-AAF0-8FC15C3A6792.png

,通过观察在设置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)

运行代码后,条件约束一切正常,等于指哪打哪。

Xnip2022-06-30_22-52-56.jpg

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优先级更高。

D446964E-47DF-4E69-B555-99719D075A92.png

如何进行图文排列

理解上面的排列规则,十分重要。这间接告诉我们需要从哪里开始改,而不是乱改一气。

首先常规方向上,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
    }