如何为约束更改设置动画?


959

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

旧样式,我将帧设置在动画块中。新样式,我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;
}

附带一提,我已经检查了很多次,并且这是在线程上执行的。


11
我以前从未见过这么多关于错字的问答式投票
大约

3
如果答案中有错字,则应编辑答案。这就是为什么它们是可编辑的。
i_am_jorf 2014年

@jeffamaphone-如果您指出拼写错误,这样我会知道错误在哪里,这将更加有用。您可以自己编辑答案并解决错字问题,从而节省其他人的负担。如果您要指的是,我只是对其进行了编辑以从动画块中删除常量。
DBD 2014年

1
我不知道错字是什么。我在回应以上评论。
i_am_jorf 2014年

9
然后错字问题。愚蠢的是,我键入的是“ setNeedsLayout”而不是“ layoutIfNeeded”。当我剪切并粘贴带有错误的代码和带有正确命令的屏幕截图时,问题中清楚地显示了它。直到有人指出它为止,它似乎还没注意到。
DBD 2014年

Answers:


1696

两个重要说明:

  1. 您需要layoutIfNeeded在动画块中调用。苹果实际上建议您在动画块之前调用一次,以确保所有未完成的布局操作已完成

  2. 您需要专门在父视图(例如self.view)上调用它,而不是在附加了约束的子视图上调用它。这样做将更新所有受约束的视图,包括为其他可能受约束的视图设置动画(例如,将视图B附加到视图A的底部,而您刚刚更改了视图A的顶部偏移,并且希望视图B)制作动画)

尝试这个:

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

199
您知道...您的答案有效。WWDC工作正常。由于某种原因,我花了一个星期才意识到我正在打电话setNeedsLayout而不是打电话layoutIfNeeded。我花了几个小时没有注意到我刚刚输入了错误的方法名称,这让我有些恐惧。
DBD 2012年

23
该解决方案有效,但是您无需更改动画块的约束常数。在开始动画之前设置约束一次是完全可以的。您应该编辑答案。
Ortwin Gentz

74
最初这对我不起作用,然后我意识到您需要在PARENT视图上调用layoutIfNeeded,而不是约束所适用的视图。
奥利弗·皮尔曼

20
使用layoutIfNeeded可以为所有子视图刷新设置动画,而不仅仅是约束更改。您如何仅对约束更改进行动画处理?
ngb

11
“ Apple实际上建议您在动画块之前调用它一次,以确保完成所有待处理的布局操作”,谢谢,从未考虑过,但这是有道理的。
里克·范德·林德

109

我感谢提供的答案,但我认为将其进一步扩展会很好。

文档中的基本块动画

[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];
}

《自动版式指南》有很多不足之处,但值得阅读。我本人将此作为a的一部分,通过简单而细微的折叠动画(长0.2秒)UISwitch用一对UITextFields 切换子视图。如上所述,子视图的约束在UIView子类的updateConstraints方法中进行处理。


在上面的所有方法上self.view(而不是在该视图的子视图上)调用时updateConstraintsIfNeeded,不需要调用(因为该视图的setNeedsLayout触发器updateConstraints也是如此)。可能对大多数人来说微不足道,但直到现在
才对

在您不了解特定情况下自动布局与弹簧和撑杆布局的相互作用的情况下,很难做出评论。我完全是在谈论纯自动布局方案。我很想知道您的layoutSubviews方法中到底发生了什么。
卡梅隆·洛厄尔·帕尔默

translationsAutoresizingMaskIntoConstraints = NO; 如果要纯自动布局,则绝对应禁用此功能。
卡梅伦·洛厄尔·帕尔默

我还没有看到,但是如果没有一些代码,我就无法真正解决这个问题。也许您可以模拟TableView并将其粘贴在GitHub上?
卡梅隆·洛厄尔·帕尔默

抱歉,我不使用gitHub :(
anneblue 2014年

74

通常,您只需要更新约束并layoutIfNeeded在动画块内调用即可。这可以是更改的.constant属性NSLayoutConstraint,添加删除约束(iOS 7)或更改.active约束的属性(iOS 8和9)。

样例代码:

[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项目,因此示例动画项目。

争议

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

以下是教授iOS的马丁·皮尔金顿(Martin Pilkington)和编写《自动版图》的Ken Ferry之间的Twitter对话。肯(Ken)解释说,尽管当前可以在动画块之外更改常量,但是这并不安全,应该在动画块进行更改。 https://twitter.com/kongtomorrow/status/440627401018466305

动画:

样例项目

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


+1用于显示使用新的活动标记的示例。此外,在动画块外更改约束总是让我感到不舒服
Korey Hinton

我在github中提出了一个问题,因为此解决方案无法将视图移回原处。我认为更改活动状态不是正确的解决方案,您应该更改优先级。将顶部设置为750,将底部设置为250,然后在代码中在UILayoutPriorityDefaultHigh和UILayoutPriorityDefaultLow之间交替。
malhal '16

在块内进行更新的另一个原因是,它将更改与调用代码隔离开来,而这些代码可能也会调用layoutIfNeeded。(并感谢Twitter链接)
克里斯·康诺夫

1
好答案。特别是因为您提到了Martin和Ken关于此事的对话。
乔纳

我对更新约束的方式感到困惑,并撰写了这篇文章。你能看看吗?
蜂蜜

36
// 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];
}];

29

Swift 4解决方案

UIView.animate

三个简单步骤:

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

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

更改约束和启动动画器的顺序很重要-如果我们仅更改约束并将动画器留给以后使用,下一个重绘周期可以调用自动布局重新计算,并且更改不会生效。

另外,请记住,单个动画师是不可重用的-一旦运行它,就无法“重新运行”它。因此,我认为没有足够的理由保留动画师,除非我们将其用于控制​​交互式动画。


in在swift 5上也表现出色
spnkr

layoutIfNeeded()是关键
Medhi

15

故事板,代码,技巧和一些陷阱

其他答案也很好,但是这个例子使用最近的例子强调了一些非常重要的动画约束技巧。在意识到以下几点之前,我经历了很多变化:

将要定位的约束放入Class变量中,以保留强引用。在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!
}()

一些实验后,我注意到,一个必须获得从视图约束ABOVE(又名上海华),其中该约束所限定的两个视图。在下面的示例中(MNGStarRating和UIWebView都是我要在它们之间创建约束的两种类型的项目,它们都是self.view中的子视图)。

过滤链

我利用Swift的filter方法来分离所需的约束,该约束将用作拐点。一个人也可能变得更加复杂,但是过滤器在这里做得很好。

使用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. 总是花一点时间阅读文档(新旧),快速帮助和标题。Apple不断进行大量更改以更好地管理AutoLayout约束(请参阅堆栈视图)。或至少是AutoLayout Cookbook。请记住,有时最好的解决方案是在较早的文档/视频中。

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

  4. 不要将特定的布局值硬编码为确定对其他常量进行更改的标准,而应使用允许您确定视图位置的值。CGRectContainsRect是一个例子

  5. 如果需要,请不要犹豫地使用与参与约束定义的视图关联的布局边距 let viewMargins = self.webview.layoutMarginsGuide:上的例子
  6. 不要做您不需要做的工作,所有在情节提要上受约束的视图都将约束附加到属性self.viewName.constraints
  7. 将所有约束的优先级更改为小于1000。我在情节提要上将我的优先级设置为250(低)或750(高)。(如果您尝试将代码中的1000优先级更改为任何优先级,则该应用将崩溃,因为需要1000)
  8. 考虑不要立即尝试使用activateConstraints和deactivateConstraints(它们有其位置,但是当刚学习或如果您使用情节提要时,使用这些可能意味着您做得太多了-尽管它们确实有位置,如下所示)
  9. 除非您确实在代码中添加了新约束,否则请考虑不使用addConstraints / removeConstraints。我发现,大多数情况下,我会使用所需的约束在情节提要中布局视图(将视图放置在屏幕外),然后在代码中,对先前在情节提要中创建的约束进行动画处理以移动视图。
  10. 我花了很多时间在新的NSAnchorLayout类和子类上建立约束。这些工作很好,但是我花了一段时间才意识到我需要的所有约束已经存在于情节提要中。如果您在代码中构建约束,那么肯定可以使用此方法来聚合约束:

使用情节提要时AVOID解决方案的快速样本

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),则很可能什么都不会发生:在这种情况下,您可能会遇到这样的问题:

注意:请花一点时间阅读下面的“自动版式”部分和原始指南。有一种方法可以使用这些技术来补充动态动画师。

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的早期视频

密切关注

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

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



6

工作解决方案100% Swift 3.1

我已经阅读了所有答案,并希望共享我在所有应用程序中使用过的代码和行的层次结构以使它们正确地进行动画处理。此处的某些解决方案不起作用,您现在应该在速度较慢的设备(例如iPhone 5)上检查它们。

self.view.layoutIfNeeded() // Force lays of all subviews on root view
UIView.animate(withDuration: 0.5) { [weak self] in // allowing to ARC to deallocate it properly
       self?.tbConstraint.constant = 158 // my constraint constant change
       self?.view.layoutIfNeeded() // Force lays of all subviews on root view again.
}

4

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

其他答案在说的完全是对的:您需要在[self.view layoutIfNeeded];内部 打电话animateWithDuration: animations:。但是,另一个重要的一点是要为每个NSLayoutConstraint要设置动画的指针。

我在GitHub中创建了一个示例


4

使用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而不是self.calendarView上调用了.layoutIfNeeded()

希望能有所帮助。


3

有一篇文章对此进行了讨论:http : //weblog.sivecode.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];
        }];
    }
}

希望能帮助到你。


1

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

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

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

在我看来,打开键盘可能会导致一些其他布局。添加一个具有10ms延迟的简单dispatch_after块使动画每次都运行-不会跳跃。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.