在 Swift 中构建一个圆形头像堆:如何将最后一张照片隐藏在第一张照片下面?

Posted

技术标签:

【中文标题】在 Swift 中构建一个圆形头像堆:如何将最后一张照片隐藏在第一张照片下面?【英文标题】:Building a circular facepile of profile pictures in Swift: how to have the last photo tucked under the first? 【发布时间】:2020-08-21 17:36:40 【问题描述】:

我正在尝试构建一个UIView,其中有几个UIImageViews 以圆形重叠的方式排列(见下图)。假设我们有 N 个图像。画出第一个 N - 1 很容易,只需使用 sin/cos 函数将 UIImageViews 的中心围绕一个圆排列。 问题在于似乎有两个 z-index 值的最后一张图片!我知道这是可能的,因为 kik messenger 有类似的群组头像。

到目前为止,我提出的最佳想法是拍摄最后一张图像,将其分成“上半部分”和“下半部分”之类的内容,并为每个图像分配不同的 z 值。当图像位于最左侧时,这似乎是可行的,但如果图像位于最顶部会发生什么?在这种情况下,我需要左右拆分而不是上下拆分。

由于这个问题,它可能不是顶部、左侧或右侧,而更像是从整个面罩中心到UIImageView 中心的某个假想轴上的分割。 我该怎么做?!

以下代码将 UIImageView 布局成圆形

您需要导入 SDWebImage 并提供一些图片 URL 来运行下面的代码。

import Foundation
import UIKit
import SDWebImage

class EventDetailsFacepileView: UIView 
    static let dimension: CGFloat = 66.0
    static let radius: CGFloat = dimension / 1.68
    
    private var profilePicViews: [UIImageView] = []
    var profilePicURLs: [URL] = [] 
        didSet 
            updateView()
        
    
    
    func updateView() 
        self.profilePicViews = profilePicURLs.map( (profilePic) -> UIImageView in
            let imageView = UIImageView()
            imageView.sd_setImage(with: profilePic)
            imageView.roundImage(imageDimension: EventDetailsFacepileView.dimension, showsBorder: true)
            imageView.sd_imageTransition = .fade
            return imageView
        )
        self.profilePicViews.forEach  (imageView) in
            self.addSubview(imageView)
        
        self.setNeedsLayout()
        self.layer.borderColor = UIColor.green.cgColor
        self.layer.borderWidth = 2
    
    
    override func layoutSubviews() 
        super.layoutSubviews()
        
        let xOffset: CGFloat = 0
        let yOffset: CGFloat = 0
        
        let center = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.size.height / 2)
        let radius: CGFloat =  EventDetailsFacepileView.radius
        let angleStep: CGFloat = 2 * CGFloat(Double.pi) / CGFloat(profilePicViews.count)
        var count = 0
        for profilePicView in profilePicViews 
            let xPos = center.x + CGFloat(cosf(Float(angleStep) * Float(count))) * (radius - xOffset)
            let yPos = center.y + CGFloat(sinf(Float(angleStep) * Float(count))) * (radius - yOffset)
            profilePicView.frame = CGRect(origin: CGPoint(x: xPos, y: yPos),
                                          size: CGSize(width: EventDetailsFacepileView.dimension, height: EventDetailsFacepileView.dimension))
            count += 1
        
    
    
    override func sizeThatFits(_ size: CGSize) -> CGSize 
        let requiredSize = EventDetailsFacepileView.dimension + EventDetailsFacepileView.radius
        return CGSize(width: requiredSize,
                      height: requiredSize)
    


【问题讨论】:

【参考方案1】:

我认为您尝试分割图像以超过/低于 z-indexes 不会取得很大成功。

一种方法是使用掩码使图像视图显示重叠。

一般的想法是:

子类UIImageView 在 layoutSubviews() 中 将cornerRadius应用于图层以使图像变圆 从“重叠视图”中获取一个矩形 将该矩形转换为局部坐标 将该矩形扩展为所需的“轮廓”宽度 从该矩形获取椭圆路径 将它与来自自我的路径结合起来 将其应用为遮罩层

这是一个例子......

我不完全确定您的尺寸计算在做什么...尝试按原样使用您的 EventDetailsFacepileView 在视图的右下角给了我小图像?

所以,我通过几种方式修改了您的EventDetailsFacepileView

使用名为“pro1”到“pro5”的本地图像(您应该可以用您的SDWebImage 替换) 使用自动布局约束而不是显式框架 使用MyOverlapImageView 类处理屏蔽

代码 - 没有 @IBOutlet 连接,因此只需将空白视图控制器设置为 OverlapTestViewController

