首页 文章

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

提问于
浏览
865

我正在使用 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;
}

Banner 移动,完全符合预期,但没有动画 .

UPDATE: 我重新观看了WWDC12视频“Best Practices for Mastering Auto Layout”,其中包括动画 . 它讨论了如何使用 CoreAnimation 更新约束 .

enter image description here

enter image description here

我尝试使用以下代码,但得到完全相同的结果 .

- (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 回答

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

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

  • 101

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

    希望能帮助到你 .

  • 2

    通常,您只需更新约束并在动画块内调用 layoutIfNeeded . 这可以是更改 NSLayoutConstraint.constant 属性,添加删除约束(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];
    }];
    

    样本设置:

    争议

    关于约束是否应该更改动画块或 inside (参见前面的答案),有一些问题 .

    以下是教授iOS的Martin Pilkington和编写Auto Layout的Ken Ferry之间的Twitter对话 . Ken解释说,尽管在动画块 may currently 之外更改常量,但它并不安全,它们应该真正改变动画块 . https://twitter.com/kongtomorrow/status/440627401018466305

    动画:

    示例项目

    这里's a simple project showing how a view can be animated. It'使用Objective C并通过更改几个约束的 .active 属性来动画化视图 . https://github.com/shepting/SampleAutoLayoutAnimation

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

    Swift 4 solution

    UIView.animate

    三个简单的步骤:

    • 更改约束,例如:
    heightAnchor.constant = 50
    
    • 告诉包含 view 它的布局是否脏,并且autolayout应该重新计算布局:
    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()
    }
    

    Sidenote

    有一个可选的第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()
    

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

    另外,请记住,单个动画师是不可重复使用的 - 一旦你运行它,就不能“重新运行”它 . 所以我想除非我们用它来控制交互式动画,否则没有一个很好的理由让动画师保持不变 .

  • 1552

    Working Solution 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.
    }
    
  • 6

    两个重要说明:

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

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

    试试这个:

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

    Swift 3

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

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

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

    将要定位的约束设置为类变量以保留强引用 . 在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中的子视图) .

    Filter Chaining

    我利用Swift的滤波器方法来分离将作为拐点的所需约束 . 人们也可能会变得更加复杂,但过滤器在这里做得很好 .

    Animating Constraints Using 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中丢失了用户定义的运行时属性并导致动画视图被覆盖(工作时)的情况 .

    • 总是花一点时间阅读文档(新旧),快速帮助和 Headers . Apple不断进行大量更改以更好地管理AutoLayout约束(请参阅堆栈视图) . 或者至少AutoLayout Cookbook . 请记住,有时最好的解决方案是在较旧的文档/视频中 .

    • 使用动画中的值并考虑使用其他animateWithDuration变体 .

    • 不要将特定布局值硬编码为确定其他常量更改的条件,而是使用允许您确定视图位置的值 . CGRectContainsRect 就是一个例子

    • 如果需要,请不要犹豫使用与参与约束定义的视图关联的布局边距 let viewMargins = self.webview.layoutMarginsGuide :是示例

    • Don 't do work you don' t必须这样做,所有带有约束的故事板视图都附加了属性self.viewName.constraints

    • 将任何约束的优先级更改为小于1000.我在故事板上将我的设置设置为250(低)或750(高); (如果您尝试将1000优先级更改为代码中的任何内容,则应用程序将崩溃,因为需要1000)

    • 考虑不要立即尝试使用activateConstraints和deactivateConstraints(他们有自己的位置,但只是在学习或者如果你正在使用故事板,这可能意味着你做得太多了〜虽然如下所示,他们确实有一个地方)

    • 除非您真的在代码中添加新约束,否则请考虑不使用addConstraints / removeConstraints . 我发现大多数时候我在故事板中布置了具有所需约束的视图(将视图放在屏幕外),然后在代码中,我为先前在故事板中创建的约束设置动画以移动视图 .

    • 我浪费了很多时间来 Build 新的NSAnchorLayout类和子类的约束 . 这些工作很好但我花了一段时间才意识到我需要的所有约束已经存在于故事板中 . 如果在代码中构建约束,那么大多数肯定会使用此方法来聚合约束:

    Quick Sample Of Solutions to AVOID when using Storyboards

    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 - This is no longer in the current guide as far as I can see. 首选技术不断发展 .

    Animating Changes Made by Auto Layout 如果您需要完全控制自动布局所做的动画更改,则必须以编程方式更改约束 . 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 .

    关闭注意力

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

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

  • 5

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

    其他答案是完全正确的:你需要在 animateWithDuration: animations: 内调用 [self.view layoutIfNeeded]; . 但是,另一个重点是为每个想要动画的 NSLayoutConstraint 指针 .

    I created an example in GitHub .

  • 0

    Swift解决方案:

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

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

    希望这有帮助 .

  • 1

    对于Xamarians同事,这里是Xamarin.iOS / C#版本:

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

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

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

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

    它发生在我身上可能会因键盘打开而发生一些额外的布局 . 添加一个延迟10毫秒的简单dispatch_after块使动画每次都运行 - 没有跳跃 .

相关问题