Swift: UIView.animate 工作意外

Swift: UIView.animate works unexpectedly

我正在尝试为我的应用制作一个 "record button",它是一个带有手势识别器的 UIView。现在我正在实现这样一个功能,即当我单击视图时,我想缩小内圈(红色圆圈),但它最终会发生意想不到的转变。我使用 UIView.animate 函数来执行此操作,下面是我的相关代码:

import UIKit

@IBDesignable
class RecordView: UIView {

    @IBInspectable
    var borderColor: UIColor = UIColor.clear {
        didSet {
            self.layer.borderColor = borderColor.cgColor
        }
    }

    @IBInspectable
    var borderWidth: CGFloat = 20 {
        didSet {
            layer.borderWidth = borderWidth
        }
    }

    @IBInspectable
    var cornerRadius: CGFloat = 100 {
        didSet {
            layer.cornerRadius = cornerRadius
        }
    }

    private var fillView = UIView()

    private func setupFillView() {
        let radius = (self.cornerRadius - self.borderWidth) * 0.95
        fillView.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: radius * 2, height: radius * 2))
        fillView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
        fillView.layer.cornerRadius = radius
        fillView.backgroundColor = UIColor.red
        self.addSubview(fillView)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        setupFillView()
    }

    func didClick() {
        UIView.animate(withDuration: 1.0, animations: {
            self.fillView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
        }) { (true) in
            print()
        }
    }

}

在我的 ViewController 中,我有:

@IBAction func recoreViewTapped(_ sender: UITapGestureRecognizer) {
        recordView.didClick()
    }

然而它最终产生了这样的效果: https://media.giphy.com/media/3ohjUQrWt7vfIxzBrG/giphy.gif。我是初学者,真的不知道我的代码有什么问题?

问题是您正在应用转换,这会再次触发 layoutSubviews,因此 fillViewframe 的重置将应用于转换后的视图.

至少有两个选项:

  1. 您可以变换整个 RecordButton 视图:

    @IBDesignable
    class RecordView: UIView {
    
        @IBInspectable
        var borderColor: UIColor = .clear { didSet { layer.borderColor = borderColor.cgColor } }
    
        @IBInspectable
        var borderWidth: CGFloat = 20 { didSet { layer.borderWidth = borderWidth; setNeedsLayout() } }
    
        private var fillView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            configure()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            configure()
        }
    
        private func configure() {
            fillView.backgroundColor = .red
            self.addSubview(fillView)
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            let radius = min(bounds.width, bounds.height) / 2  - borderWidth
            fillView.frame = CGRect(origin: CGPoint(x: bounds.midX - radius, y: bounds.midY - radius),
                                    size: CGSize(width: radius * 2, height: radius * 2))
            fillView.layer.cornerRadius = radius
        }
    
        func didClick() {
            UIView.animate(withDuration: 1.0, animations: {
                self.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
            }, completion: { _ in
                print("completion", #function)
            })
        }
    
    }
    
  2. 或者您可以将 fillView 放入容器中,然后 fillView 的变换不会触发 [=20] 的 layoutSubviews =]:

    @IBDesignable
    class RecordView: UIView {
    
        @IBInspectable
        var borderColor: UIColor = .clear { didSet { layer.borderColor = borderColor.cgColor } }
    
        @IBInspectable
        var borderWidth: CGFloat = 20 { didSet { layer.borderWidth = borderWidth; setNeedsLayout() } }
    
        private var fillView = UIView()
        private var containerView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            configure()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            configure()
        }
    
        private func configure() {
            fillView.backgroundColor = .red
            self.addSubview(containerView)
            containerView.addSubview(fillView)
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            containerView.frame = bounds
    
            let radius = min(bounds.width, bounds.height) / 2  - borderWidth
            fillView.frame = CGRect(origin: CGPoint(x: bounds.midX - radius, y: bounds.midY - radius),
                                    size: CGSize(width: radius * 2, height: radius * 2))
            fillView.layer.cornerRadius = radius
        }
    
        func didClick() {
            UIView.animate(withDuration: 1.0, animations: {
                self.fillView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
            }, completion: { _ in
                print("completion", #function)
            })
        }
    
    }
    

顺便说一下其他一些小事:

  • 正如你我在别处讨论的那样,初始配置(例如添加子视图)应该从 init 调用。但是任何 frame/bounds 偶然的东西都应该从 layoutSubviews.

  • 调用
  • 完全不相关,但是传给UIView.animate(withDuration:animations:completion:)completion的参数是一个布尔值,表示动画是否finished。如果您不打算使用该布尔值,则应使用 _,而不是 (true)

  • 从长远来看,我建议将 "touches"/手势相关的内容移动到 RecordButton 中。我可以很容易地想象你变得更加狂热:例如一个动画 "touch down"(缩小按钮以呈现 "depress" 氛围)和另一个动画 "lift up"(例如,取消缩小按钮并将圆形 "record" 按钮转换为方形 "stop" 按钮)。然后,您可以让按钮在发生重大事件时通知视图控制器(例如记录或停止识别),但会降低视图控制器本身的复杂性。

    protocol RecordViewDelegate: class {
        func didTapRecord()
        func didTapStop()
    }
    
    @IBDesignable
    class RecordView: UIView {
    
        @IBInspectable
        var borderColor: UIColor = .clear { didSet { layer.borderColor = borderColor.cgColor } }
    
        @IBInspectable
        var borderWidth: CGFloat = 20 { didSet { layer.borderWidth = borderWidth; setNeedsLayout() } }
    
        weak var delegate: RecordViewDelegate?
    
        var isRecordButton = true
    
        private var fillView = UIView()
        private var containerView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            configure()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            configure()
        }
    
        private func configure() {
            fillView.backgroundColor = .red
            self.addSubview(containerView)
            containerView.addSubview(fillView)
        }
    
        private var radius: CGFloat { return min(bounds.width, bounds.height) / 2  - borderWidth }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            containerView.frame = bounds
    
            fillView.frame = CGRect(origin: CGPoint(x: bounds.midX - radius, y: bounds.midY - radius),
                                    size: CGSize(width: radius * 2, height: radius * 2))
            fillView.layer.cornerRadius = isRecordButton ? radius : 0
        }
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            UIView.animate(withDuration: 0.25) {
                self.fillView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
                self.fillView.backgroundColor = UIColor(red: 0.75, green: 0, blue: 0, alpha: 1)
            }
        }
    
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            if let touch = touches.first, containerView.bounds.contains(touch.location(in: containerView)) {
                if isRecordButton {
                    delegate?.didTapRecord()
                } else {
                    delegate?.didTapStop()
                }
                isRecordButton = !isRecordButton
            }
    
            UIView.animate(withDuration: 0.25) {
                self.fillView.transform = .identity
                self.fillView.backgroundColor = UIColor.red
                self.fillView.layer.cornerRadius = self.isRecordButton ? self.radius : 0
            }
    
        }
    }
    

    产生: