1.了解基本情况
首先,让我们从几个解释开始。框架和边界都在视图中*描述了两件事*:
- 视图的原点作为 CGPoint 值(水平和垂直轴上的 x 和 y 值),
- 视图的大小作为 CGSize 值(宽度和高度)。
但是,如果它们都提供类似的信息,那么它们的真正区别在哪里呢?
好吧,区别在于**参考坐标系**。具体来说:
- *Frame 指的是视图容器(父视图)的坐标系*。
- *边界是指视图自己的坐标系。*
例如,假设一个视图控制器除了它自己的视图之外只包含一个视图(一个子视图)。该子视图的框架*在视图控制器视图的坐标中描述了它的原点和大小*,因为在这种情况下后者是容器(父)视图。容器的零原点(x=0 和 y=0)位于左上角,*子视图的原点是与该点在两个轴上的距离*。关于子视图的大小,即在任何给定时刻*围绕子视图*的虚拟矩形的宽度和高度。
当谈到边界时,原点是子视图本身的左上角,在它自己的坐标系*中总是等于零*(0, 0)。宽度和高度表示视图的实际大小,并且始终保持不变,*无论可能已应用于视图的任何转换*。稍后再详细介绍。
接下来我们将把所有这些都说清楚。在我们到达那里之前,下图说明了父视图和子视图的原点,如上所述:
> **注意**:原点在 iOS 中是左上角,但在 macOS 中不是这样;原点是左下角。
基于以上所有,我认为很明显原点值是帧和边界之间的第一个差异。但这不是唯一的。尺寸也可以不同!
默认情况下,视图的大小在 frame 和 bounds 中是相同的。但是,如果我们以某种方式转换视图,则此声明将不再有效;*旋转、平移(移动)和缩放*,它们都会影响框架;原点和大小!
这通常是真正的问题开始的地方。如果存在基于一个或多个视图的框架的 UI 相关计算,并且这些视图中的任何一个被转换,则计算将是错误的,因为大小已更改。当然,除非这是故意的。如果不是这样,就会出现令人尴尬的视觉结果,这反过来又会导致在试图弄清楚为什么界面不像最初应该的那样表现时令人头疼。
也就是说,是时候进行快速实现了,以澄清前面提到的框架和边界之间的差异。
2.实施演示
在Xcode和一个全新的基于 UIKit 的 iOS 应用程序中,我们将实现以下内容:
- 一个简单的视图,我们将用作演示以演示其框架和边界。
- 另一个视图将围绕第一个视图,目的是在任何给定时刻直观地显示演示视图的实际框架。
- 第三个视图将反映演示视图的边界。
- 每次将第一个视图旋转几度的按钮。
- 两个标签将显示第一个视图的框架和边界的值。
所有的 UI 实现都将以编程方式完成;没有使用情节提要。所以,让我们从每个新 UIKit 项目包含的默认视图控制器开始,我们将在其中声明我们需要的几个属性:
```text
class ViewController: UIViewController {
var demoView: UIView!
var frameView: UIView!
var boundsView: UIView!
var frameLabel: UILabel!
var boundsLabel: UILabel!
var rotateButton: UIButton!
...
}
```
除了这些,我们还需要一个属性来保持演示视图的当前旋转(以度为单位):
```text
var rotationDegrees: CGFloat = 0
```
接下来,我们将在 ViewController 类中定义一些方法。他们中的大多数将配置我们刚刚在上面声明的控件。
我们将关注的第一种方法是关于演示视图的初始化和配置。正如您接下来将看到的,我们给它一个背景和一个边框颜色,以使其在屏幕上具有视觉上的可区分性和突出性。
```text
func setupDemoView() {
demoView = UIView(frame: .zero)
demoView.backgroundColor = UIColor(red: 0, green: 0, blue: 255, alpha: 0.25)
demoView.layer.borderWidth = 1
demoView.layer.borderColor = UIColor.blue.cgColor
view.addSubview(demoView)
// Setup auto-layout constraints.
demoView.translatesAutoresizingMaskIntoConstraints = false
demoView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
demoView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
demoView.widthAnchor.constraint(equalToConstant: 300).isActive = true
demoView.heightAnchor.constraint(equalToConstant: 140).isActive = true
}
```
之后,我们将初始化和配置`frameView`将围绕演示视图的。这里会有两个不同之处;框架视图将只有一个彩色边框,最重要的是,我们不会对其*设置任何布局约束*!相反,每次演示视图的框架发生变化时,我们都会动态设置它的框架:
```text
func setupFrameView() {
frameView = UIView(frame: .zero)
frameView.layer.borderWidth = 2
frameView.layer.borderColor = UIColor.magenta.cgColor
self.view.addSubview(frameView)
}
```
非常相似,我们将初始化和配置`boundsView`. 我们使用此视图的目标是直观地表示演示视图的边界,我们将通过*将演示视图的边界设置为* `boundsView`. 再一次,我们也不会为此视图设置任何自动布局约束。
```text
func setupBoundsView() {
boundsView = UIView(frame: .zero)
boundsView.layer.borderWidth = 2
boundsView.layer.borderColor = UIColor.green.cgColor
self.view.addSubview(boundsView)
}
```
在完成上述所有操作之后,我们将添加一个按钮。它存在的目的很简单;每次我们点击它时,我们都会通过旋转几度来改变演示视图的转换。这种方法没有什么特别困难的,所以这里是它的实现:
```text
func setupRotateButton() {
rotateButton = UIButton(type: .system, primaryAction: UIAction(handler: { _ in
self.transform()
}))
rotateButton.setTitle("Rotate", for: .normal)
view.addSubview(rotateButton)
// Specify auto-layout constraints.
rotateButton.translatesAutoresizingMaskIntoConstraints = false
rotateButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
rotateButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 200).isActive = true
rotateButton.widthAnchor.constraint(equalToConstant: 80).isActive = true
rotateButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
}
```
我们很快就会定义`transform()`在按钮的动作闭包中被调用的方法;暂时不用管它。
> **注意**:如果您想了解更多带有动作闭包的按钮,请查看[这篇文章](https://link.zhihu.com/?target=https%3A//www.jianshu.com/p/3027596f1a97)。
最后要配置的视图是两个标签,它们将报告演示视图的框架和边界值。它们的初始化和配置以两种不同的方法进行,但正如您接下来将看到的,还定义了另外两种方法。第一个实现两个标签之间的通用属性,另一个实现通用的自动布局约束。这样做有助于避免重复相同的代码两次:
```text
func setupFrameLabel() {
frameLabel = UILabel()
setCommonProperties(toLabel: frameLabel)
view.addSubview(frameLabel)
// Specify auto-layout constraints.
frameLabel.translatesAutoresizingMaskIntoConstraints = false
frameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
// Configure common constraints.
setCommonConstraints(toLabel: frameLabel)
}
func setupBoundsLabel() {
boundsLabel = UILabel()
setCommonProperties(toLabel: boundsLabel)
view.addSubview(boundsLabel)
// Specify auto-layout constraints.
boundsLabel.translatesAutoresizingMaskIntoConstraints = false
boundsLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// Configure common constraints.
setCommonConstraints(toLabel: boundsLabel)
}
func setCommonProperties(toLabel label: UILabel) {
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.font = .boldSystemFont(ofSize: 12)
}
func setCommonConstraints(toLabel label: UILabel) {
label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16)
.isActive = true
label.widthAnchor.constraint(equalToConstant: view.bounds.size.width/2)
.isActive = true
label.heightAnchor.constraint(equalToConstant: 120)
.isActive = true
}
```
3.最后的接触
在这个小演示应用程序中完成了所有视图的配置之后,是时候添加另外三个缺失的方法了。我们将从两个类似的方法开始,它们将负责将演示视图的框架和边界值设置为两个标签的文本。但是,为了您自己的方便,我们也会让他们在控制台上打印这些值:
```text
func printFrame() {
print("-- FRAME --")
print("X: \(demoView.frame.origin.x)")
print("Y: \(demoView.frame.origin.y)")
print("Width: \(demoView.frame.size.width)")
print("Height: \(demoView.frame.size.height)")
print("")
frameLabel.text = """
-- FRAME --
X: \(demoView.frame.origin.x.rounded())
Y: \(demoView.frame.origin.y.rounded())
Width: \(demoView.frame.size.width.rounded())
Height: \(demoView.frame.size.height.rounded())
"""
}
func printBounds() {
print("-- Bounds --")
print("X: \(demoView.bounds.origin.x)")
print("Y: \(demoView.bounds.origin.y)")
print("Width: \(demoView.bounds.size.width)")
print("Height: \(demoView.bounds.size.height)")
print("")
boundsLabel.text = """
-- BOUNDS --
X: \(demoView.bounds.origin.x)
Y: \(demoView.bounds.origin.y)
Width: \(demoView.bounds.size.width)
Height: \(demoView.bounds.size.height)
"""
}
```
接下来,我们将实现`transform()`我们之前第一次遇到的方法,每次点击旋转按钮时都会调用它。其中发生了三件重要的事情:
- 演示视图每次旋转 15 度,
- 得到`frameView`一个新框架;*演示视图的框架*,
- 也得到一个新的`boundsView`框架;*演示视图的边界*。
此外,我们还会调用`printFrame()`and`printBounds()`方法来更新两个标签的内容。
这就是全部:
```text
func transform() {
// Rotate the demo view.
rotationDegrees += 15
demoView.transform = CGAffineTransform(rotationAngle: rotationDegrees * .pi / 180)
// Update the frame of the frameView.
frameView.frame = demoView.frame
// Update the frame of the boundsView.
boundsView.frame = demoView.bounds
// Update the content of the two labels with
// the new frame and bounds values.
printFrame()
printBounds()
}
```
最后,让我们在屏幕上布局所有内容,让我们显示两个标签的第一个内容;我们将在`viewWillAppear(_:)`方法中完成所有这些:
```text
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setupBoundsView()
setupFrameView()
setupDemoView()
setupFrameLabel()
setupBoundsLabel()
setupRotateButton()
view.layoutIfNeeded()
printFrame()
printBounds()
}
```
4.见证框架和边界的差异
------
本教程的小应用程序已经准备好了,是时候尝试一下了。在真实设备或模拟器中运行它,准备好后开始点击旋转按钮旋转演示视图。
以下是您将看到的情况:
当演示视图旋转时,请注意:
- 的边框`frameView`围绕演示视图的虚拟矩形“绘制”。*你看到的是旋转后的demo view的真实框架*!
- 显示在`boundsView`其框架指示的位置,这等于演示视图的边界。我们清楚地意识到,*在旋转演示视图时,边界视图始终保持不变*。
屏幕底部的两个标签也说明了这一点。每次旋转时,原点以及演示视图框架的大小都会获得反映视图位置和尺寸的新值。但是,各个边界值保持不变,我们可以在右侧的第二个标签中看到!
`borderView`看到绿色边框颜色位于屏幕左上角,不要感到惊讶。这是我们应该预料到的,因为它的帧的原点值与`demoView`边界的原点匹配,它等于 (0, 0)。
尽管在这篇文章中我们只是通过旋转来更改演示视图的转换,但结果也与其他转换相似,例如缩放或平移。在所有情况下,演示视图的原始框架都会根据应用的转换进行更改。
原文https://zhuanlan.zhihu.com/p/505314620
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