聊聊iOS中的Frame 和Bounds

2,441 阅读5分钟

简述

Frame: 视图的位置和大小使用是父视图的坐标系,所以将视图放置在父级中这一点就很重要。

Bounds:视图的位置和大小,使用的是其自己的坐标系,而对于这一点而言将视图的内容或子视图放置在其自身内很重要。

打个比方:为了我们理解与记住Frame,我们可以想起上篇文章中的透明玻璃。而画布就有点像Bounds。我们可以放这样一个组合体放在我们可以放置的任何一个位置(比如墙上,比如桌子上)....用同样相同的隐喻:我可以在父视图(superview)中的任何一个位置放置视图(view)。在iOS中坐标系的原点在左上角,我们可以将视图的xy坐标位置设置为(0,0)来将视图放置在父视图的原点(origin)。这样就相当于我们把玻璃放在了桌子的左上角一样,向右移动玻璃就增加了x,向下移动就增加了y。

如果现在当你去理解Frame和Bounds可能还会出现一些混淆,但它其实实际看上去并不会如如一开始那么槽糕了不是吗?现在让我们来进一步理解吧。

Frame vs Bounds

在图片左侧的中,我们有一个位于其父视图左上角的视图。黄色矩形代表视图的bounds。 在图片右侧的中,我们再次看到视图,但这次没有显示父视图。那是因为bounds不知道父视图在哪。绿色矩形代表视图的边界。两个图像中的 红点 表示frame或bounds的原点。

Frame
origin = (0, 0)
width = 50
height = 150

Bounds
origin = (0, 0)
width = 50
height = 150

来源于网络

因此,该图片中的Frame和Bounds完全相同。让我们看一个其他的不同的例子:

frame
origin = (30, 20)
width = 50
height = 150

Bounds
origin = (0, 0)
width = 50
height = 150

来源于网络

所以你可以看到改变Frame的 xy 坐标会在父视图中移动它。但是视图本身的看起来仍然完全一样。Bounds不知道有什么不同。到目前为止,我们看到的Frame和Bounds的宽度和高度完全相同。然而这并不总是正确的。如果我们顺时针旋转视图几度会发生什么。(旋转是使用变换完成的)有关更多信息,请参阅文档

Frame
origin = (10, 30) width = 86
height = 200

Bounds
origin = (0, 0)
width = 50
height = 150

来源于网络

您可以看到Bounds仍然相同,但是Frame已经发生了更改。现在更容易看出frame和bounds之间的区别,不是吗?这篇文章定义解释了视图帧。主要的一句话就是:

该视图相对于其父坐标系的bounds最小边界框,包括应用于该视图的任何变换。

重要的是要注意,如果您转换视图,则Frame将变为未定义。所以实际上,我在上图中围绕旋转的绿色Bounds绘制的黄色框实际上并不存在。这意味着如果您旋转、缩放或进行其他一些转换,则不应再使用Frame。不过,您仍然可以使用Bounds。Apple 文档警告:

修改transform视图的属性时,所有转换都是相对于视图的中心点执行的。

因此,如果在完成转换后确实需要在父级中移动视图,则可以通过更改view.center坐标来完成。像framecenter使用父视图的坐标系。

好的,让我们摆脱旋转并专注于Bounds。到目前为止,Bounds原点一直停留在 (0, 0)。不过也不一定是这样的。如果我们的视图有一个很大的子视图,它太大而无法一次显示呢?我们将UIImageView使用大图像制作它。这是我们从上面的第二张图片,但这次我们可以看到我们视图的子视图的整个内容会是什么样子。

Frame
origin = (20,30)
width = 50
height = 150

Bounds
origin = (0, 0)
width = 50
height = 150

来源于网络

只有图像的左上角可以适合视图的Bounds。那么现在让我们来改变Bounds的原点坐标会发生什么。

Frame
origin = (20, 30)
width = 50
height = 150

Bounds
origin = (280, 70)
width = 50
height = 150

来源于网络

视图在父视图中没有移动,但是视图里面的内容发生了变化,因为bounds矩形的原点从视图的不同部分开始。这是 aUIScrollView及其子类(例如, a UITableView)背后的思想(上篇文章)。

何时使用Frame,何时使用Bounds

由于frame关联视图在其父视图中的位置,因此您在进行向外更改时会使用它,例如更改其宽度或查找视图与其父视图顶部之间的距离。

使用bounds时,你正在向内变化,就像画的东西或视图中安排子视图。如果您对它进行了一些转换,还可以使用bounds来获取视图的大小。

