将子视图定位在圆形视图的边缘
Position a subview on the edge of a circular shaped view
我正在尝试创建一个类似于下面模型的个人资料图片视图。它有一个小绿点来表示用户的在线状态。
我正在以编程方式创建视图,以便我可以重用它。以下是我目前的代码。
import UIKit
@IBDesignable
class ProfileView: UIView {
fileprivate var imageView: UIImageView!
fileprivate var onlineStatusView: UIView!
fileprivate var onlineStatusDotView: UIView!
@IBInspectable
var image: UIImage? {
get { return imageView.image }
set { imageView.image = newValue }
}
@IBInspectable
var shouldShowStatusDot: Bool = true
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
backgroundColor = .clear
imageView = UIImageView(frame: bounds)
imageView.backgroundColor = .lightGray
imageView.clipsToBounds = true
imageView.layer.cornerRadius = imageView.frame.height / 2
addSubview(imageView)
onlineStatusView = UIView(frame: CGRect(x: 0, y: 0, width: (bounds.height / 5), height: (bounds.height / 5)))
onlineStatusView.backgroundColor = .white
onlineStatusView.clipsToBounds = true
onlineStatusView.layer.cornerRadius = onlineStatusView.frame.height / 2
addSubview(onlineStatusView)
onlineStatusDotView = UIView(frame: CGRect(x: 0, y: 0, width: (onlineStatusView.bounds.height / 1.3), height: (onlineStatusView.bounds.height / 1.3)))
onlineStatusDotView.center = onlineStatusView.center
onlineStatusDotView.backgroundColor = UIColor(red: 0.17, green: 0.71, blue: 0.45, alpha: 1.0)
onlineStatusDotView.clipsToBounds = true
onlineStatusDotView.layer.cornerRadius = onlineStatusDotView.frame.height / 2
onlineStatusView.addSubview(onlineStatusDotView)
}
}
我丢失的是如何将绿点视图固定在图像视图右上角的 圆形 边缘。显然视图的框架不是圆形的,所以我不知道在这种情况下使用什么自动布局约束。而且我也不想对值进行硬编码,因为它必须根据图像视图的大小移动。
我必须设置哪些自动布局约束才能使其到达正确的位置?
我在这里也上传了一个demo project
使用以下内容更改您的初始化函数:
您可以在给定的图像中看到结果 link...
private func initialize() {
backgroundColor = .clear
imageView = UIImageView(frame: bounds)
imageView.backgroundColor = .lightGray
imageView.clipsToBounds = true
imageView.layer.cornerRadius = imageView.frame.height / 2
addSubview(imageView)
onlineStatusView = UIView(frame: CGRect(x: 0, y: 0, width: (bounds.height / 5), height: (bounds.height / 5)))
onlineStatusView.center = CGPoint(x: bounds.width / 7, y: bounds.height / 7)
onlineStatusView.backgroundColor = .white
onlineStatusView.clipsToBounds = true
onlineStatusView.layer.cornerRadius = onlineStatusView.frame.height / 2
addSubview(onlineStatusView)
onlineStatusDotView = UIView(frame: CGRect(x: 0, y: 0, width: (onlineStatusView.bounds.height / 1.3), height: (onlineStatusView.bounds.height / 1.3)))
onlineStatusDotView.center = CGPoint(x: onlineStatusView.frame.width / 2, y: onlineStatusView.frame.height / 2)
onlineStatusDotView.backgroundColor = UIColor(red: 0.17, green: 0.71, blue: 0.45, alpha: 1.0)
onlineStatusDotView.clipsToBounds = true
onlineStatusDotView.layer.cornerRadius = onlineStatusDotView.frame.height / 2
onlineStatusView.addSubview(onlineStatusDotView)
}
要将小绿圈放在大圈的右上角:
- 让小圆圈成为大圆圈的子视图。
- 添加约束,小圆的
.centerX
等于大圆的.trailing
,multiplier
等于0.8536
。
- 添加约束,小圆的
.centerY
等于大圆的 .bottom
,multiplier
为 0.1464
。
注意: 两个 multiplier
是使用 三角学 通过查看单位圆并计算比率计算得出的: (distance from top of square containing unit circle)/(height of unit circle)
和 (distance from left edge of square containing unit circle)/(width of unit circle)
。在下面的示例代码中,我提供了一个名为 computeMultipliers(angle:)
的 func
,它计算任何 angle
的乘数(以度为单位)。避免完全 90
和 180
的角度,因为这会产生 0
的倍数,而 Auto Layout 不喜欢。
这是独立的例子:
class ViewController: UIViewController {
var bigCircle: UIView!
var littleCircle: UIView!
override func viewDidLoad() {
super.viewDidLoad()
bigCircle = UIView()
bigCircle.translatesAutoresizingMaskIntoConstraints = false
bigCircle.backgroundColor = .red
view.addSubview(bigCircle)
bigCircle.widthAnchor.constraint(equalToConstant: 240).isActive = true
bigCircle.heightAnchor.constraint(equalToConstant: 240).isActive = true
littleCircle = UIView()
littleCircle.translatesAutoresizingMaskIntoConstraints = false
littleCircle.backgroundColor = .green
bigCircle.addSubview(littleCircle)
bigCircle.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
bigCircle.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
littleCircle.widthAnchor.constraint(equalToConstant: 60).isActive = true
littleCircle.heightAnchor.constraint(equalToConstant: 60).isActive = true
let (hMult, vMult) = computeMultipliers(angle: 45)
// position the little green circle using a multiplier on the right and bottom
NSLayoutConstraint(item: littleCircle!, attribute: .centerX, relatedBy: .equal, toItem: bigCircle!, attribute: .trailing, multiplier: hMult, constant: 0).isActive = true
NSLayoutConstraint(item: littleCircle!, attribute: .centerY, relatedBy: .equal, toItem: bigCircle!, attribute: .bottom, multiplier: vMult, constant: 0).isActive = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
bigCircle.layer.cornerRadius = 0.5 * bigCircle.frame.height
littleCircle.layoutIfNeeded()
littleCircle.layer.cornerRadius = 0.5 * littleCircle.frame.height
}
func computeMultipliers(angle: CGFloat) -> (CGFloat, CGFloat) {
let radians = angle * .pi / 180
let h = (1.0 + cos(radians)) / 2
let v = (1.0 - sin(radians)) / 2
return (h, v)
}
}
这是您的代码的修改版本。我添加了约束来设置小圆圈的大小,并将设置 cornerRadius
的代码移动到 layoutSubviews()
:
class ProfilePictureView: UIView {
var bigCircle: UIView!
var borderCircle: UIView!
var littleCircle: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
bigCircle = UIView(frame: bounds)
bigCircle.backgroundColor = .red
addSubview(bigCircle)
borderCircle = UIView()
borderCircle.translatesAutoresizingMaskIntoConstraints = false
borderCircle.backgroundColor = .white
bigCircle.addSubview(borderCircle)
borderCircle.widthAnchor.constraint(equalTo: bigCircle.widthAnchor, multiplier: 1/3).isActive = true
borderCircle.heightAnchor.constraint(equalTo: bigCircle.heightAnchor, multiplier: 1/3).isActive = true
littleCircle = UIView()
littleCircle.translatesAutoresizingMaskIntoConstraints = false
littleCircle.backgroundColor = .green
borderCircle.addSubview(littleCircle)
littleCircle.widthAnchor.constraint(equalTo: borderCircle.widthAnchor, multiplier: 1/1.3).isActive = true
littleCircle.heightAnchor.constraint(equalTo: borderCircle.heightAnchor, multiplier: 1/1.3).isActive = true
littleCircle.centerXAnchor.constraint(equalTo: borderCircle.centerXAnchor).isActive = true
littleCircle.centerYAnchor.constraint(equalTo: borderCircle.centerYAnchor).isActive = true
let (hMult, vMult) = computeMultipliers(angle: 45)
// position the border circle using a multiplier on the right and bottom
NSLayoutConstraint(item: borderCircle!, attribute: .centerX, relatedBy: .equal, toItem: bigCircle!, attribute: .trailing, multiplier: hMult, constant: 0).isActive = true
NSLayoutConstraint(item: borderCircle!, attribute: .centerY, relatedBy: .equal, toItem: bigCircle!, attribute: .bottom, multiplier: vMult, constant: 0).isActive = true
}
override func layoutSubviews() {
super.layoutSubviews()
bigCircle.layer.cornerRadius = bigCircle.frame.height / 2
borderCircle.layoutIfNeeded()
borderCircle.layer.cornerRadius = borderCircle.frame.height / 2
littleCircle.layoutIfNeeded()
littleCircle.layer.cornerRadius = littleCircle.frame.height / 2
}
private func computeMultipliers(angle: CGFloat) -> (CGFloat, CGFloat) {
let radians = angle * .pi / 180
let h = (1.0 + cos(radians)) / 2
let v = (1.0 - sin(radians)) / 2
return (h, v)
}
}
computeMultipliers(angle:)
背后的数学解释
computeMultipliers(angle:)
的想法是应该为 水平约束 计算一个乘数,为 垂直约束 计算一个乘数].这些值是一个比例,范围从 0
到 1
,其中 0
是垂直约束圆的 top,0
是水平约束的圆的 left 边缘。同样,1
是垂直约束的圆的 底部 ,1
是圆的 右 边缘对于水平约束。
乘数是通过查看三角学中的 the unit circle 计算得出的。单位圆是坐标系上以(0, 0)
为中心,半径1
的圆。单位圆(根据定义)的好处是,圆上的一条线(从原点开始)与圆相交的点是 (cos(angle), sin(angle))
,其中角度从正 x-axis
开始测量counter-clockwise 到与圆相交的线。注意单位圆的宽和高都是2
.
sin(angle)
和 cos(angle)
从 -1
到 1
。
等式:
1 + cos(angle)
会根据角度从 0
到 2
不等。由于我们正在寻找从 0
到 1
的值,我们将其除以 2
:
// compute the horizontal multiplier based upon the angle
let h = (1.0 + cos(radians)) / 2
在垂直方向,我们首先注意到坐标系是从数学意义上翻转的。在iOS中,y
是向下生长的,而在数学中,y
是向上生长的。考虑到这一点,垂直计算使用负 -
而不是 +
:
1 - sin(angle)
同样,由于 sin
从 -1
到 1
,这个计算将从 0
到 2
,所以我们除以 2
:
// compute the vertical multiplier based upon the angle
let h = (1.0 - sin(radians)) / 2
这给了我们想要的结果。当角度为 90
度(或 .pi/2
弧度)时,sin
为 1
,因此垂直倍数将为 0
。当角度为 270
度(或 3*.pi/2
弧度)时,sin
为 -1
,垂直倍数将为 1
.
为什么要使用弧度? 一旦您理解了弧度是什么,它就会很直观。它们只是沿单位圆圆周的弧长。圆的周长公式是circumference = 2 * .pi * radius
,所以单位圆的周长是2 * .pi
。所以 360
度是 2 * .pi
弧度。
我正在尝试创建一个类似于下面模型的个人资料图片视图。它有一个小绿点来表示用户的在线状态。
我正在以编程方式创建视图,以便我可以重用它。以下是我目前的代码。
import UIKit
@IBDesignable
class ProfileView: UIView {
fileprivate var imageView: UIImageView!
fileprivate var onlineStatusView: UIView!
fileprivate var onlineStatusDotView: UIView!
@IBInspectable
var image: UIImage? {
get { return imageView.image }
set { imageView.image = newValue }
}
@IBInspectable
var shouldShowStatusDot: Bool = true
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
backgroundColor = .clear
imageView = UIImageView(frame: bounds)
imageView.backgroundColor = .lightGray
imageView.clipsToBounds = true
imageView.layer.cornerRadius = imageView.frame.height / 2
addSubview(imageView)
onlineStatusView = UIView(frame: CGRect(x: 0, y: 0, width: (bounds.height / 5), height: (bounds.height / 5)))
onlineStatusView.backgroundColor = .white
onlineStatusView.clipsToBounds = true
onlineStatusView.layer.cornerRadius = onlineStatusView.frame.height / 2
addSubview(onlineStatusView)
onlineStatusDotView = UIView(frame: CGRect(x: 0, y: 0, width: (onlineStatusView.bounds.height / 1.3), height: (onlineStatusView.bounds.height / 1.3)))
onlineStatusDotView.center = onlineStatusView.center
onlineStatusDotView.backgroundColor = UIColor(red: 0.17, green: 0.71, blue: 0.45, alpha: 1.0)
onlineStatusDotView.clipsToBounds = true
onlineStatusDotView.layer.cornerRadius = onlineStatusDotView.frame.height / 2
onlineStatusView.addSubview(onlineStatusDotView)
}
}
我丢失的是如何将绿点视图固定在图像视图右上角的 圆形 边缘。显然视图的框架不是圆形的,所以我不知道在这种情况下使用什么自动布局约束。而且我也不想对值进行硬编码,因为它必须根据图像视图的大小移动。
我必须设置哪些自动布局约束才能使其到达正确的位置?
我在这里也上传了一个demo project
使用以下内容更改您的初始化函数: 您可以在给定的图像中看到结果 link...
private func initialize() {
backgroundColor = .clear
imageView = UIImageView(frame: bounds)
imageView.backgroundColor = .lightGray
imageView.clipsToBounds = true
imageView.layer.cornerRadius = imageView.frame.height / 2
addSubview(imageView)
onlineStatusView = UIView(frame: CGRect(x: 0, y: 0, width: (bounds.height / 5), height: (bounds.height / 5)))
onlineStatusView.center = CGPoint(x: bounds.width / 7, y: bounds.height / 7)
onlineStatusView.backgroundColor = .white
onlineStatusView.clipsToBounds = true
onlineStatusView.layer.cornerRadius = onlineStatusView.frame.height / 2
addSubview(onlineStatusView)
onlineStatusDotView = UIView(frame: CGRect(x: 0, y: 0, width: (onlineStatusView.bounds.height / 1.3), height: (onlineStatusView.bounds.height / 1.3)))
onlineStatusDotView.center = CGPoint(x: onlineStatusView.frame.width / 2, y: onlineStatusView.frame.height / 2)
onlineStatusDotView.backgroundColor = UIColor(red: 0.17, green: 0.71, blue: 0.45, alpha: 1.0)
onlineStatusDotView.clipsToBounds = true
onlineStatusDotView.layer.cornerRadius = onlineStatusDotView.frame.height / 2
onlineStatusView.addSubview(onlineStatusDotView)
}
要将小绿圈放在大圈的右上角:
- 让小圆圈成为大圆圈的子视图。
- 添加约束,小圆的
.centerX
等于大圆的.trailing
,multiplier
等于0.8536
。 - 添加约束,小圆的
.centerY
等于大圆的.bottom
,multiplier
为0.1464
。
注意: 两个 multiplier
是使用 三角学 通过查看单位圆并计算比率计算得出的: (distance from top of square containing unit circle)/(height of unit circle)
和 (distance from left edge of square containing unit circle)/(width of unit circle)
。在下面的示例代码中,我提供了一个名为 computeMultipliers(angle:)
的 func
,它计算任何 angle
的乘数(以度为单位)。避免完全 90
和 180
的角度,因为这会产生 0
的倍数,而 Auto Layout 不喜欢。
这是独立的例子:
class ViewController: UIViewController {
var bigCircle: UIView!
var littleCircle: UIView!
override func viewDidLoad() {
super.viewDidLoad()
bigCircle = UIView()
bigCircle.translatesAutoresizingMaskIntoConstraints = false
bigCircle.backgroundColor = .red
view.addSubview(bigCircle)
bigCircle.widthAnchor.constraint(equalToConstant: 240).isActive = true
bigCircle.heightAnchor.constraint(equalToConstant: 240).isActive = true
littleCircle = UIView()
littleCircle.translatesAutoresizingMaskIntoConstraints = false
littleCircle.backgroundColor = .green
bigCircle.addSubview(littleCircle)
bigCircle.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
bigCircle.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
littleCircle.widthAnchor.constraint(equalToConstant: 60).isActive = true
littleCircle.heightAnchor.constraint(equalToConstant: 60).isActive = true
let (hMult, vMult) = computeMultipliers(angle: 45)
// position the little green circle using a multiplier on the right and bottom
NSLayoutConstraint(item: littleCircle!, attribute: .centerX, relatedBy: .equal, toItem: bigCircle!, attribute: .trailing, multiplier: hMult, constant: 0).isActive = true
NSLayoutConstraint(item: littleCircle!, attribute: .centerY, relatedBy: .equal, toItem: bigCircle!, attribute: .bottom, multiplier: vMult, constant: 0).isActive = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
bigCircle.layer.cornerRadius = 0.5 * bigCircle.frame.height
littleCircle.layoutIfNeeded()
littleCircle.layer.cornerRadius = 0.5 * littleCircle.frame.height
}
func computeMultipliers(angle: CGFloat) -> (CGFloat, CGFloat) {
let radians = angle * .pi / 180
let h = (1.0 + cos(radians)) / 2
let v = (1.0 - sin(radians)) / 2
return (h, v)
}
}
这是您的代码的修改版本。我添加了约束来设置小圆圈的大小,并将设置 cornerRadius
的代码移动到 layoutSubviews()
:
class ProfilePictureView: UIView {
var bigCircle: UIView!
var borderCircle: UIView!
var littleCircle: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
bigCircle = UIView(frame: bounds)
bigCircle.backgroundColor = .red
addSubview(bigCircle)
borderCircle = UIView()
borderCircle.translatesAutoresizingMaskIntoConstraints = false
borderCircle.backgroundColor = .white
bigCircle.addSubview(borderCircle)
borderCircle.widthAnchor.constraint(equalTo: bigCircle.widthAnchor, multiplier: 1/3).isActive = true
borderCircle.heightAnchor.constraint(equalTo: bigCircle.heightAnchor, multiplier: 1/3).isActive = true
littleCircle = UIView()
littleCircle.translatesAutoresizingMaskIntoConstraints = false
littleCircle.backgroundColor = .green
borderCircle.addSubview(littleCircle)
littleCircle.widthAnchor.constraint(equalTo: borderCircle.widthAnchor, multiplier: 1/1.3).isActive = true
littleCircle.heightAnchor.constraint(equalTo: borderCircle.heightAnchor, multiplier: 1/1.3).isActive = true
littleCircle.centerXAnchor.constraint(equalTo: borderCircle.centerXAnchor).isActive = true
littleCircle.centerYAnchor.constraint(equalTo: borderCircle.centerYAnchor).isActive = true
let (hMult, vMult) = computeMultipliers(angle: 45)
// position the border circle using a multiplier on the right and bottom
NSLayoutConstraint(item: borderCircle!, attribute: .centerX, relatedBy: .equal, toItem: bigCircle!, attribute: .trailing, multiplier: hMult, constant: 0).isActive = true
NSLayoutConstraint(item: borderCircle!, attribute: .centerY, relatedBy: .equal, toItem: bigCircle!, attribute: .bottom, multiplier: vMult, constant: 0).isActive = true
}
override func layoutSubviews() {
super.layoutSubviews()
bigCircle.layer.cornerRadius = bigCircle.frame.height / 2
borderCircle.layoutIfNeeded()
borderCircle.layer.cornerRadius = borderCircle.frame.height / 2
littleCircle.layoutIfNeeded()
littleCircle.layer.cornerRadius = littleCircle.frame.height / 2
}
private func computeMultipliers(angle: CGFloat) -> (CGFloat, CGFloat) {
let radians = angle * .pi / 180
let h = (1.0 + cos(radians)) / 2
let v = (1.0 - sin(radians)) / 2
return (h, v)
}
}
computeMultipliers(angle:)
背后的数学解释
computeMultipliers(angle:)
的想法是应该为 水平约束 计算一个乘数,为 垂直约束 计算一个乘数].这些值是一个比例,范围从 0
到 1
,其中 0
是垂直约束圆的 top,0
是水平约束的圆的 left 边缘。同样,1
是垂直约束的圆的 底部 ,1
是圆的 右 边缘对于水平约束。
乘数是通过查看三角学中的 the unit circle 计算得出的。单位圆是坐标系上以(0, 0)
为中心,半径1
的圆。单位圆(根据定义)的好处是,圆上的一条线(从原点开始)与圆相交的点是 (cos(angle), sin(angle))
,其中角度从正 x-axis
开始测量counter-clockwise 到与圆相交的线。注意单位圆的宽和高都是2
.
sin(angle)
和 cos(angle)
从 -1
到 1
。
等式:
1 + cos(angle)
会根据角度从 0
到 2
不等。由于我们正在寻找从 0
到 1
的值,我们将其除以 2
:
// compute the horizontal multiplier based upon the angle
let h = (1.0 + cos(radians)) / 2
在垂直方向,我们首先注意到坐标系是从数学意义上翻转的。在iOS中,y
是向下生长的,而在数学中,y
是向上生长的。考虑到这一点,垂直计算使用负 -
而不是 +
:
1 - sin(angle)
同样,由于 sin
从 -1
到 1
,这个计算将从 0
到 2
,所以我们除以 2
:
// compute the vertical multiplier based upon the angle
let h = (1.0 - sin(radians)) / 2
这给了我们想要的结果。当角度为 90
度(或 .pi/2
弧度)时,sin
为 1
,因此垂直倍数将为 0
。当角度为 270
度(或 3*.pi/2
弧度)时,sin
为 -1
,垂直倍数将为 1
.
为什么要使用弧度? 一旦您理解了弧度是什么,它就会很直观。它们只是沿单位圆圆周的弧长。圆的周长公式是circumference = 2 * .pi * radius
,所以单位圆的周长是2 * .pi
。所以 360
度是 2 * .pi
弧度。