swift - 在 Xcode Playground 中制作动画时,位置元素与 super View 成比例

标签 swift xcode autolayout uiviewanimation swift-playground

我试图将 View 元素放置在 UIImageView 中,然后对 UIImageView 的宽度进行动画处理以填充 super View 。我正在使用自动布局约束来定位 View 并为其设置动画。在动画开始时,圆正好位于 superView 的 centerX 的一半处:

self.circleXConstraint = NSLayoutConstraint(item: self.circle, attribute: .centerX, relatedBy: .equal, toItem: self.bodyView, attribute: .centerX, multiplier: 0.5, Constant: 0)

但是当动画开始时,圆圈会偏离正确的位置(两条线交叉的位置)。

我花了几天时间试图找出为什么自动布局不能保持圆形 View 正确定位,但无法找出原因。

Body Animation

Xcode Playground 的完整代码可以在这里下载: https://github.com/imyrvold/Animate-body

Xcode Playground swift 代码:

import UIKit
import PlaygroundSupport

class ViewController : UIViewController {

lazy var bodyView: UIImageView = {
    let bodyView = UIImageView()
    bodyView.contentMode = .scaleAspectFill
    bodyView.image = self.image
    bodyView.backgroundColor = .yellow
    return bodyView
}()
lazy var image: UIImage? = {
    UIImage(named: "body")
}()
lazy var button: UIButton = {
    let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
    button.setTitle("Start", for: .normal)
    button.addTarget(self, action: #selector(headZoom), for: .touchDown)
    button.titleLabel?.textColor = .black
    button.backgroundColor = .lightGray
    return button
}()
lazy var circle: BodyAreaView = {
    let circle = BodyAreaView(frame: CGRect(x: 0, y: 0, width: 48, height: 48))
    circle.backgroundColor = .clear

    return circle
}()
// bodyView constraints
var topAnchorConstraint: NSLayoutConstraint!
var centerXAnchorConstraint: NSLayoutConstraint!
var aspectConstraint: NSLayoutConstraint!
var superWidthConstraint: NSLayoutConstraint!
var iphoneHeightConstraint: NSLayoutConstraint!
var iphoneWidthConstraint: NSLayoutConstraint!

// circle constraints
var circleHeightConstraint: NSLayoutConstraint!
var circleXConstraint: NSLayoutConstraint!
var circleYConstraint: NSLayoutConstraint!
var circleAspectConstraint: NSLayoutConstraint!

override func loadView() {
    let view = UIView()
    view.backgroundColor = .white
    self.view = view

    view.addSubview(self.bodyView)
    self.bodyView.addSubview(self.circle)
    view.addSubview(self.button)
    self.view.translatesAutoresizingMaskIntoConstraints = false
}

override func viewDidLoad() {
    super.viewDidLoad()
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    self.setBodyViewConstraints()
    self.setFibroConstraints()

    self.bodyView.translatesAutoresizingMaskIntoConstraints = false
    self.circle.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        self.topAnchorConstraint,
        self.centerXAnchorConstraint,
        self.aspectConstraint,
        self.iphoneWidthConstraint
        ])

    NSLayoutConstraint.activate([
        self.circleHeightConstraint,
        self.circleXConstraint,
        self.circleYConstraint,
        self.circleAspectConstraint
        ])
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
}

@objc func headZoom() {
    self.superWidthConstraint.isActive = true
    print("body view before: \(self.bodyView.bounds)")
    print("circle origin before: \(self.circle.frame.origin)")
    UIView.animate(withDuration: 3, animations: {
        self.view.layoutIfNeeded()
    }) { _ in
        print("body view after: \(self.bodyView.bounds)")
        print("circle frame after: \(self.circle.frame)")
    }
}

func setBodyViewConstraints() {
    self.topAnchorConstraint = self.bodyView.topAnchor.constraint(equalTo: self.view.topAnchor)
    self.centerXAnchorConstraint = self.bodyView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
    self.aspectConstraint = self.bodyView.heightAnchor.constraint(equalTo: self.bodyView.widthAnchor, multiplier: 1388/408.0)
    self.superWidthConstraint = self.bodyView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1)
    self.iphoneHeightConstraint = self.bodyView.heightAnchor.constraint(equalToConstant: 633)
    self.iphoneWidthConstraint = self.bodyView.widthAnchor.constraint(equalToConstant: 186)
}

