ios - 将 subview 定位在圆形 View 的边缘

标签 ios swift uiview autolayout position

我正在尝试创建一个看起来像下面的模型的个人资料图片 View 。它有一个小绿点来表示用户的在线状态。

enter image description here

我正在以编程方式创建 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)
    }
}

enter image description here

我失去的是如何将绿点 View 固定在 ImageView 右上角的圆形边缘。显然 View 的框架不是圆形的,所以我不知道在这种情况下要使用什么自动布局约束。而且我也不想硬编码这些值,因为它必须根据 ImageView 的大小移动。

我必须设置哪些自动布局约束才能将其放置到正确的位置?

我上传了 demo project这里也是。

最佳答案

将绿色小圆圈放在大圆圈的右上角:

  • 使小圆圈成为大圆圈的 subview 。
  • 使用 .centerX 添加约束的小圆圈等于 .trailing带有 multiplier 的大圆圈的 0.8536 .
  • 使用 .centerY 添加约束的小圆圈等于 .bottom带有 multiplier 的大圆圈的 0.1464 .

  • 注:两个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) .在下面的示例代码中,我提供了 func调用computeMultipliers(angle:)计算任何 angle 的乘数度数。完全避免角度 90180因为这可以创建 0 的乘数哪个自动布局不喜欢。

    这是独立的示例:
    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)
        }
    }
    

    image of sample code running in the simulator

    这是您的代码的修改版本。我添加了约束来设置小圆圈的大小,并移动了设置 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)
        }
    }
    

    second image with white border
    computeMultipliers(angle:) 背后的数学解释computeMultipliers(angle:) 的想法就是应该计算水平约束的乘数和垂直约束的乘数。这些值是一个比例,范围为 01在哪里 0是垂直约束的圆的顶部,0是水平约束的圆的左边缘。同样,1是垂直约束的圆底,1是水平约束的圆的右边缘。
    通过查看 the unit circle 来计算乘数。在三角学中。单位圆是半径为 1 的圆以 (0, 0) 为中心在坐标系上。单位圆的好处(根据定义)是圆上一条线(从原点开始)与圆相交的点是(cos(angle), sin(angle))其中角度从正 x-axis 开始测量逆时针到与圆相交的线。注意单位圆的宽度和高度都是2 .sin(angle)cos(angle)各不相同 -11 .
    方程:
    1 + cos(angle)
    
    将不同于 02取决于角度。因为我们正在寻找 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不同于 -11 ,此计算将来自 02 , 所以我们除以 2 :
    // compute the vertical multiplier based upon the angle
    let h = (1.0 - sin(radians)) / 2
    
    这给了我们想要的结果。当角度为90度(或 .pi/2 弧度),sin1 ,因此垂直乘数将为 0 .当角度为270度(或 3*.pi/2 弧度),sin-1垂直乘数为 1 .
    为什么使用弧度?一旦你了解了弧度是什么,它们就很直观。它们只是沿单位圆圆周的弧长。圆周长的公式是circumference = 2 * .pi * radius ,所以对于单位圆,周长是2 * .pi .所以360度数是 2 * .pi弧度。

    关于ios - 将 subview 定位在圆形 View 的边缘,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58471055/

    相关文章:

    swift - NSEvent.addLocalMonitorForEvents 的内存泄漏

    ios uiview 自动展开尺寸

    ios - Xcode 5 : Images disappear in storyboard when switching view between ios6 and ios7

    ios - resignFirstResponder 与 endEditing 键盘关闭

    swift - 是否可以在 Swift 中为 UITextField 使用不同的 UIMenuController 实例?

    ios - 如何在 SpriteKit 中使用 UITextView?

    iphone - 使用动画从 SuperView 中删除 UIVIew

    ios - 为什么变量 "preferredStatusBarStyle"会删除标签栏?

    ios - 如何在 iOS 6 上创建和保存 EKCalendar

    ios - 如何使用 SpriteKit 根据点击来上下移动球?