Cocoa:"frame"和"bounds"有什么区别?

631

UIView 及其子类都拥有属性 framebounds,它们之间有什么区别呢?


1
理解框架 http://macoscope.com/blog/understanding-frame/ - onmyway133
对我来说最简洁的解释在这里:https://videos.raywenderlich.com/courses/99-scroll-view-school/lessons/2 - Volodymyr
1
一个2分30秒的视频并不简洁。我可以比2分30秒更快地阅读一两个句子。 - dsjoerg
3
对我来说,如果我这样想会更清晰:框架=边界和位置。 - Serg
13个回答

951
一个 UIViewbounds 是一个矩形,由位置 (x,y) 和大小 (width,height) 相对于自己的坐标系 (0,0) 表示。 一个 UIViewframe 是一个矩形,由位置 (x,y) 和大小 (width,height) 相对于其所包含的父视图表示。 因此,想象一个大小为 100x100(宽 x 高)且位于其父视图的 25,25(x,y)处的视图。以下代码打印出此视图的边界和框架:
// This method is in the view controller of the superview
- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"bounds.origin.x: %f", label.bounds.origin.x);
    NSLog(@"bounds.origin.y: %f", label.bounds.origin.y);
    NSLog(@"bounds.size.width: %f", label.bounds.size.width);
    NSLog(@"bounds.size.height: %f", label.bounds.size.height);

    NSLog(@"frame.origin.x: %f", label.frame.origin.x);
    NSLog(@"frame.origin.y: %f", label.frame.origin.y);
    NSLog(@"frame.size.width: %f", label.frame.size.width);
    NSLog(@"frame.size.height: %f", label.frame.size.height);
}

这段代码的输出结果是:

bounds.origin.x: 0
bounds.origin.y: 0
bounds.size.width: 100
bounds.size.height: 100

frame.origin.x: 25
frame.origin.y: 25
frame.size.width: 100
frame.size.height: 100
因此,我们可以看到在这两种情况下,视图的宽度和高度是相同的,无论我们是查看边界还是框架。不同的是视图的x,y定位。对于边界,x和y坐标为0,0,因为这些坐标是相对于视图本身的。然而,框架的x和y坐标是相对于父视图中视图的位置(早先我们说过是25,25)。 还有一个非常好的演示文稿介绍了UIView。请参阅幻灯片1-20,其中不仅解释了框架和边界之间的区别,还展示了视觉示例。

41
因为它是对象在...自身的位置,所以边界的 x 和 y 坐标总是 0,0。你能否提供一种情况,使得它不会是这样? - mk12
5
据我所知,边界的起始点始终为0,0。 - shek
78
实际上,bounds.origin 可以不是 0,0。使用 setBoundsOrigin: 来移动/平移原点。有关更多信息,请参阅 Cocoa 的 "View Programming Guide" 中的 "View Geometry" 章节。 - Meltemi
135
UIScrollView是一个示例,其中边界可以移动以显示视图中的不同内容区域。 - Brad Larson
94
一个小建议 - 使用NSStringFromCGRect可以节省记录矩形的时间。 - beryllium
显示剩余5条评论

752

简短回答

frame = 用父视图的坐标系来表示一个视图的位置和大小

  • 重要用途:在父视图中放置视图

bounds = 用自身坐标系来表示一个视图的位置和大小

  • 重要用途:在视图内部放置内容或子视图

详细回答

为了方便记忆frame,可以将其比作挂在墙上的画框。画框就像视图的边框。我可以在墙上任何想放置的地方挂画。同样地,在父视图(也称为超视图)中我可以将视图放置在任何想放置的位置上。父视图就像墙。iOS中的坐标系统原点是左上角。我们可以将视图的x-y坐标设置为(0,0)使其位于超视图的原点上,这就像是将画挂在墙的左上角。向右移动,增加x值;向下移动,增加y值。

为了方便记忆bounds,可以将其比作篮球场地,有时候篮球会被击出界。你可以在篮球场地上运球,但实际上并不关心篮球场地本身的位置。篮球场地可以在体育馆内、高中外面或者你家门前,都没有关系。你只想打篮球。同样地,视图的bounds坐标系仅关心视图本身,不知道视图在父视图中的位置。bounds的原点(默认是(0,0))位于视图的左上角。任何该视图具有的子视图都将相对于此点进行布局。这就像将篮球带到篮球场地的前左角。

现在问题来了:当我们试着比较frame和bounds时,会产生混淆。但事实上,一开始看起来并不那么糟糕。让我们用一些图片来帮助我们理解。

Frame和Bounds的比较

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

Frame
    origin = (0, 0)
    width = 80
    height = 130

