提问人:DBD 提问时间:9/27/2012 最后编辑:DBD 更新时间:4/27/2021 访问量:335870
如何对约束更改进行动画处理?
How do I animate constraint changes?
问:
我正在更新一个旧应用程序,当没有广告时,它会滑出屏幕。当有广告时,它会在屏幕上滑动。基本的东西。AdBannerView
旧风格,我在动画块中设置帧。
新样式,我有一个 to auto-layout 约束,它决定了位置,在这种情况下,它是与 superview 底部的距离,并修改常量:IBOutlet
Y
- (void)moveBannerOffScreen {
[UIView animateWithDuration:5 animations:^{
_addBannerDistanceFromBottomConstraint.constant = -32;
}];
bannerIsVisible = FALSE;
}
- (void)moveBannerOnScreen {
[UIView animateWithDuration:5 animations:^{
_addBannerDistanceFromBottomConstraint.constant = 0;
}];
bannerIsVisible = TRUE;
}
横幅移动,完全符合预期,但没有动画。
更新:我重新观看了 WWDC 12 演讲 掌握自动布局的最佳实践 其中涵盖了动画。它讨论了如何使用 CoreAnimation 更新约束:
我尝试了以下代码,但得到完全相同的结果:
- (void)moveBannerOffScreen {
_addBannerDistanceFromBottomConstraint.constant = -32;
[UIView animateWithDuration:2 animations:^{
[self.view setNeedsLayout];
}];
bannerIsVisible = FALSE;
}
- (void)moveBannerOnScreen {
_addBannerDistanceFromBottomConstraint.constant = 0;
[UIView animateWithDuration:2 animations:^{
[self.view setNeedsLayout];
}];
bannerIsVisible = TRUE;
}
顺便说一句,我已经检查了无数次,这是在主线程上执行的。
答:
两个重要说明:
您需要在动画块内调用。Apple 实际上建议您在动画块之前调用它一次,以确保所有挂起的布局操作都已完成
layoutIfNeeded
您需要在父视图(例如)上专门调用它,而不是附加了约束的子视图。这样做将更新所有受约束的视图,包括对可能受约束到您更改约束的视图的其他视图进行动画处理(例如,视图 B 附加到视图 A 的底部,而您刚刚更改了视图 A 的顶部偏移量,并且您希望视图 B 使用它进行动画处理)
self.view
试试这个:
Objective-C语言
- (void)moveBannerOffScreen {
[self.view layoutIfNeeded];
[UIView animateWithDuration:5
animations:^{
self._addBannerDistanceFromBottomConstraint.constant = -32;
[self.view layoutIfNeeded]; // Called on parent view
}];
bannerIsVisible = FALSE;
}
- (void)moveBannerOnScreen {
[self.view layoutIfNeeded];
[UIView animateWithDuration:5
animations:^{
self._addBannerDistanceFromBottomConstraint.constant = 0;
[self.view layoutIfNeeded]; // Called on parent view
}];
bannerIsVisible = TRUE;
}
斯威夫特 3
UIView.animate(withDuration: 5) {
self._addBannerDistanceFromBottomConstraint.constant = 0
self.view.layoutIfNeeded()
}
评论
setNeedsLayout
layoutIfNeeded
有一篇文章谈到了这一点:http://weblog.invasivecode.com/post/42362079291/auto-layout-and-core-animation-auto-layout-was
其中,他是这样编码的:
- (void)handleTapFrom:(UIGestureRecognizer *)gesture {
if (_isVisible) {
_isVisible = NO;
self.topConstraint.constant = -44.; // 1
[self.navbar setNeedsUpdateConstraints]; // 2
[UIView animateWithDuration:.3 animations:^{
[self.navbar layoutIfNeeded]; // 3
}];
} else {
_isVisible = YES;
self.topConstraint.constant = 0.;
[self.navbar setNeedsUpdateConstraints];
[UIView animateWithDuration:.3 animations:^{
[self.navbar layoutIfNeeded];
}];
}
}
希望它有所帮助。
// Step 1, update your constraint
self.myOutletToConstraint.constant = 50; // New height (for example)
// Step 2, trigger animation
[UIView animateWithDuration:2.0 animations:^{
// Step 3, call layoutIfNeeded on your animated view's parent
[self.view layoutIfNeeded];
}];
我很欣赏提供的答案,但我认为更进一步会很好。
文档中的基本块动画
[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
// Make all constraint changes here
[containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];
但实际上,这是一个非常简单的场景。如果我想通过该方法对子视图约束进行动画处理怎么办?updateConstraints
调用子视图 updateConstraints 方法的动画块
[self.view layoutIfNeeded];
[self.subView setNeedsUpdateConstraints];
[self.subView updateConstraintsIfNeeded];
[UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionLayoutSubviews animations:^{
[self.view layoutIfNeeded];
} completion:nil];
updateConstraints 方法在 UIView 子类中重写,必须在方法末尾调用 super。
- (void)updateConstraints
{
// Update some constraints
[super updateConstraints];
}
AutoLayout 指南还有很多不足之处,但值得一读。我自己正在使用它作为的一部分,该子视图带有一对 s 切换子视图,并带有简单而微妙的折叠动画(0.2 秒长)。如上所述,子视图的约束在 UIView 子类 updateConstraints 方法中处理。UISwitch
UITextField
评论
self.view
updateConstraintsIfNeeded
setNeedsLayout
updateConstraints
UIViewAnimationOptionBeginFromCurrentState
我试图为约束制作动画,但要找到一个好的解释并不容易。
其他答案所说的是完全正确的:你需要打电话到里面.但是,另一个重要的一点是要为每个想要动画制作动画的人提供指针。[self.view layoutIfNeeded];
animateWithDuration: animations:
NSLayoutConstraint
通常,您只需要更新约束并在动画块内部调用即可。这可以是更改 的属性 ,添加删除约束 (iOS 7),或更改约束的属性 (iOS 8 和 9)。layoutIfNeeded
.constant
NSLayoutConstraint
.active
示例代码:
[UIView animateWithDuration:0.3 animations:^{
// Move to right
self.leadingConstraint.active = false;
self.trailingConstraint.active = true;
// Move to bottom
self.topConstraint.active = false;
self.bottomConstraint.active = true;
// Make the animation happen
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}];
示例设置:
争议
关于是否应该在动画块之前或动画块内部更改约束,存在一些问题(请参阅前面的答案)。
以下是教授 iOS 的 Martin Pilkington 和编写 Auto Layout 的 Ken Ferry 之间的 Twitter 对话。Ken 解释说,虽然在动画块之外更改常量目前可能有效,但它并不安全,它们确实应该在动画块内更改。https://twitter.com/kongtomorrow/status/440627401018466305
动画:
示例项目
下面是一个简单的项目,演示如何对视图进行动画处理。它使用 Objective C,并通过更改多个约束的属性来对视图进行动画处理。https://github.com/shepting/SampleAutoLayoutAnimation.active
评论
故事板、代码、提示和一些陷阱
其他答案都很好,但这个答案使用最近的一个示例突出显示了动画约束的一些相当重要的陷阱。在我意识到以下几点之前,我经历了很多变化:
将要作为目标的约束放入类变量中以保存强引用。在 Swift 中,我使用了惰变量:
lazy var centerYInflection:NSLayoutConstraint = {
let temp = self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
return temp!
}()
经过一些实验,我注意到必须从上面的视图(又名超级视图)中获取约束,这两个视图定义了约束。在下面的示例中(MNGStarRating 和 UIWebView 都是我在其中创建约束的两种类型的项,它们是 self.view 中的子视图)。
筛选器链接
我利用 Swift 的滤波方法来分离将作为拐点的所需约束。一个也可能变得更加复杂,但过滤器在这里做得很好。
使用 Swift 对约束进行动画处理
Nota Bene - 此示例是情节提要/代码解决方案,并假设 一个在情节提要中设置了默认约束。然后可以 使用代码对更改进行动画处理。
假设您创建了一个属性来使用准确的条件进行过滤,并到达动画的特定拐点(当然,如果需要多个约束,也可以过滤数组并循环):
lazy var centerYInflection:NSLayoutConstraint = {
let temp = self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
return temp!
}()
....
过了一会儿......
@IBAction func toggleRatingView (sender:AnyObject){
let aPointAboveScene = -(max(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height) * 2.0)
self.view.layoutIfNeeded()
//Use any animation you want, I like the bounce in springVelocity...
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.75, options: [.CurveEaseOut], animations: { () -> Void in
//I use the frames to determine if the view is on-screen
if CGRectContainsRect(self.view.frame, self.ratingView.frame) {
//in frame ~ animate away
//I play a sound to give the animation some life
self.centerYInflection.constant = aPointAboveScene
self.centerYInflection.priority = UILayoutPriority(950)
} else {
//I play a different sound just to keep the user engaged
//out of frame ~ animate into scene
self.centerYInflection.constant = 0
self.centerYInflection.priority = UILayoutPriority(950)
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}) { (success) -> Void in
//do something else
}
}
}
许多错误的转弯
这些笔记实际上是我为自己写的一组技巧。我亲自做了所有不该做的事,而且很痛苦。希望本指南可以饶恕其他人。
注意 zPositioning。有时当什么都没有明显的时候 发生这种情况时,您应该隐藏其他一些视图或使用该视图 调试器来定位动画视图。我什至发现用户定义的运行时 属性在情节提要的 xml 中丢失,并导致动画 视图被覆盖(工作时)。
总是花一分钟时间阅读文档(新的和旧的),快速 帮助和标头。苹果不断做出很多改变,以变得更好 管理自动布局约束(请参阅堆栈视图)。或者至少是 AutoLayout Cookbook。请记住,有时最好的解决方案是在较旧的文档/视频中。
尝试使用动画中的值,并考虑使用 其他 animateWithDuration 变体。
不要将特定布局值硬编码为确定的标准 更改其他常量时,请改用允许您 确定视图的位置。 是其中之一 例
CGRectContainsRect
- 如果需要,请随时使用与
参与约束定义 : 的视图在示例中
let viewMargins = self.webview.layoutMarginsGuide
- 不做你不必做的工作,所有视图都带有约束 情节提要具有附加到属性的约束 self.viewName.约束
- 将任何约束的优先级更改为小于 1000。我设置 我在故事板上达到 250(低)或 750(高);(如果您尝试将 1000 优先级更改为代码中的任何内容,则应用程序将崩溃,因为需要 1000)
- 考虑不要立即尝试使用 activateConstraints 和 deactivateConstraints(它们有自己的位置,但是当只是学习时,或者如果你正在使用故事板时,使用它们可能意味着你做得太多了~它们确实有一席之地,如下图所示)
- 考虑不要使用 addConstraints / removeConstraints,除非你是 真正在代码中添加新的约束。我发现大多数时候我 在情节提要中布局具有所需约束的视图(将 屏幕外的视图),然后在代码中,我对之前在情节提要中创建的约束进行动画处理,以移动视图。
- 我花了很多时间浪费在新的 NSAnchorLayout 类和子类。这些工作很好,但它 我花了一段时间才意识到我需要的所有限制 已存在于情节提要中。如果在代码中生成约束 那么最肯定的是,使用这种方法来聚合约束:
使用情节提要时要避免的解决方案的快速示例
private var _nc:[NSLayoutConstraint] = []
lazy var newConstraints:[NSLayoutConstraint] = {
if !(self._nc.isEmpty) {
return self._nc
}
let viewMargins = self.webview.layoutMarginsGuide
let minimumScreenWidth = min(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height)
let centerY = self.ratingView.centerYAnchor.constraintEqualToAnchor(self.webview.centerYAnchor)
centerY.constant = -1000.0
centerY.priority = (950)
let centerX = self.ratingView.centerXAnchor.constraintEqualToAnchor(self.webview.centerXAnchor)
centerX.priority = (950)
if let buttonConstraints = self.originalRatingViewConstraints?.filter({
($0.firstItem is UIButton || $0.secondItem is UIButton )
}) {
self._nc.appendContentsOf(buttonConstraints)
}
self._nc.append( centerY)
self._nc.append( centerX)
self._nc.append (self.ratingView.leadingAnchor.constraintEqualToAnchor(viewMargins.leadingAnchor, constant: 10.0))
self._nc.append (self.ratingView.trailingAnchor.constraintEqualToAnchor(viewMargins.trailingAnchor, constant: 10.0))
self._nc.append (self.ratingView.widthAnchor.constraintEqualToConstant((minimumScreenWidth - 20.0)))
self._nc.append (self.ratingView.heightAnchor.constraintEqualToConstant(200.0))
return self._nc
}()
如果您忘记了这些提示之一或更简单的提示(例如在哪里添加 layoutIfNeeded),很可能什么都不会发生: 在这种情况下,您可能有一个半生不熟的解决方案,如下所示:
注意 - 花点时间阅读下面的 AutoLayout 部分和 原始指南。有一种方法可以使用这些技术来补充 您的动态动画师。
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 1.0, options: [.CurveEaseOut], animations: { () -> Void in
//
if self.starTopInflectionPoint.constant < 0 {
//-3000
//offscreen
self.starTopInflectionPoint.constant = self.navigationController?.navigationBar.bounds.height ?? 0
self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
} else {
self.starTopInflectionPoint.constant = -3000
self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
}
}) { (success) -> Void in
//do something else
}
}
《自动布局指南》中的代码段(请注意,第二个代码段用于使用 OS X)。顺便说一句 - 据我所知,这不再在当前指南中。首选技术不断发展。
对“自动布局”所做的更改进行动画处理
如果需要完全控制“自动布局”所做的动画更改,则必须以编程方式进行约束更改。iOS 和 OS X 的基本概念是相同的,但有一些细微的区别。
在 iOS 应用中,代码如下所示:
[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
// Make all constraint changes here
[containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];
在 OS X 中,使用图层支持的动画时,请使用以下代码:
[containterView layoutSubtreeIfNeeded];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
[context setAllowsImplicitAnimation: YES];
// Make all constraint changes here
[containerView layoutSubtreeIfNeeded];
}];
如果不使用图层支持的动画,则必须使用约束的动画器对常量进行动画处理:
[[constraint animator] setConstant:42];
对于那些在视觉上学习得更好的人,请查看 Apple 的这段早期视频。
密切关注
在文档中,通常有一些小的注释或代码片段,可以带来更大的想法。例如,将自动布局约束附加到动态动画师是一个大主意。
祝你好运,愿原力与你同在。
在约束动画的上下文中,我想提一个特定情况,即我立即在keyboard_opened通知中对约束进行动画处理。
约束定义了从文本字段到容器顶部的顶部空间。打开键盘后,我只是将常数除以 2。
我无法直接在键盘通知中实现一致的平滑约束动画。大约一半的时间视图会跳转到新位置 - 没有动画。
我突然想到,由于键盘打开,可能会发生一些额外的布局。 添加一个延迟为 10 毫秒的简单dispatch_after块使动画每次都运行——没有跳跃。
Swift 解决方案:
yourConstraint.constant = 50
UIView.animate(withDuration: 1.0, animations: {
yourView.layoutIfNeeded
})
工作解决方案 100%斯威夫特 5.3
我已经阅读了所有答案,并想分享我在所有应用程序中使用的代码和线条层次结构,以正确地为它们制作动画,这里的一些解决方案不起作用,您应该在较慢的设备上检查它们,例如 iPhone 5 目前。
self.btnHeightConstraint.constant = 110
UIView.animate(withDuration: 0.27) { [weak self] in
self?.view.layoutIfNeeded()
}
适用于 Xcode 8.3.3 的 Swift 3 工作和刚刚测试的解决方案:
self.view.layoutIfNeeded()
self.calendarViewHeight.constant = 56.0
UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
请记住,self.calendarViewHeight 是引用 customView (CalendarView) 的约束。我在self.view上调用了.layoutIfNeeded(),而不是在self.calendarView上调用了.layoutIfNeeded()
希望这有帮助。
Swift 4 解决方案
三个简单的步骤:
更改约束条件,例如:
heightAnchor.constant = 50
告诉包含它的布局是脏的,并且自动布局应该重新计算布局:
view
self.view.setNeedsLayout()
在动画块中,告诉布局重新计算布局,这相当于直接设置帧(在这种情况下,自动布局将设置帧):
UIView.animate(withDuration: 0.5) { self.view.layoutIfNeeded() }
完整的最简单的例子:
heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
旁注
有一个可选的第 0 步 - 在更改约束之前,您可能需要调用以确保动画的起点来自应用了旧约束的状态(如果动画中不应包含其他一些约束更改):self.view.layoutIfNeeded()
otherConstraint.constant = 30
// this will make sure that otherConstraint won't be animated but will take effect immediately
self.view.layoutIfNeeded()
heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
由于在iOS 10中,我们得到了一种新的动画机制 - ,我们应该知道基本上相同的机制适用于它。步骤基本相同:UIViewPropertyAnimator
heightAnchor.constant = 50
self.view.setNeedsLayout()
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
self.view.layoutIfNeeded()
}
animator.startAnimation()
由于是动画的封装,我们可以保留对它的引用,并在以后调用它。但是,由于在动画块中,我们只是告诉自动布局重新计算帧,因此我们必须在调用之前更改约束。因此,这样的事情是可能的:animator
startAnimation
// prepare the animator first and keep a reference to it
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
self.view.layoutIfNeeded()
}
// at some other point in time we change the constraints and call the animator
heightAnchor.constant = 50
self.view.setNeedsLayout()
animator.startAnimation()
更改约束和启动动画器的顺序很重要 - 如果我们只是更改约束并将动画器留到以后的某个时间点,则下一个重绘周期可以调用自动布局重新计算,并且不会对更改进行动画处理。
另外,请记住,单个动画器是不可重用的 - 一旦你运行它,你就不能“重新运行”它。所以我想没有充分的理由让动画师留在身边,除非我们用它来控制交互式动画。
评论