将子视图定位在圆形视图的边缘 [英] Position a subview on the edge of a circular shaped view

查看:30
本文介绍了将子视图定位在圆形视图的边缘的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

导入 UIKit@IBDesignable类 ProfileView: UIView {文件私有 var imageView: UIImageView!fileprivate var onlineStatusView:UIView!fileprivate var onlineStatusDotView:UIView!@IBInspectable变种图像:UIImage?{获取 { 返回 imageView.image }设置 { imageView.image = newValue }}@IBInspectablevar shouldShowStatusDot: Bool = true覆盖初始化(帧:CGRect){超级初始化(帧:帧)初始化()}需要初始化?(编码器 aDecoder:NSCoder){super.init(编码器:aDecoder)初始化()}私人函数初始化(){背景颜色 = .clearimageView = UIImageView(框架:边界)imageView.backgroundColor = .lightGrayimageView.clipsToBounds = trueimageView.layer.cornerRadius = imageView.frame.height/2添加子视图(图像视图)onlineStatusView = UIView(frame: CGRect(x: 0, y: 0, width: (bounds.height/5), height: (bounds.height/5)))onlineStatusView.backgroundColor = .whiteonlineStatusView.clipsToBounds = trueonlineStatusView.layer.cornerRadius = onlineStatusView.frame.height/2addSubview(onlineStatusView)onlineStatusDotView = UIView(frame: CGRect(x: 0, y: 0, width: (onlineStatusView.bounds.height/1.3), height: (onlineStatusView.bounds.height/1.3)))onlineStatusDotView.center = onlineStatusView.centeronlineStatusDotView.backgroundColor = UIColor(红色:0.17,绿色:0.71,蓝色:0.45,alpha:1.0)onlineStatusDotView.clipsToBounds = trueonlineStatusDotView.layer.cornerRadius = onlineStatusDotView.frame.height/2onlineStatusView.addSubview(onlineStatusDotView)}}

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

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

我也在这里上传了一个


这是您的代码的修改版本.我添加了约束来设置小圆圈的大小,并将设置 cornerRadius 的代码移动到 layoutSubviews():

类 ProfilePictureView: UIView {var bigCircle:UIView!var borderCircle: UIView!var littleCircle: UIView!覆盖初始化(帧:CGRect){超级初始化(帧:帧)初始化()}需要初始化?(编码器 aDecoder:NSCoder){super.init(编码器:aDecoder)初始化()}私人函数初始化(){bigCircle = UIView(框架:边界)bigCircle.backgroundColor = .redaddSubview(bigCircle)边界圆 = UIView()borderCircle.translatesAutoresizingMaskIntoConstraints = falseborderCircle.backgroundColor = .whitebigCircle.addSubview(borderCircle)borderCircle.widthAnchor.constraint(equalTo: bigCircle.widthAnchor, multiplier: 1/3).isActive = trueborderCircle.heightAnchor.constraint(equalTo: bigCircle.heightAnchor, multiplier: 1/3).isActive = truelittleCircle = UIView()littleCircle.translatesAutoresizingMaskIntoConstraints = falselittleCircle.backgroundColor = .greenborderCircle.addSubview(littleCircle)littleCircle.widthAnchor.constraint(equalTo: borderCircle.widthAnchor, multiplier: 1/1.3).isActive = truelittleCircle.heightAnchor.constraint(equalTo: borderCircle.heightAnchor, multiplier: 1/1.3).isActive = truelittleCircle.centerXAnchor.constraint(equalTo:borderCircle.centerXAnchor).isActive = truelittleCircle.centerYAnchor.constraint(equalTo:borderCircle.centerYAnchor).isActive = true让(hMult,vMult)=计算乘数(角度:45)//使用乘数在右下角定位边框圆NSLayoutConstraint(项目:borderCircle!,属性:.centerX,relatedBy:.equal,toItem:bigCircle!,属性:.trailing,乘数:hMult,常量:0).isActive = trueNSLayoutConstraint(项目:borderCircle!,属性:.centerY,relatedBy:.equal,toItem:bigCircle!,属性:.bottom,乘数:vMult,常量:0).isActive = true}覆盖 func layoutSubviews() {super.layoutSubviews()bigCircle.layer.cornerRadius = bigCircle.frame.height/2borderCircle.layoutIfNeeded()borderCircle.layer.cornerRadius = borderCircle.frame.height/2littleCircle.layoutIfNeeded()littleCircle.layer.cornerRadius = littleCircle.frame.height/2}私有函数计算乘数(角度:CGFloat)->(CGFloat, CGFloat) {让弧度 = 角度 * .pi/180让 h = (1.0 + cos(弧度))/2让 v = (1.0 - sin(弧度))/2返回 (h, v)}}



computeMultipliers(angle:)

背后的数学解释

computeMultipliers(angle:) 的思想是应该为水平约束计算一个乘数,为垂直约束计算一个乘数.这些值是一个比例,范围从 01 其中 0 是圆的 top垂直约束和 0 是水平约束的圆的 left 边缘.同样,1 是圆的 bottom 用于垂直约束,1 是圆的 边缘用于水平约束.

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

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

等式:

1 + cos(角度)

会根据角度从 02 变化.由于我们正在寻找从 01 的值,我们将其除以 2:

//根据角度计算水平乘数让 h = (1.0 + cos(弧度))/2

在垂直方向上,我们首先注意到坐标系是从数学意义上的翻转.在 iOS 中,y 向下增长,但在数学中,y 向上增长.考虑到这一点,垂直计算使用减号 - 而不是 +:

1 - sin(角度)

同样,由于 sin-11 变化,所以这个计算将从 02,所以我们除以2:

//根据角度计算垂直乘数让 h = (1.0 - sin(弧度))/2

这给了我们想要的结果.当角度为90度(或.pi/2弧度)时,sin1,所以垂直乘数将为 0.当角度为 270 度(或 3*.pi/2 弧度)时,sin-1 并且垂直乘数将为 1.

为什么要使用弧度?一旦您了解弧度是什么,它们就很直观.它们只是沿单位圆圆周的弧长.圆的周长公式是circumference = 2 * .pi * radius,所以对于单位圆,周长是2 * .pi.所以 360 度数是 2 * .pi 弧度.

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.
  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.

Note: The two multipliers 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). In the sample code below, I have provided a func called computeMultipliers(angle:) which computes the multipliers for any angle in degrees. Avoid angles exactly 90 and 180 because that can create multipliers of 0 which Auto Layout does not like.


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():

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:)

The idea of computeMultipliers(angle:) is that is should compute a multiplier for the horizontal constraint and a multiplier for the vertical constraint. 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. 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.

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. 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. Note the the width and height of the unit circle are each 2.

sin(angle) and cos(angle) each vary from -1 to 1.

The equation:

1 + cos(angle)

will vary from 0 to 2 depending on the angle. Since we're looking for a value from 0 to 1, we divide this by 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. 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:

// 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. When the angle is 270 degrees (or 3*.pi/2 radians), sin is -1 and the vertical multiplier will be 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. So 360 degrees is 2 * .pi radians.

这篇关于将子视图定位在圆形视图的边缘的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