[英]Position a subview on the edge of a circular shaped view
I'm trying to create a profile picture view that looks like the mock-up below. 我正在尝试创建一个个人资料图片视图,看起来像下面的模型。 It has a small green dot to denote the user's online status. 它带有一个小绿点,表示用户的在线状态。
I'm creating the view programmatically so I can reuse it. 我正在以编程方式创建视图,因此可以重用它。 Below is my code so far. 下面是到目前为止的代码。
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)
}
}
What has me lost is how to pin the green dot view on the circular edge of the top right corner of the image view. 我丢失的是如何将绿色圆点视图固定在图像视图右上角的圆形边缘上。 Obviously the view's frame isn't circular so I can't figure out what auto layout constraints to use in this case. 显然,视图的框架不是圆形的,因此我无法弄清楚在这种情况下要使用的自动布局约束。 And I don't want to hardcode the values either because it has to move depending on the size of the image view. 而且我也不想对值进行硬编码,因为它必须根据图像视图的大小移动。
What auto layout constraints do I have to set to get it to the right position? 我必须设置哪些自动布局约束才能将其放置在正确的位置?
I uploaded a demo project here as well. 我也在这里上传了一个演示项目 。
To place the small green circle in the upper right corner of the big circle: 要将小绿色圆圈放在大圆圈的右上角:
.centerX
of the small circle equal to the .trailing
of the big circle with a multiplier
of 0.8536
. 添加一个约束,使.centerX
等于大圆的.trailing
, multiplier
为0.8536
。 .centerY
of the small circle equal to the .bottom
of the big circle with a multiplier
of 0.1464
. 添加一个约束,使.centerY
等于大圆的.bottom
, multiplier
为0.1464
。 Note: The two multiplier
s were computed using Trigonometry by looking at the unit circle and computing the ratios: (distance from top of square containing unit circle)/(height of unit circle)
and (distance from left edge of square containing unit circle)/(width of unit circle)
. 注意:两个multiplier
s是使用三角函数通过查看单位圆并计算比率来计算的:( (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)
。 In the sample code below, I have provided a func
called computeMultipliers(angle:)
which computes the multipliers for any angle
in degrees. 在下面的示例代码中,我提供了一个称为computeMultipliers(angle:)
的func
,该func
可计算任意angle
以度为单位)的乘数。 Avoid angles exactly 90
and 180
because that can create multipliers of 0
which Auto Layout does not like. 避免正好成90
和180
度的角度,因为这会产生自动布局不喜欢的0
乘数。
Here is standalone example: 这是一个独立的示例:
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)
}
}
Here is a modified version of your code. 这是您代码的修改版本。 I added constraints to set the size of the small circle and moved the code which sets the cornerRadius
to layoutSubviews()
: 我添加了约束来设置小圆圈的大小,并移动了将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:)
背后的数学解释 The idea of computeMultipliers(angle:)
is that is should compute a multiplier for the horizontal constraint and a multiplier for the vertical constraint . computeMultipliers(angle:)
的想法是应该为水平约束计算一个乘数,为垂直约束计算一个乘数。 These values are a proportion and will range from 0
to 1
where 0
is the top of the circle for the vertical constraint and 0
is the left edge of the circle for the horizontal constraint. 这些值是一个比例和范围从0
到1
,其中0
是圆的垂直约束的顶部和0
是圆形用于水平约束的左边缘。 Likewise, 1
is the bottom of the circle for the vertical constraint and 1
is the right edge of the circle for the horizontal constraint. 同样地, 1
是该圆的用于垂直约束底部和1
是圆圈用于水平约束的右边缘。
The multipliers are computed by looking at the unit circle in Trigonometry. 通过查看三角学中的单位圆来计算乘数。 The unit circle is a circle of radius 1
centered at (0, 0)
on the coordinate system. 单位圆是在坐标系上以(0, 0)
为中心的半径1
的圆。 The nice thing about the unit circle (by definition) is that the point on the circle where a line (starting at the origin) intersects the circle is (cos(angle), sin(angle))
where the angle is measured starting at positive x-axis
going counter-clockwise to the line that intersects the circle. 关于单位圆(按定义)的好处是,圆上的一条线(从原点开始)与圆相交的点是(cos(angle), sin(angle))
,在该点开始以正角度进行测量x-axis
逆时针方向与圆相交的线。 Note the the width and height of the unit circle are each 2
. 请注意,单位圆的宽度和高度均为2
。
sin(angle)
and cos(angle)
each vary from -1
to 1
. sin(angle)
和cos(angle)
从-1
到1
。
The equation: 等式:
1 + cos(angle)
will vary from 0
to 2
depending on the angle. 根据角度从0
到2
不等。 Since we're looking for a value from 0
to 1
, we divide this by 2
: 由于我们正在寻找0
到1
的值,因此我们将其除以2
:
// compute the horizontal multiplier based upon the angle
let h = (1.0 + cos(radians)) / 2
In the vertical direction, we first note the coordinate system is flipped from the mathematical sense. 在垂直方向上,我们首先注意到坐标系是从数学意义上翻转的。 In iOS, y
grows in the downward direction, but in mathematics, y
grows in the upward direction. 在iOS中, y
沿向下方向增长,但在数学上, y
沿向上方向增长。 To account for this, the vertical calculation uses minus -
instead of +
: 为了解决这个问题,垂直计算使用负号-
代替+
:
1 - sin(angle)
Again, since sin
varies from -1
to 1
, this calculation will be from 0
to 2
, so we divide by 2
: 同样,由于sin
从-1
到1
变化,该计算将是从0
到2
,因此我们除以2
:
// compute the vertical multiplier based upon the angle
let h = (1.0 - sin(radians)) / 2
This gives us the desired result. 这给了我们想要的结果。 When the angle is 90
degrees (or .pi/2
radians), sin
is 1
, so the vertical multiplier will be 0
. 当角度为90
度(或.pi/2
弧度)时, sin
为1
,因此垂直乘数将为0
。 When the angle is 270
degrees (or 3*.pi/2
radians), sin
is -1
and the vertical multiplier will be 1
. 当角度为270
度(或3*.pi/2
弧度)时, sin
为-1
,垂直乘数将为1
。
Why use radians? 为什么要使用弧度? Radians are intuitive once you understand what they are. 一旦了解弧度,弧度就会很直观。 They are just the length of arc along the circumference of the unit circle. 它们只是沿单位圆的圆周的弧长。 The formula for the circumference of a circle is circumference = 2 * .pi * radius
, so for the unit circle, the circumference is 2 * .pi
. 圆的圆周的公式为circumference = 2 * .pi * radius
,因此对于单位圆,圆周为2 * .pi
。 So 360
degrees is 2 * .pi
radians. 因此360
度为2 * .pi
弧度。
Change your initialize function with the following: You can see the result in the given image 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)
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.