Bounds 
    origin = (0, 0)
    width = 80
    height = 130

在此输入图片描述

所以,该图片中的框架和边界是完全相同的。现在让我们看一个框架和边界不同的例子。

Frame
    origin = (40, 60)  // That is, x=40 and y=60
    width = 80
    height = 130

Bounds 
    origin = (0, 0)
    width = 80
    height = 130

enter image description here

你可以看到,改变视图框架的x-y坐标会将其在父视图中移动。但是视图本身的内容仍然完全相同。边界不知道任何不同。

到目前为止,视图框架和边界的宽度和高度一直完全相同。尽管如此,并非总是如此。看看如果我们将视图顺时针旋转20度会发生什么。(使用变换进行旋转。有关更多信息,请参见文档以及这些视图(view)层(layer)示例。)

Frame
    origin = (20, 52)  // These are just rough estimates.
    width = 118
    height = 187

Bounds 
    origin = (0, 0)
    width = 80
    height = 130

图片描述

您可以看到边界仍然是相同的。它们仍然不知道发生了任何事情!尽管如此,帧值已全部更改。

现在,很容易看出框架和边界之间的区别,不是吗?文章《你可能并不理解框架和边界》将视图框架定义为:

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

需要注意的是,如果您转换视图,则框架变得未定义。因此,实际上,我在上面的图像中绘制的围绕旋转的绿色边界的黄色框架实际上从未存在过。这意味着,如果您旋转、缩放或进行其他变换,则不再应使用框架值。但您仍然可以使用边界值。苹果文档警告:

重要提示:如果视图的transform属性不包含恒等变换,则该视图的框架未定义,其自动调整大小行为的结果也是未定义的。

这对于自动调整大小来说非常不幸....不过,您仍然可以采取一些措施。

苹果文档中指出:

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

因此,如果您确实需要在进行变换后在父视图中移动视图,则可以通过更改视图的center坐标来完成。与frame一样,center使用父视图的坐标系。

好吧,让我们去掉旋转并关注边界。到目前为止,边界原点始终保持在(0,0)。但是它不必如此。如果我们的视图具有太大而无法一次显示全部内容的大型子视图怎么办?我们将其作为UIImageView,其中包含一个大型图像。这是我们之前的第二张图片,但这次我们可以看到我们的视图子视图的整个内容。

Frame
    origin = (40, 60)
    width = 80
    height = 130

Bounds 
    origin = (0, 0)
    width = 80
    height = 130

输入图像描述

仅有图像的左上角可以适应视图的边界。现在看看如果我们改变边界的原点坐标会发生什么。

Frame
    origin = (40, 60)
    width = 80
    height = 130

Bounds 
    origin = (280, 70)
    width = 80
    height = 130

图片描述进入此处

在父视图中,框架没有移动,但是框架内的内容已更改,因为边界矩形的原点从视图的不同部分开始。这是UIScrollView及其子类(例如UITableView)的整个理念。有关更多解释,请参见了解UIScrollView

何时使用框架和边界

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

在进行内部更改时,例如绘制东西或在视图内排列子视图时,请使用bounds。 如果对其进行了一些变换,则还可以使用边界来获取视图的大小。

进一步研究的文章:

苹果文档

相关的StackOverflow问题

其他资源

自己练习

除了阅读上述文章,制作一个测试应用程序对我很有帮助。您可能需要尝试做类似的事情。 (我从这个视频课程中得到了灵感,但不幸的是它不是免费的。)

enter image description here

以下是您可以参考的代码:

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))"

    }

}

使用边界(bounds)来“绘制内容或在视图(view)内排列子视图(subviews)”。你的意思是,如果我有一个具有两个按钮的视图控制器(viewController),我应该使用视图控制器的‘边界’来放置这些按钮?! 我一直都使用视图的框架(frame)…… - mfaani
@亲爱的,我已经有一年没有用iOS技术工作了,有点生疏。我相信视图控制器的根视图会填满整个屏幕,因此它的frame和bounds应该是一样的。 - Suragch
10
创建了 https://github.com/maniramezan/FrameVsBounds.git 仓库,基于此处提供的示例应用程序,以帮助更好地理解视图的 boundsframe - manman
谢谢!解释得很好。一张图片胜过千言万语。 - Pedro Trujillo
1
@IanWarburton 当视图旋转时,边界的宽度和高度与框架不匹配,因此不是多余的。 - Edward Brey
显示剩余3条评论

43

尝试运行下面的代码

