gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-11-30 11:16:31 28 4
gpt4 key购买 nike

我试图将 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/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com