【问题标题】:Not all pixels clipped when using UIView.layer.clipsToBounds = true in conjunction with CABasicAnimation将 UIView.layer.clipsToBounds = true 与 CABasicAnimation 结合使用时,并非所有像素都被裁剪
【发布时间】:2023-03-25 19:55:02
【问题描述】:

我在做什么: 我正在创建一个音乐应用程序。在这个音乐应用程序中,我有一个与 Apple Music 和 Spotify 链接的音乐播放器。音乐播放器在UIImageView 上显示当前歌曲的专辑封面。每当播放歌曲时,UIImageView 就会旋转(就像唱机上的唱片一样)。

我的问题是: UIImageView 的专辑封面默认为方形。我将UIImageViewlayer 的角四舍五入并将UIImageView.clipsToBounds 设置为等于true 以使其显示为一个圆圈。 每当UIImageView 旋转时,UIImageViewlayer 之外的一些像素(对图像进行四舍五入后被切断的部分)就会渗出。

这是错误的样子: https://www.youtube.com/watch?v=OJxX5PQc7Jo&feature=youtu.be

我的代码: UIImageView 通过将其layercornerRadius 设置为等于UIImageView.frame.height / 2 并设置UIImageView.clipsToBounds = true 来进行舍入:

class MyViewController: UIViewController {

    @IBOutlet var albumArtImageView: UIImageView()

    override func viewDidLoad() {
        super.viewDidLoad()
        albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
        albumArtImageView.clipsToBounds = true

        //I've also tried the following code, and am getting the same behavior:
        /*
        albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
        albumArtImageView.layer.masksToBounds = true
        albumArtImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
        */
    }
}

只要按下按钮,UIImageView 就会开始旋转。我使用了以下extension 来使UIView 旋转:

extension UIView {
    func rotate(duration: Double = 1, startPoint: CGFloat) {
        if layer.animation(forKey: UIView.kRotationAnimationKey) == nil {
            let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")

            rotationAnimation.fromValue = startPoint
            rotationAnimation.toValue = (CGFloat.pi * 2.0) + startPoint
            rotationAnimation.duration = duration
            rotationAnimation.repeatCount = Float.infinity

            layer.add(rotationAnimation, forKey: UIView.kRotationAnimationKey)
        }
    }
}

我还有以下扩展来结束轮换:

extension UIView {

    ...

    func stopRotating(beginTime: Double!, startingAngle: CGFloat) -> CGFloat? {
        if layer.animation(forKey: UIView.kRotationAnimationKey) != nil {
            let animation = layer.animation(forKey: UIView.kRotationAnimationKey)!
            let elapsedTime = CACurrentMediaTime() - beginTime
            let angle = elapsedTime.truncatingRemainder(dividingBy: animation.duration)/animation.duration

            layer.transform = CATransform3DMakeRotation((CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle, 0.0, 0.0, 1.0)
            layer.removeAnimation(forKey: UIView.kRotationAnimationKey)
            return (CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle
        } else {
            return nil
        }
    }
}

这就是这些扩展函数在我的视图控制器上下文中的使用方式:

class MyViewController: UIViewController {

    var songBeginTime: Double!
    var currentRecordAngle: CGFloat = 0.0
    var isPlaying = false

    ...

    @IBAction func playButtonPressed(_ sender: Any) {
        if isPlaying {
            if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
                currentRecordAngle = angle
            }
            songBeginTime = nil
        } else {
            songBeginTime = CACurrentMediaTime()
            albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
        }
    }
}

所以,MyViewController 看起来像这样:

class MyViewController: UIViewController {

    @IBOutlet var albumArtImageView: UIImageView()

    var songBeginTime: Double!
    var currentRecordAngle: CGFloat = 0.0
    var isPlaying = false

    override func viewDidLoad() {
        super.viewDidLoad()
        albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
        albumArtImageView.clipsToBounds = true
    }

    @IBAction func playButtonPressed(_ sender: Any) {
        if isPlaying {
            if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
                currentRecordAngle = angle
            }
            songBeginTime = nil
        } else {
            songBeginTime = CACurrentMediaTime()
            albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
        }
    }
}

【问题讨论】:

    标签: ios swift uiview calayer cabasicanimation


    【解决方案1】:

    我将您的代码复制到项目中,我可以重现此问题。但是,如果您将动画添加到另一个 CALayer,这似乎可以解决问题。

    extension UIView {
    
    static var kRotationAnimationKey: String {
        return "kRotationAnimationKey"
    }
    
    func makeAnimationLayer() -> CALayer {
    
        let results: [CALayer]? = layer.sublayers?.filter({ $0.name ?? "" == "animationLayer" })
    
        let animLayer: CALayer
        if let sublayers = results, sublayers.count > 0 {
            animLayer = sublayers[0]
        }
        else {
            animLayer = CAShapeLayer()
            animLayer.name = "animationLayer"
            animLayer.frame = self.bounds
            animLayer.contents = UIImage(named: "imageNam")?.cgImage
            layer.addSublayer(animLayer)
        }
    
        return animLayer
    }
    
    func rotate(duration: Double = 1, startPoint: CGFloat) {
        if layer.animation(forKey: UIView.kRotationAnimationKey) == nil {
    
            let animLayer = makeAnimationLayer()
    
            let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
            rotationAnimation.fromValue = startPoint
            rotationAnimation.toValue = (CGFloat.pi * 2.0) + startPoint
            rotationAnimation.duration = duration
            rotationAnimation.repeatCount = Float.infinity
            animLayer.add(rotationAnimation, forKey: UIView.kRotationAnimationKey)
        }
    }
    
    func stopRotating(beginTime: Double!, startingAngle: CGFloat) -> CGFloat? {
    
        let animLayer = makeAnimationLayer()
    
        if animLayer.animation(forKey: UIView.kRotationAnimationKey) != nil {
            let animation = animLayer.animation(forKey: UIView.kRotationAnimationKey)!
            let elapsedTime = CACurrentMediaTime() - beginTime
            let angle = elapsedTime.truncatingRemainder(dividingBy: animation.duration)/animation.duration
            animLayer.transform = CATransform3DMakeRotation(CGFloat(angle) * (2 * CGFloat.pi) + startingAngle, 0.0, 0.0, 1.0)
            animLayer.removeAnimation(forKey: UIView.kRotationAnimationKey)
            return (CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle
        }
        else {
            return nil
        }
    }
    

    【讨论】:

    • 这非常令人鼓舞。让我尝试实现它并回复您!
    • 完美运行。所以让我等了两个小时才给你赏金,所以你今天晚些时候会得到它:)
    【解决方案2】:

    剪辑到边界不包括圆角。尝试改用角落遮罩。

    class MyViewController: UIViewController {
    
        @IBOutlet var albumArtImageView: UIImageView()
    
        var songBeginTime: Double!
        var currentRecordAngle: CGFloat = 0.0
        var isPlaying = false
    
        override func viewDidLoad() {
            super.viewDidLoad()
            albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
            albumArtImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
        }
    
        @IBAction func playButtonPressed(_ sender: Any) {
            if isPlaying {
                if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
                    currentRecordAngle = angle
                }
                songBeginTime = nil
            } else {
                songBeginTime = CACurrentMediaTime()
                albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
            }
        }
    }
    

    【讨论】:

    • 我通过albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2albumArtImageView.layer.masksToBounds = truealbumArtImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,.layerMinXMaxYCorner, .layerMaxXMaxYCorner] 实现了这一点,不幸的是,我得到了同样的行为。