class OverlapTestViewController: UIViewController 
    
    let facePileView = MyFacePileView()
    
    override func viewDidLoad() 
        super.viewDidLoad()
        
        facePileView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(facePileView)
        
        facePileView.dimension = 120
        let sz = facePileView.sizeThatFits(.zero)
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            facePileView.widthAnchor.constraint(equalToConstant: sz.width),
            facePileView.heightAnchor.constraint(equalTo: facePileView.widthAnchor),
            facePileView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            facePileView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
        ])
        
        facePileView.profilePicNames = [
            "pro1", "pro2", "pro3", "pro4", "pro5"
        ]
        
    
    


class MyFacePileView: UIView 
    var dimension: CGFloat = 66.0
    lazy var radius: CGFloat = dimension / 1.68
    
    private var profilePicViews: [MyOverlapImageView] = []
    
    var profilePicNames: [String] = [] 
        didSet 
            updateView()
        
    
    
    func updateView() 
        self.profilePicViews = profilePicNames.map( (profilePic) -> MyOverlapImageView in
            let imageView = MyOverlapImageView()
            if let img = UIImage(named: profilePic) 
                imageView.image = img
            
            return imageView
        )
        
        // add MyOverlapImageViews to self
        //  and set width / height constraints
        self.profilePicViews.forEach  (imageView) in
            self.addSubview(imageView)
            imageView.translatesAutoresizingMaskIntoConstraints = false
            imageView.widthAnchor.constraint(equalToConstant: dimension).isActive = true
            imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
        
        
        // start at "12 o'clock"
        var curAngle: CGFloat = .pi * 1.5
        // angle increment
        let incAngle: CGFloat = ( 360.0 / CGFloat(self.profilePicViews.count) ) * .pi / 180.0

        // calculate position for each image view
        //  set center constraints
        self.profilePicViews.forEach  imgView in
            let xPos = cos(curAngle) * radius
            let yPos = sin(curAngle) * radius
            imgView.centerXAnchor.constraint(equalTo: centerXAnchor, constant: xPos).isActive = true
            imgView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: yPos).isActive = true
            curAngle += incAngle
        
        
        // set "overlapView" property for each image view
        let n = self.profilePicViews.count
        for i in (1..<n).reversed() 
            self.profilePicViews[i].overlapView = self.profilePicViews[i-1]
        
        self.profilePicViews[0].overlapView = self.profilePicViews[n - 1]

        self.layer.borderColor = UIColor.green.cgColor
        self.layer.borderWidth = 2
        
    
    
    override func sizeThatFits(_ size: CGSize) -> CGSize 
        let requiredSize = dimension * 2.0 + radius / 2.0
        return CGSize(width: requiredSize,
                      height: requiredSize)
    



class MyOverlapImageView: UIImageView 
    
    // reference to the view that is overlapping me
    weak var overlapView: MyOverlapImageView?
    
    // width of "outline"
    var outlineWidth: CGFloat = 6
    
    override func layoutSubviews() 
        super.layoutSubviews()
        
        // make image round
        layer.cornerRadius = bounds.size.width * 0.5
        layer.masksToBounds = true

        let mask = CAShapeLayer()
        
        if let v = overlapView 
            // get bounds from overlapView
            //  converted to self
            //  inset by outlineWidth (negative numbers will make it grow)
            let maskRect = v.convert(v.bounds, to: self).insetBy(dx: -outlineWidth, dy: -outlineWidth)
            // oval path from mask rect
            let path = UIBezierPath(ovalIn: maskRect)
            // path from self bounds
            let clipPath = UIBezierPath(rect: bounds)
            // append paths
            clipPath.append(path)
            mask.path = clipPath.cgPath
            mask.fillRule = .evenOdd
            // apply mask
            layer.mask = mask
        
    
    

结果:

(我通过在 Google 上搜索 sample profile pictures 抓取了随机图片)

【讨论】:

感谢您的回答。在小于 3 的情况下,似乎掩码开始与未定义的结果发生冲突。例如,使用 3 张图像并将半径设置为 22(尺寸设置为 33)。关于解决这个问题有什么建议吗? 当然,对于边缘情况,例如只有一张图像,或者只有两张图像,或者三张具有异常半径/尺寸/轮廓宽度/等的图像,我可以看到潜在的问题。不过应该很容易解释。我尝试了您提出的 3 张图像并将半径设置为 22(尺寸设置为 33) 的建议,它看起来不错(虽然非常小,而且由于重叠区域非常小而看起来很奇怪)。

以上是关于在 Swift 中构建一个圆形头像堆:如何将最后一张照片隐藏在第一张照片下面?的主要内容,如果未能解决你的问题,请参考以下文章

Flutter 网络图像不适合圆形头像

Flutter圆形头像不支持来自存储的图像

如何在 Swift 中使按钮具有圆形边框?

基于PhotoView的头像/圆形裁剪控件

对齐两个圆形头像的最佳方法是啥?

如何在 Swift 中创建一个圆形按钮? [关闭]