在Xcode Playground中进行动画时,位置元素与Superview成比例

时间:2018-08-05 11:48:59

标签: swift xcode autolayout uiviewanimation swift-playground

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

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

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

我花了几天的时间试图找出为什么自动布局无法使圆形视图正确定位,但是却找不到原因。

Body Animation

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

Xcode Playground快速代码:

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游乐场,现在它将按照建议以.cyan颜色显示圆形视图的背景。

仅更新UIView子类的图形代码听起来很容易,但是经过几个小时在堆栈溢出处搜索解决方案后,所有帖子都没有任何帮助。看起来圆圈拒绝重绘,即使视图本身的边界发生了变化。

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

这是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
    }
}

}

1 个答案:

答案 0 :(得分:0)

约束冲突

首先,您没有考虑到布局可能发生多次。因此,在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的经典错误,即您不绘画。相反,您执行了其他完全不相关的操作:添加了一层。这总是很危险的,因为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
}()

首先,您的图层没有框架,因此没有大小。其次,此代码将仅运行一次 。因此,您将获得一条路径,一次,然后将不会发生任何会改变路径大小的事情。 ringPath半径不会随着视图的变化而永久地与视图的大小神奇地联系在一起。它以一定的大小创建,仅此而已。这就是为什么您实际上没有看到路径永远改变的原因。 (needsDisplayOnBoundsChange的使用完全无关紧要,因为您没有可显示的图层!这将是您在其中绘制图形的 层,而不是您所需要的。 )

随视图绘图

好的,最简单的解决方案是什么?首先,我要做的就是完全丢弃该图层,然后画一个圆圈:

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()
    }
}

这非常好用,因为我们绘制一次 ,然后使用缓存的圆图,该图随着视图的增长而增长。而且,如果我们再次绘制,我们将根据当时视图的大小按比例绘制,因此圆圈将始终看起来正确。

绘制图层

好的,但这并不能真正回答您的原始问题,即如何使用 layer 进行此操作。好吧,答案再次是:不要使用形状层,因为那不是自重绘制层。使用 custom 图层执行此操作,该图层知道如何根据其当前边界大小将自己重画为一个圆形。例如:

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
            }
        }

    }
}

但是,请注意,这会使圆的大小在动画开始时 jump 。它跳到正确的位置,即该圆必须位于动画的结尾。那是因为您不会获得图层的自动布局动画。这就是为什么我更喜欢第一个解决方案。至少在那里,圆圈随视图而扩大。

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