参考文章

  1. I Totally Didn't Understand Frames and Bounds
  2. View Programming Guide for iOS
  3. Unexpected frame and bounds sizes

练习项目

import UIKit
 
   class ViewController: UIViewController {

    @IBOutlet weak var myView: UIView!
 
    // Labels
    @IBOutlet weak var frameX: UILabel!
    @IBOutlet weak var frameY: UILabel!
    @IBOutlet weak var frameWidth: UILabel!
    @IBOutlet weak var frameHeight: UILabel!
    @IBOutlet weak var boundsX: UILabel!
    @IBOutlet weak var boundsY: UILabel!
    @IBOutlet weak var boundsWidth: UILabel!
    @IBOutlet weak var boundsHeight: UILabel!
    @IBOutlet weak var centerX: UILabel!
    @IBOutlet weak var centerY: UILabel!
    @IBOutlet weak var rotation: UILabel!
 
    // Sliders
    @IBOutlet weak var frameXSlider: UISlider!
    @IBOutlet weak var frameYSlider: UISlider!
    @IBOutlet weak var frameWidthSlider: UISlider!
    @IBOutlet weak var frameHeightSlider: UISlider!
    @IBOutlet weak var boundsXSlider: UISlider!
    @IBOutlet weak var boundsYSlider: UISlider!
    @IBOutlet weak var boundsWidthSlider: UISlider!
    @IBOutlet weak var boundsHeightSlider: UISlider!
    @IBOutlet weak var centerXSlider: UISlider!
    @IBOutlet weak var centerYSlider: UISlider!
    @IBOutlet weak var rotationSlider: UISlider!
 
    // Slider actions
    @IBAction func frameXSliderChanged(sender: AnyObject) {
        myView.frame.origin.x = CGFloat(frameXSlider.value)
        updateLabels()
    }
    @IBAction func frameYSliderChanged(sender: AnyObject) {
        myView.frame.origin.y = CGFloat(frameYSlider.value)
        updateLabels()
    }
    @IBAction func frameWidthSliderChanged(sender: AnyObject) {
        myView.frame.size.width = CGFloat(frameWidthSlider.value)
        updateLabels()
    }
    @IBAction func frameHeightSliderChanged(sender: AnyObject) {
        myView.frame.size.height = CGFloat(frameHeightSlider.value)
        updateLabels()
    }
    @IBAction func boundsXSliderChanged(sender: AnyObject) {
        myView.bounds.origin.x = CGFloat(boundsXSlider.value)
       updateLabels()
    }
    @IBAction func boundsYSliderChanged(sender: AnyObject) {
        myView.bounds.origin.y = CGFloat(boundsYSlider.value)
        updateLabels()
    }
    @IBAction func boundsWidthSliderChanged(sender: AnyObject) {
        myView.bounds.size.width = CGFloat(boundsWidthSlider.value)
        updateLabels()
    }
    @IBAction func boundsHeightSliderChanged(sender: AnyObject) {
        myView.bounds.size.height = CGFloat(boundsHeightSlider.value)
        updateLabels()
    }
    @IBAction func centerXSliderChanged(sender: AnyObject) {
        myView.center.x = CGFloat(centerXSlider.value)
        updateLabels()
    }
    @IBAction func centerYSliderChanged(sender: AnyObject) {
        myView.center.y = CGFloat(centerYSlider.value)
        updateLabels()
    }
    @IBAction func rotationSliderChanged(sender: AnyObject) {
        let rotation = CGAffineTransform(rotationAngle: CGFloat(rotationSlider.value))
        myView.transform = rotation
        updateLabels()
    }
 
    private func updateLabels() {
 
        frameX.text = "frame x = (Int(myView.frame.origin.x))"

        frameY.text = "frame y = (Int(myView.frame.origin.y))"

        frameWidth.text = "frame width = (Int(myView.frame.width))"

        frameHeight.text = "frame height = (Int(myView.frame.height))"

        boundsX.text = "bounds x = (Int(myView.bounds.origin.x))"

        boundsY.text = "bounds y = (Int(myView.bounds.origin.y))"

        boundsWidth.text = "bounds width = (Int(myView.bounds.width))"

        boundsHeight.text = "bounds height = (Int(myView.bounds.height))"

        centerX.text = "center x = (Int(myView.center.x))"

        centerY.text = "center y = (Int(myView.center.y))"

        rotation.text = "rotation = ((rotationSlider.value))"
 
    }
    
    override func viewDidLoad() {

        super.viewDidLoad()

    }
 
}