如何对约束更改进行动画处理?

How do I animate constraint changes?

提问人:DBD 提问时间:9/27/2012 最后编辑:DBD 更新时间:4/27/2021 访问量:335870

问:

我正在更新一个旧应用程序,当没有广告时,它会滑出屏幕。当有广告时,它会在屏幕上滑动。基本的东西。AdBannerView

旧风格,我在动画块中设置帧。 新样式,我有一个 to auto-layout 约束,它决定了位置,在这种情况下,它是与 superview 底部的距离,并修改常量:IBOutletY

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

顺便说一句,我已经检查了无数次,这是在线程上执行的。

iOS Objective-C 动画 自动布局 iOS6

评论


答:

1812赞 g3rv4 10/1/2012 #1

两个重要说明:

  1. 您需要在动画块内调用。Apple 实际上建议您在动画块之前调用它一次,以确保所有挂起的布局操作都已完成layoutIfNeeded

  2. 您需要在父视图(例如)上专门调用它,而不是附加了约束的子视图。这样做将更新所有受约束的视图,包括对可能受约束到您更改约束的视图的其他视图进行动画处理(例如,视图 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()
}

评论

233赞 DBD 10/15/2012
你知道吗。。。你的答案有效。WWDC工作....我的愿景失败了。出于某种原因,我花了一个星期才意识到我打电话而不是.我花了多少时间没有注意到我只是输入了错误的方法名称,这让我有点震惊。setNeedsLayoutlayoutIfNeeded
26赞 Ortwin Gentz 1/14/2013
该解决方案有效,但您不需要更改动画块的约束常量。在启动动画之前设置一次约束是完全可以的。你应该编辑你的答案。
79赞 Oliver Pearmain 3/21/2013
这最初对我不起作用,然后我意识到您需要在 PARENT 视图上调用 layoutIfNeeded,而不是约束适用的视图。
20赞 ngb 8/24/2013
使用 layoutIfNeeded 将对所有子视图刷新进行动画处理,而不仅仅是约束更改。如何仅对约束更改进行动画处理?
11赞 Rick van der Linde 11/23/2015
“苹果实际上建议你在动画块之前调用它一次,以确保所有待处理的布局操作都已完成”,谢谢,从来没有想过这一点,但这是有道理的。
3赞 D.D. 9/13/2013 #2

有一篇文章谈到了这一点: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];
        }];
    }
}

希望它有所帮助。

40赞 John Erck 12/23/2013 #3
// 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];
}];
117赞 Cameron Lowell Palmer 1/22/2014 #4

我很欣赏提供的答案,但我认为更进一步会很好。

文档中的基本块动画

[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 方法中处理。UISwitchUITextField

评论

0赞 anneblue 2/24/2014
当调用上述所有方法时(而不是在该视图的子视图上),调用不是必需的(因为也会触发该视图)。对大多数人来说可能是微不足道的,但直到现在才适合我;)self.viewupdateConstraintsIfNeededsetNeedsLayoutupdateConstraints
0赞 Cameron Lowell Palmer 2/24/2014
如果不了解自动布局与弹簧和支柱布局在您的特定情况下的全部相互作用,就很难发表评论。我说的是纯粹的自动布局方案。我很想知道您的 layoutSubviews 方法中到底发生了什么。
0赞 Cameron Lowell Palmer 2/25/2014
translatesAutoresizingMaskIntoConstraints = 否;如果你想纯自动布局,你绝对应该禁用它。
0赞 Cameron Lowell Palmer 2/26/2014
我还没有看到,但如果没有一些代码,我无法真正谈论这个问题。也许你可以模拟一个 TableView 并将其粘贴到 GitHub 上?
0赞 Robert 9/12/2014
小心:如果使用 布局约束将在动画之前设置!UIViewAnimationOptionBeginFromCurrentState
5赞 Gabriel.Massana 11/25/2014 #5

我试图为约束制作动画,但要找到一个好的解释并不容易。

其他答案所说的是完全正确的:你需要打电话到里面.但是,另一个重要的一点是要为每个想要动画制作动画的人提供指针。[self.view layoutIfNeeded];animateWithDuration: animations:NSLayoutConstraint

我在 GitHub 中创建了一个示例

85赞 Steven Hepting 11/13/2015 #6

通常,您只需要更新约束并在动画块内部调用即可。这可以是更改 的属性 ,添加删除约束 (iOS 7),或更改约束的属性 (iOS 8 和 9)。layoutIfNeeded.constantNSLayoutConstraint.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];
}];

示例设置:

Xcode Project so sample animation project.

争议

关于是否应该在动画块之前或动画块内部更改约束,存在一些问题(请参阅前面的答案)。

以下是教授 iOS 的 Martin Pilkington 和编写 Auto Layout 的 Ken Ferry 之间的 Twitter 对话。Ken 解释说,虽然在动画块之外更改常量目前可能有效,但它并不安全,它们确实应该在动画块更改。https://twitter.com/kongtomorrow/status/440627401018466305

动画:

示例项目

下面是一个简单的项目,演示如何对视图进行动画处理。它使用 Objective C,并通过更改多个约束的属性来对视图进行动画处理。https://github.com/shepting/SampleAutoLayoutAnimation.active

评论

0赞 Korey Hinton 11/25/2015
+1 表示使用新活动标志的示例。此外,更改动画块之外的约束对我来说总是感觉像是一种黑客攻击
0赞 malhal 1/12/2016
我在 github 中提出了一个问题,因为此解决方案不适用于将视图移回原来的位置。我相信更改活动不是正确的解决方案,您应该更改优先级。将顶部设置为 750,将底部设置为 250,然后在代码中在 UILayoutPriorityDefaultHigh 和 UILayoutPriorityDefaultLow 之间交替。
0赞 Chris Conover 6/23/2016
在块内部更新的另一个原因是,它将更改与调用代码隔离开来,这些代码可能也调用 layoutIfNeeded。(并感谢您的 Twitter 链接)
1赞 Jona 11/18/2017
很好的答案。特别是因为你提到了马丁和肯关于这个问题的谈话。
0赞 mfaani 12/16/2017
我对更新约束的方式感到困惑,并写了这篇文章。你能看一下吗?
16赞 Tommie C. 12/11/2015 #7