func setFibroConstraints() {
    self.circleHeightConstraint = self.circle.heightAnchor.constraint(equalTo: self.bodyView.heightAnchor, multiplier: 0.05)
    self.circleXConstraint = NSLayoutConstraint(item: self.circle, attribute: .centerX, relatedBy: .equal, toItem: self.bodyView, attribute: .centerX, multiplier: 0.5, constant: 0)
    self.circleYConstraint = NSLayoutConstraint(item: self.circle, attribute: .centerY, relatedBy: .equal, toItem: self.bodyView, attribute: .centerY, multiplier: 0.5, constant: 0)
    self.circleAspectConstraint = self.circle.heightAnchor.constraint(equalTo: self.circle.widthAnchor, multiplier: 1)
}

}

let viewController = ViewController()
viewController.preferredContentSize = CGSize(width: 375, height: 812)

PlaygroundPage.current.liveView = viewController

我已经更新了 Xcode Playground,现在它将按照建议以 .cyan 颜色显示圆形 View 的背景。

听起来,更新 UIView 子类的绘图代码是一项简单的任务,但在堆栈溢出上搜索解决方案几个小时后,没有一篇文章有​​任何帮助。看起来圆圈拒绝重新绘制自身,即使 View 本身的边界发生变化。

我简化了 UIView 子类代码,以便更容易理解绘图的工作原理。我很乐意为此获得任何帮助。我看过的许多帖子都建议重写layoutSubviews,将子层框架设置为 View 层边界,但这在这种情况下不起作用。一定是我忽略了一些东西。

这是 BodyAreaView 的代码,简化为仅绘制圆环:

import UIKit

public class BodyAreaView: UIView {
var ringColor: UIColor = .black

lazy var ringLayer: CAShapeLayer = {
    let ringLayer = CAShapeLayer()
    let ringPath = UIBezierPath(ovalIn: self.bounds.insetBy(dx: 4, dy: 4))
    ringLayer.fillColor = UIColor.clear.cgColor
    ringLayer.strokeColor = UIColor.black.cgColor
    ringLayer.path = ringPath.cgPath
    ringLayer.name = "ring"
    ringLayer.needsDisplayOnBoundsChange = true

    return ringLayer
}()

override public func draw(_ rect: CGRect) {
    self.layer.addSublayer(self.ringLayer)
}

override public func layoutSubviews() {
    guard let sublayers = self.layer.sublayers else { return }
    for layer in sublayers {
        layer.frame = layer.bounds
    }
}

}

最佳答案

约束冲突

首先,您没有考虑到布局可能会出现多次。因此,在您的override func viewDidLayoutSubviews()中,您会一遍又一遍地重新激活相同的约束。这意味着当你说

    self.superWidthConstraint.isActive = true

您遇到了约束冲突,因为您的iphoneWidthConstraint仍然处于事件状态:您要求两者旧的宽度约束和新的宽度约束。事实上,您看到任何宽度变化纯粹是运气。

解决办法如下。首先,编写带有标志的 layout 代码,以便仅初始化所有内容一次:

var didInitialLayout = false
override func viewDidLayoutSubviews() {
    if didInitialLayout { return } // *
    didInitialLayout = true //
    // ...

其次,在激活新的宽度限制之前停用旧的宽度限制:

@objc func headZoom() {
    self.iphoneWidthConstraint.isActive = false // *
    self.superWidthConstraint.isActive = true
    // ...

好的,现在冲突已经消失了。

绘制圆圈

现在我们可以专注于绘图代码了!该代码(您的 BodyAreaView)有很多问题,我不知道从哪里开始。但是,总结一下:

override public func draw(_ rect: CGRect) {
    self.layer.addSublayer(self.ringLayer)
}

这里我们有一个典型的错误:绘制,而您不绘制。相反,您会做一些完全无关的事情:添加一个层。这总是很危险的,因为draw可以被调用很多次;在最坏的情况下,您最终可能会一遍又一遍地添加此层。 (在这里,这可能不会发生,但这只是运气。)

下一个:

override public func layoutSubviews() {

您似乎认为在动画播放期间您会多次收到该消息。你不。而且你忘记调用 super 这可能是灾难性的。

最后,您对形状图层的使用:

lazy var ringLayer: CAShapeLayer = {
    let ringLayer = CAShapeLayer()
    let ringPath = UIBezierPath(ovalIn: self.bounds.insetBy(dx: 4, dy: 4))
    ringLayer.fillColor = UIColor.clear.cgColor
    ringLayer.strokeColor = UIColor.black.cgColor
    ringLayer.path = ringPath.cgPath
    ringLayer.name = "ring"
    ringLayer.needsDisplayOnBoundsChange = true

    return ringLayer
}()

首先,您的图层没有框架,因此没有大小。其次,此代码将仅运行一次。因此,您将获得一条路径,一次,然后就不会发生任何会改变路径大小的事情。当 View 发生变化时,ringPath 半径不会神奇地永远与 View 的大小相关联。它是按照一定的尺寸创建的,仅此而已。这就是为什么您实际上看不到路径发生变化的原因。 (使用needsDisplayOnBoundsChange是完全无关的,因为你没有一个可以显示的图层!那将是进行绘图的图层,而这并不是什么你有。)

用 View 绘图

好吧,最简单的解决方案是什么?我首先要做的就是完全丢弃图层并只绘制圆圈​​:

public class BodyAreaView: UIView {
    override public func draw(_ rect: CGRect) {
        let ringPath = UIBezierPath(ovalIn: self.bounds.insetBy(dx: 4, dy: 4))
        let con = UIGraphicsGetCurrentContext()!
        con.addPath(ringPath.cgPath)
        con.strokePath()
    }
}

效果非常好,因为我们绘制了一次,从那时起我们就使用缓存的圆形绘图,它会随着 View 的增长而增长。如果我们再次绘制,我们将根据当时 View 的大小按比例绘制,因此圆圈看起来总是正确的。

使用图层进行绘制

好的,但这并不能真正回答您最初的问题,即如何使用图层来做到这一点。好吧,答案又是:不要使用形状图层,因为那不是自动重绘图层。使用自定义图层执行此操作,该图层知道如何根据当前边界大小将自身重新绘制为圆形。例如:

public class BodyAreaView: UIView {
    class Circle : CALayer {
        override func draw(in con: CGContext) {
            let ringPath = UIBezierPath(ovalIn: self.bounds.insetBy(dx: 4, dy: 4))
            con.addPath(ringPath.cgPath)
            con.strokePath()
        }
    }
    let circle = Circle()
    public override init(frame: CGRect) {
        super.init(frame:frame)
        self.circle.needsDisplayOnBoundsChange = true
        self.circle.frame = self.bounds
        self.layer.addSublayer(self.circle)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    public override func layoutSubviews() {
        super.layoutSubviews()
        if let layers = self.layer.sublayers {
            for lay in layers {
                lay.frame = self.bounds
            }
        }

    }
}

但是,请注意,这会导致圆圈的大小在动画开始时跳跃。它跳到正确的位置,即动画结束时圆圈需要出现的位置。那是因为你没有获得图层的自动布局动画。这就是为什么我更喜欢第一个解决方案;至少在那里,圆圈随着视野而变大。

但是您可以通过单独设置图层大小的动画来解决该问题。这留给读者作为练习。 :)

关于swift - 在 Xcode Playground 中制作动画时,位置元素与 super View 成比例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51694042/

相关文章:

ios - UITableView 和 NSFetchedResultsController 内存泄漏

ios - 如何从字典中删除指定键的一对?

ios - 将前导空间设置为 UIView , swift 崩溃

iOS Autolayout 与 2 个 View 保持距离

ios - Stackview 安排问题

swift - 如何在 iOS Swift 中获取蓝牙已打开的设备的名称

ios - 在 Swift 中使用 Alamofire 5.0 获取 3 级深度 JSON 值

ios - objective-c - ios : How to pick video from Camera Roll?

ios - 如何修复 UITableViewCell segue

xcode - 在 Jenkins 中运行 XCodeBuild 的代码签名错误