在Xcode游乐场中进行动画处理时,位置元素与视图成比例

问题描述 投票:0回答:1

我正在尝试将视图元素放置在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
    }
}

}
swift xcode autolayout uiviewanimation swift-playground
1个回答
0
投票

约束冲突

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

    self.superWidthConstraint.isActive = true

您正在获得约束冲突,因为您的iphoneWidthConstraint仍处于活动状态:您要both旧的宽度约束and新的宽度约束。因此,您几乎看不到任何宽度变化的事实真是太幸运了。

解决方案如下。首先,用一个标志编写您的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的使用完全无关紧要,因为您没有可显示的图层!那是you进行绘图的图层,而那不是您所拥有的。)带视图绘图

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

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

这很好用,因为我们绘制

once

,从那时起,我们使用缓存的圆形绘图,该圆形绘图随着视图的增长而增长。如果再次进行绘制,则将按当时视图的大小成比例绘制,因此圆圈将始终看起来正确。带有图层的绘图好的,但这并不能真正回答您的原始问题,即如何使用

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。它跳到正确的位置,即该圆必须位于动画的结尾。那是因为您不会获得图层的自动布局动画。这就是为什么我更喜欢第一个解决方案。至少在那里,圆随着视图而增长。

但是您可以通过

单独设置图层大小的动画]解决此问题。留给读者练习。 :)

© www.soinside.com 2019 - 2024. All rights reserved.