- (void)viewDidLoad {
    [super viewDidLoad];
    UIWindow *w = [[UIApplication sharedApplication] keyWindow];
    UIView *v = [w.subviews objectAtIndex:0];

    NSLog(@"%@", NSStringFromCGRect(v.frame));
    NSLog(@"%@", NSStringFromCGRect(v.bounds));
}

这段代码的输出是:

设备方向为纵向(Portrait)

{{0, 0}, {768, 1024}}
{{0, 0}, {768, 1024}}

设备方向为横屏时

{{0, 0}, {768, 1024}}
{{0, 0}, {1024, 768}}

显然,您可以看到frame和bounds之间的区别。


2
这个已经不再是真实的了(iOS 8) - Julian
1
https://dev59.com/amAf5IYBdhLWcg3w52Pm#25140300 - Julian

13

框架(frame)是指相对于其父视图而定义UIView的矩形。

边界矩形(bounds rect)是指定义NSView坐标系统的值范围。

也就是说,在此矩形内的任何内容实际上都会显示在UIView中。


12

iOS的边界和框架

[iOS CALayer]

UIView在幕后使用CALayer

UIView -> CALayer -> bounds, anchorPoint, position
//CALayer.position == UIView.center
创建CALayer时,您可以设置一个Frame,然后计算bounds、anchorPoint和position。 应用transform时,bounds、anchorPoint和position不会改变,但frame会相应地改变。因此,frame实际上是一个派生属性。 修改视图的transform属性时,所有变换都是相对于视图的中心点进行的。 Bounds: - (x, y) - 矩形在自己的坐标系中绘制内部内容(子视图)的点 - Width x Height - 矩形的大小 Frame与bounds、position和transform有直接关系。 (x,y)-矩形在parenView(superview)坐标系中的点 宽度x高度-由bounds占用的完整矩形尺寸。 请考虑以下事项: 当frame的大小改变时->bounds的大小也会改变 当bounds的大小改变时->frame的大小也会改变 如果应用了任何view.transform(例如缩放、旋转、平移)(除了.identity CGAffineTransformIdentity),您应该依赖于CALayer.bounds(而不是CALayer.frame)和CALayer.position 当此属性的值不是identity变换时,frame属性中的值是未定义的,应该被忽略。
  • view.bounds.origin 不总是 (0, 0)。例如,viewB 包含 viewC,而 viewC 比父视图(viewB)要大,在这种情况下,通过调整 viewB.bounds.origin,我们可以调整渲染的内容。这个矩形(点和大小)将传递给 view.draw(方法)。这种技巧也被用在 UIScrollView 中。

再举一个例子来说明 frame 和 bounds 的区别。

  • viewA 包含 viewB
  • viewB 包含 viewC
  • viewCviewB 要大
  • View B 被移动到 x:72, y: 22
  • View B 被旋转了 45 度


案例4(bounds.origin不是(0, 0)):

如果一个视图的变换属性不包含标识变换,那么该视图的框架是未定义的,其自动调整大小的行为的结果也是未定义的。

viewB.autoresizingMask = [.flexibleWidth]

//case 1
//viewA.frame.size = CGSize(width: 300, height: 300)

//transform        
let radian = 45 * CGFloat.pi / 180
viewB.transform = CGAffineTransform(rotationAngle: radian)

//case 2
//viewA.frame.size = CGSize(width: 300, height: 300)
案例1和案例2: 源代码:
override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    
    
    let viewA = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 100), size: CGSize(width: 200, height: 200)))
    viewA.translatesAutoresizingMaskIntoConstraints = false
    viewA.backgroundColor = .gray
    self.view.addSubview(viewA)
    
    let viewBWidth: CGFloat = 20
    let viewBHeight: CGFloat = 40
    
    let viewB = UIView(frame: CGRect(origin: CGPoint(x: 72, y: 22), size: CGSize(width: viewBWidth, height: viewBHeight)))
    viewB.translatesAutoresizingMaskIntoConstraints = false
    viewB.backgroundColor = .cyan
    
    viewB.clipsToBounds = true //<- important

    //viewB.autoresizingMask = [.flexibleWidth]
    
    viewA.addSubview(viewB)

    //image.size 2560x666
    let image = UIImage(named: "icon")!
    let viewC = UIImageView(frame: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
    viewC.image = image
    
    viewB.addSubview(viewC)
    
    //case 1. rotate B
    //viewA.frame.size = CGSize(width: 300, height: 300)

    //transform
    let radian = 45 * CGFloat.pi / 180
    viewB.transform = CGAffineTransform(rotationAngle: radian)

    //case 2. Changing bounds <-> changing frame
    //viewA.frame.size = CGSize(width: 300, height: 300)
    
    //change bounds -> change frame
    //viewB.bounds.size.width = 60
    
    //change frame -> change bounds
    //viewB.frame.size.width = viewBWidth
    
//change bounds origin
    
    //case 3
    //viewB.bounds.origin.x = 0
    
    //case 4
    //viewB.bounds.origin.x = 1500
}

