Swift scale-transform 动画不小心平移了 UIView

Swift scale-transform animation accidentally translates UIView

真的很简单的问题。我正在使用 UIView 动画让我的按钮根据用户输入缩小和增大:

private func animateSelect() {
    guard !isAnimating else {
        return
    }
    
    alpha = 0.8
    UIView.animate(withDuration: 0.1) {
        self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
    }
    isAnimating = true
}

private func animateDeselect() {
    guard isAnimating else {
        return
    }
    
    alpha = 1.0
    UIView.animate(withDuration: 0.2) {
        self.transform = .identity
    }
    isAnimating = false
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)

    animateSelect()
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesEnded(touches, with: event)

    animateDeselect()
}

而且我认为我使用 isAnimating 变量安排了一切以避免布局或动画冲突 - 守卫 layoutSubviews:

override func layoutSubviews() {
    guard !isAnimating else {
        return
    }
    
...

但是按钮会随动画一起移动。这真的很烦人,破坏了平滑度,我有点困惑,因为从动画内部的变换中减去平移并没有解决它。有人有解决此问题的提示吗?

编辑:

仔细一看,好像根本不需要isAnimating。像这样应该没问题:

class MyView: UIView {
    private func animateSelect() {
        alpha = 0.8
        UIView.animate(withDuration: 0.1) {
            self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
        }
    }
    
    private func animateDeselect() {
        alpha = 1.0
        UIView.animate(withDuration: 0.2) {
            self.transform = .identity
        }
    }
}

除了,问题最有可能是self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)。当转换不是 .identity 时,frame 会变得混乱。因此,例如,当您初始化 MyView:

let myView = MyView()
myView.frame = CGRect(x: 50, y: 60, width: 100, height: 200)

...这里,myView.frame可能不准确。

MyView 中,您应该创建一个新的子视图 contentView。这将包含您要缩放的内容。像这样:

/// usage
let myView = MyView()
myView.frame = CGRect(x: 50, y: 200, width: 50, height: 100)
myView.contentView.backgroundColor = .red
view.addSubview(myView)
/// code
class MyView: UIView {
    var contentView: UIView!
    
    private func animateSelect() {
        alpha = 0.8
        UIView.animate(withDuration: 0.1) {
            self.contentView?.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
        }
    }
    
    private func animateDeselect() {
        alpha = 1.0
        UIView.animate(withDuration: 0.2) {
            self.contentView?.transform = .identity
        }
    }
    
    // MARK: Init methods
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    // MARK: Set up the contentView
    private func commonInit() {
        let contentView = UIView()
        addSubview(contentView)
        contentView.frame = self.bounds
        contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        self.contentView = contentView
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        animateSelect()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        animateDeselect()
    }
}

结果:


旧答案:

动画完成后,使用完成处理程序重置 isAnimating。此外,无需检查 guard isAnimating else { return }...UIView 动画通常会自动为您解决。

private func animateSelect() {
    alpha = 0.8
    isAnimating = true /// put it in front so that it's easier to read
    UIView.animate(withDuration: 0.1) {
        self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
    } completion: { _ in /// set the completion handler
        self.isAnimating = false
    }
}

private func animateDeselect() {
    alpha = 1.0
    isAnimating = true
    UIView.animate(withDuration: 0.2) {
        self.transform = .identity
    } completion: { _ in
        self.isAnimating = false
    }
}