故事板、代码、提示和一些陷阱

其他答案都很好,但这个答案使用最近的一个示例突出显示了动画约束的一些相当重要的陷阱。在我意识到以下几点之前,我经历了很多变化:

将要作为目标的约束放入类变量中以保存强引用。在 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

        }
    }
}

许多错误的转弯

这些笔记实际上是我为自己写的一组技巧。我亲自做了所有不该做的事,而且很痛苦。希望本指南可以饶恕其他人。

  1. 注意 zPositioning。有时当什么都没有明显的时候 发生这种情况时,您应该隐藏其他一些视图或使用该视图 调试器来定位动画视图。我什至发现用户定义的运行时 属性在情节提要的 xml 中丢失,并导致动画 视图被覆盖(工作时)。

  2. 总是花一分钟时间阅读文档(新的和旧的),快速 帮助和标头。苹果不断做出很多改变,以变得更好 管理自动布局约束(请参阅堆栈视图)。或者至少是 AutoLayout Cookbook。请记住,有时最好的解决方案是在较旧的文档/视频中。

  3. 尝试使用动画中的值,并考虑使用 其他 animateWithDuration 变体。

  4. 不要将特定布局值硬编码为确定的标准 更改其他常量时,请改用允许您 确定视图的位置。 是其中之一 例CGRectContainsRect

  5. 如果需要,请随时使用与 参与约束定义 : 的视图在示例中let viewMargins = self.webview.layoutMarginsGuide
  6. 不做你不必做的工作,所有视图都带有约束 情节提要具有附加到属性的约束 self.viewName.约束
  7. 将任何约束的优先级更改为小于 1000。我设置 我在故事板上达到 250(低)或 750(高);(如果您尝试将 1000 优先级更改为代码中的任何内容,则应用程序将崩溃,因为需要 1000)
  8. 考虑不要立即尝试使用 activateConstraints 和 deactivateConstraints(它们有自己的位置,但是当只是学习时,或者如果你正在使用故事板时,使用它们可能意味着你做得太多了~它们确实有一席之地,如下图所示)
  9. 考虑不要使用 addConstraints / removeConstraints,除非你是 真正在代码中添加新的约束。我发现大多数时候我 在情节提要中布局具有所需约束的视图(将 屏幕外的视图),然后在代码中,我对之前在情节提要中创建的约束进行动画处理,以移动视图。
  10. 我花了很多时间浪费在新的 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 的这段早期视频

密切关注

在文档中,通常有一些小的注释或代码片段,可以带来更大的想法。例如,将自动布局约束附加到动态动画师是一个大主意。

祝你好运,愿原力与你同在。

1赞 ivanferdelja 4/15/2016 #8

在约束动画的上下文中,我想提一个特定情况,即我立即在keyboard_opened通知中对约束进行动画处理。

约束定义了从文本字段到容器顶部的顶部空间。打开键盘后,我只是将常数除以 2。

我无法直接在键盘通知中实现一致的平滑约束动画。大约一半的时间视图会跳转到新位置 - 没有动画。

我突然想到,由于键盘打开,可能会发生一些额外的布局。 添加一个延迟为 10 毫秒的简单dispatch_after块使动画每次都运行——没有跳跃。

19赞 Nazariy Vlizlo 8/11/2016 #9

Swift 解决方案:

yourConstraint.constant = 50
UIView.animate(withDuration: 1.0, animations: {
    yourView.layoutIfNeeded
})
13赞 C0mrade 6/12/2017 #10

工作解决方案 100%斯威夫特 5.3

我已经阅读了所有答案,并想分享我在所有应用程序中使用的代码和线条层次结构,以正确地为它们制作动画,这里的一些解决方案不起作用,您应该在较慢的设备上检查它们,例如 iPhone 5 目前。

self.btnHeightConstraint.constant = 110
UIView.animate(withDuration: 0.27) { [weak self] in 
     self?.view.layoutIfNeeded()
}
4赞 Edoardo Vicoli 7/19/2017 #11

适用于 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()

希望这有帮助。

32赞 Milan Nosáľ 2/16/2018 #12

Swift 4 解决方案

UIView.动画

三个简单的步骤:

  1. 更改约束条件,例如:

    heightAnchor.constant = 50
    
  2. 告诉包含它的布局是脏的,并且自动布局应该重新计算布局:view

    self.view.setNeedsLayout()
    
  3. 在动画块中,告诉布局重新计算布局,这相当于直接设置帧(在这种情况下,自动布局将设置帧):

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

UIViewPropertyAnimator

由于在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()

由于是动画的封装,我们可以保留对它的引用,并在以后调用它。但是,由于在动画块中,我们只是告诉自动布局重新计算帧,因此我们必须在调用之前更改约束。因此,这样的事情是可能的:animatorstartAnimation

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

更改约束和启动动画器的顺序很重要 - 如果我们只是更改约束并将动画器留到以后的某个时间点,则下一个重绘周期可以调用自动布局重新计算,并且不会对更改进行动画处理。

另外,请记住,单个动画器是不可重用的 - 一旦你运行它,你就不能“重新运行”它。所以我想没有充分的理由让动画师留在身边,除非我们用它来控制交互式动画。

评论

0赞 Medhi 4/3/2020
layoutIfNeeded() 是关键