11

Frame 是视图在其父视图坐标系中的原点(左上角)和大小,这意味着您可以通过改变 frame 原点在其父视图中的位置来移动视图。 Bounds 是视图自己坐标系中的大小和原点,因此默认情况下,bounds 原点是 (0,0)。

大多数情况下,frame 和 bounds 是相等的,但是如果您有一个 frame 为 ((140,65),(200,250)),bounds 为 ((0,0),(200,250)) 的视图,例如将视图倾斜以使其位于右下角,则 bounds 仍然是 ((0,0),(200,250)),但是 frame 不是。

enter image description here

frame 将是包围视图的最小矩形,如照片中所示,frame 将是 ((140,65),(320,320))。

另一个区别是,例如,如果您有一个 superView,其 bounds 为 ((0,0),(200,200)),并且该 superView 具有一个 subView,其 frame 为 ((20,20),(100,100)),并且您将 superView 的 bounds 更改为 ((20,20),(200,200)),那么 subView 的 frame 将仍然是 ((20,20),(100,100)),但会被偏移 (20,20),因为其父视图坐标系已经移动了 (20,20)。

希望这能对某人有所帮助。


关于最后一段:subView 的框架是相对于它的 superView 的。改变 superView 的边界不会使 subView 的原点与其 superView 重合。 - staticVoidMan

7
让我来贡献我的意见。 Frame 是父视图用来将子视图放入其中的位置。 Bounds 则是视图本身用来定位其自己内容的(例如滚动视图在滚动时所做的)。还可以参考 clipsToBounds。同时 Bounds 也可用于对视图中的内容进行缩放和移动等操作。 类比:
Frame ~ 电视屏幕
Bounds ~ 相机(缩放、移动、旋转)

7

以上所有答案都是正确的,这是我的看法:

为了区分框架和边界的概念,开发人员应该阅读以下内容:

  1. 相对于其所包含的父视图(一个父视图),它包含在内 = 框架(FRAME)
  2. 相对于它自己的坐标系,确定其子视图位置 = 边界(BOUNDS)

“bounds”令人困惑,因为它给人的印象是它所设定的视图的坐标位置。但实际上,这些坐标是按照框架常量进行调整的。

iPhone screen


6

Frame属性相对于其父视图进行定位,而Bounds属性相对于其NSView进行定位。

例如:X=40,Y=60。它还包含3个视图。该图表清晰地展示了您的思路。

FRAME

BOUNDS


4
以上回答很好地解释了Bounds和Frames之间的区别。 Bounds:视图在其自己的坐标系中的大小和位置。 Frame:视图相对于其SuperView的大小和位置。 然后有些人会混淆,在Bounds的情况下X,Y将始终为“0”。这是不正确的。这在UIScrollView和UICollectionView中也可以理解。 当bounds的X、Y不为0时, 假设我们有一个UIScrollView并实现了分页,该UIScrollView有3个页面,其ContentSize的宽度是屏幕宽度的三倍(假设ScreenWidth为320)。高度是固定的(假设为200)。
scrollView.contentSize = CGSize(x:320*3, y : 200)

添加三个UIImageViews作为子视图,密切关注frame的x值。

let imageView0 = UIImageView.init(frame: CGRect(x:0, y: 0 , width : scrollView.frame.size.width, height : scrollView.frame.size.height))
let imageView1 :  UIImageView.init( frame: CGRect(x:320, y: 0 , width : scrollView.frame.size.width, height : scrollView.frame.size.height))
let imageView2 :  UIImageView.init(frame: CGRect(x:640, y: 0 , width : scrollView.frame.size.width, height : scrollView.frame.size.height))
scrollView.addSubview(imageView0)
scrollView.addSubview(imageView0)
scrollView.addSubview(imageView0)
  1. 第0页: 当ScrollView处于第0页时,Bounds将为(x:0,y:0, width : 320, height : 200)

  2. 第1页: 滚动并移至第1页。
    现在的边界将是(x:320,y:0, width : 320, height : 200) 记住我们说过相对于它自己的坐标系统。因此,我们的ScrollView的"可见部分"现在将其"x"设置为320。看一下imageView1的框架。

  3. 第2页: 滚动并移至第2页 Bounds : (x:640,y:0, width : 320, height : 200) 同样适用于imageView2的框架。

对于UICollectionView也同样适用。查看collectionView的最简单方法就是滚动并打印/记录其边界,您就可以理解了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,