将子视图定位在圆形视图的边缘

Posted

技术标签:

【中文标题】将子视图定位在圆形视图的边缘【英文标题】:Position a subview on the edge of a circular shaped view 【发布时间】:2019-07-15 10:35:07 【问题描述】:

我正在尝试创建一个类似于以下模型的个人资料图片视图。它有一个小绿点来表示用户的在线状态。

我正在以编程方式创建视图,以便可以重复使用它。以下是我目前的代码。

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。

【问题讨论】:

【参考方案1】:

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

    使小圆圈成为大圆圈的子视图。 添加一个约束,小圆圈的.centerX 等于大圆圈的.trailing0.8536multiplier。 添加一个约束,小圆圈的.centerY 等于大圆圈的.bottom0.1464multiplier

注意:两个multipliers 是使用三角函数通过查看单位圆并计算比率来计算的:(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 的乘数。避免使用 90180 的角度,因为这会产生 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:) 的想法是应该为水平约束计算一个乘数,为垂直约束计算一个乘数。这些值是一个比例,范围从01,其中0 是垂直约束的圆的顶部0 水平约束的圆的边缘。同样,1 是垂直约束的圆的底部1 是水平约束的圆的边缘。

乘数是通过查看三角函数中的the unit circle 来计算的。单位圆是在坐标系上以(0, 0) 为中心半径为1 的圆。单位圆的好处(根据定义)是圆上的点(从原点开始)与圆相交的点是(cos(angle), sin(angle)),其中角度从正x-axis开始逆时针测量到与圆相交的线。注意单位圆的宽度和高度都是2

sin(angle)cos(angle) 各不相同,从 -11

等式:

1 + cos(angle)

会从02 不等,具体取决于角度。由于我们正在寻找从 01 的值,因此我们将其除以 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 弧度。

【讨论】:

感谢您的详细回答。我正在尝试将计算部分合并到我的可重用视图中。所以我删除了硬编码的高度、宽度约束并注释掉了translatesAutoresizingMaskIntoConstraintsbigCirclelittleCircle 视图。因此,现在似乎没有应用绿点定位约束。我应该如何允许用户设置尺寸约束但自己设置定位约束?这是我的code。 translatesAutoresizingMaskIntoConstraints = false留给小圈子。您正在证明该视图的所有 4 个约束。 hh 水平乘数,vv 垂直乘数。 我用自己的三角学和单位圆知识创建了这个“算法”。这个关于the unit circle 的链接可能会有所帮助。 @Scar。试试这个。使用虚拟不可见视图作为小圆圈的占位符。使其成为大圆圈的子视图。将真正的小圆圈添加到superview并添加约束以将其水平和垂直中心设置为dummy view的中心。【参考方案2】:

使用以下内容更改您的初始化函数: 您可以在给定的图片链接中看到结果...

  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)

【讨论】:

感谢您的回答,但绿点必须在右上角。而且我不想硬编码定位值,因为如果图像视图的大小发生变化,绿点也必须移动。 您可以使用任何尺寸的图片框。如您所见,它会起作用,上面附加的图像大小为 250x250,为了将绿点位置移动到右侧,让我试试 用这个换行: onlineStatusView.center = CGPoint(x: bounds.width - bounds.width / 7, y: bounds.height / 7)

以上是关于将子视图定位在圆形视图的边缘的主要内容,如果未能解决你的问题,请参考以下文章

如何将视图边缘弯曲成弧形

如何将边缘设置为零?

在 UIView 中定位文本

在导航栏iOS 11安全区域下定位视图

使用 UIBezierPath 为带有阴影的选定边缘添加角半径 | iOS |斯威夫特 4.2

旋转时在屏幕的所有边缘对齐可组合项