简体   繁体   English

将子视图放置在圆形视图的边缘

[英]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: 要将小绿色圆圈放在大圆圈的右上角:

  1. Make the small circle a subview of the big circle. 将小圆圈作为大圆圈的子视图。
  2. Add a constraint with the .centerX of the small circle equal to the .trailing of the big circle with a multiplier of 0.8536 . 添加一个约束,使.centerX等于大圆的.trailingmultiplier0.8536
  3. Add a constraint with the .centerY of the small circle equal to the .bottom of the big circle with a multiplier of 0.1464 . 添加一个约束,使.centerY等于大圆的.bottommultiplier0.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. 避免正好成90180度的角度,因为这会产生自动布局不喜欢的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)
    }
}

第二个带有白色边框的图像


Explanation of the math behind 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. 这些值是一个比例和范围从01 ,其中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)-11

The equation: 等式:

1 + cos(angle)

will vary from 0 to 2 depending on the angle. 根据角度从02不等。 Since we're looking for a value from 0 to 1 , we divide this by 2 : 由于我们正在寻找01的值,因此我们将其除以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-11变化,该计算将是从02 ,因此我们除以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弧度)时, sin1 ,因此垂直乘数将为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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM