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

时间:2012-09-27 13:23:06

标签: ios objective-c animation autolayout ios6

我正在使用AdBannerView更新旧应用,当没有广告时,它会从屏幕上滑落。当有广告时,它会在屏幕上滑动。基本的东西。

旧样式,我在动画块中设置框架。 新样式,我有一个IBOutlet自动布局约束,它决定了Y位置,在这种情况下是它与superview底部的距离,并修改常量:

- (void)moveBannerOffScreen {
    [UIView animateWithDuration:5 animations:^{
        _addBannerDistanceFromBottomConstraint.constant = -32;
    }];
    bannerIsVisible = FALSE;
}

- (void)moveBannerOnScreen {
    [UIView animateWithDuration:5 animations:^{
        _addBannerDistanceFromBottomConstraint.constant = 0;
    }];
    bannerIsVisible = TRUE;
}

横幅移动,完全符合预期,但没有动画。

更新:我重新观看WWDC 12 talk Best Practices for Mastering Auto Layout,其中包含动画。它讨论了如何使用 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;
}

在旁注中,我已经多次检查过,这是在线程上执行的。

13 个答案:

答案 0 :(得分:1603)

两个重要的注释:

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

  2. 您需要专门在父视图(例如self.view)上调用它,而不是附加了约束的子视图。这样做会更新所有约束视图,包括动画可能被约束到您更改约束的视图的其他视图(例如,视图B附加到视图A的底部,您刚刚更改了视图A的顶部偏移,您希望视图B使用它进行动画处理)

  3. 试试这个:

    <强>目标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;
    }
    

    Swift 3

    UIView.animate(withDuration: 5) {
        self._addBannerDistanceFromBottomConstraint.constant = 0
        self.view.layoutIfNeeded()
    }
    

答案 1 :(得分:104)

我很欣赏所提供的答案,但我认为接受它会更好。

文档中的基本块动画

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

在UIView子类中重写updateConstraints方法,并且必须在方法结束时调用super。

- (void)updateConstraints
{
    // Update some constraints

    [super updateConstraints];
}

The AutoLayout Guide还有很多不足之处,但值得一读。我自己正在使用它作为UISwitch的一部分,用一对UITextField来切换子视图,其中包含一个简单而细微的折叠动画(0.2秒长)。如上所述,子视图的约束正在UIView子类updateConstraints方法中处理。

答案 2 :(得分:65)

通常,您只需要更新约束并在动画块中调用layoutIfNeeded。这可以是更改.constant的{​​{1}}属性,添加删除约束(iOS 7),也可以更改约束的NSLayoutConstraint属性(iOS 8&amp; 9)。

示例代码:

.active

样本设置:

Xcode Project so sample animation project.

关于是否应该在 动画块之前更改 ,或者 之内,存在一些问题(见前面的答案)。

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

动画:

示例项目

这是一个简单的项目,展示了如何动画视图。它使用Objective C并通过更改几个约束的[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]; }]; 属性来动画化视图。 https://github.com/shepting/SampleAutoLayoutAnimation

答案 3 :(得分:33)

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

答案 4 :(得分:20)

Swift 4解决方案

<强> UIView.animate

三个简单的步骤:

  1. 更改约束,例如:

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

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

    UIView.animate(withDuration: 0.5) {
        self.view.layoutIfNeeded()
    }
    
  4. 完整最简单的例子:

    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是动画的封装,我们可以继续引用它并稍后调用它。但是,由于在动画块中我们只是告诉autolayout重新计算帧,我们必须在调用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()
    

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

    此外,请记住,单个动画师是不可重复使用的 - 一旦你运行它,你就不能重新运行&#34;它。所以我想除非我们用它来控制交互式动画,否则没有什么理由让动画师保持不变。

答案 5 :(得分:12)

故事板,代码,提示和一些问题

其他答案都很好,但是这个答案突出了一些使用最近的例子制作动画约束的相当重要的问题。在我意识到以下内容之前,我经历了很多变化:

将要定位的约束设置为类变量以保存强引用。在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的过滤方法来分离将用作拐点的所需约束。人们也可能会变得更复杂,但过滤器在这里做得很好。

使用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类和子类。这些工作得很好,但它 我花了一段时间才意识到我需要的所有限制 已经存在于故事板中。如果在代码中构建约束 那么肯定会使用这种方法来聚合你的约束:
  11. 使用故事板时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的位置,则很可能不会发生任何事情:在这种情况下,您可能会有一个半假的解决方案:

      

    注意 - 请花点时间阅读下面的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
            }
    
        }
    

    AutoLayout指南中的代码段(请注意第二个代码段用于使用OS X)。 BTW - 就我所见,目前的指南已不再适用。首选技术不断发展。

    动画显示自动布局所做的更改

    如果您需要完全控制自动布局所做的动画更改,则必须以编程方式更改约束。 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];
    

    对于那些在视觉上学得更好的人,请查看早期video from Apple

    密切注意

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

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

答案 6 :(得分:6)

快速解决方案:

yourConstraint.constant = 50
UIView.animateWithDuration(1, animations: yourView.layoutIfNeeded)

答案 7 :(得分:5)

工作解决方案100% Swift 3.1

我已经阅读了所有的答案,并希望分享我在我的所有应用程序中使用的行的代码和层次结构,以便正确地设置它们的动画。这里的一些解决方案不起作用,你应该在较慢的设备上检查它们,例如iPhone 5 at这一刻。

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.
}

答案 8 :(得分:4)

我试图为Constraints制作动画,并不是很容易找到一个好的解释。

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

I created an example in GitHub

答案 9 :(得分:3)

使用Xcode 8.3.3为Swift 3工作且刚刚测试过的解决方案:

exit_group

请记住,self.calendarViewHeight是一个引用customView(CalendarView)的约束。我在self.view上调用了.layoutIfNeeded()而在self.calendarView上调用了

希望得到这个帮助。

答案 10 :(得分: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];
        }];
    }
}

希望它有所帮助。

答案 11 :(得分:1)

在约束动画的上下文中,我想提一下我在keyboard_opened通知中立即为约束设置动画的特定情况。

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

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

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

答案 12 :(得分:-1)

对于Xamarian同伴,这是Xamarin.iOS / C#版本:

UIView.Animate(5, () =>
{
    _addBannerDistanceFromBottomConstraint.Constant = 0;
    View.LayoutIfNeeded();
});