ios - 我如何動畫約束更改?




animation ios6 (8)

故事板,代碼,技巧和幾個問題

其他答案都很好,但是這個例子使用最近的一個例子突出了一些相當重要的動畫約束問題。 在我意識到以下情況之前,我經歷了很多變化:

將要定位的約束條件放入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 (也就是superview)中獲取約束條件約束的兩個視圖。 在下面的示例中(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食譜 。 請記住,有時最好的解決方案是在較舊的文檔/視頻中。

  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的位置,那麼很可能什麼事情都不會發生:在這種情況下,您可能會有這樣一個半熟的解決方案:

注意 - 花點時間閱讀下面的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)。 順便說一句 - 據我所知,這已不在當前指南中。 首選技術不斷發展。

動畫製作自動佈局所做的更改

如果您需要完全控制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];

對於那些更好地視覺學習的人來說,請查看Apple的這個早期視頻

密切關注

通常在文檔中有小筆記或代碼片斷,這些都會導致更大的想法。 例如,將自動佈局約束附加到動態動畫師是一個很大的想法。

祝你好運,願軍隊與你同在。

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

橫幅的移動與預期完全相同,但沒有動畫。

更新:我重新觀看了涵蓋動畫的WWDC12視頻“ 掌握自動佈局的最佳實踐 ”。 它討論瞭如何使用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;
}

在附註中,我已經檢查了很多次,並且正在主線程上執行。


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

由於animatoranimator的封裝,我們可以繼續參考並稍後調用它。 但是,由於在動畫塊中我們只是告訴自動佈局來重新計算幀,我們必須在調用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()

改變約束和啟動動畫的順序非常重要 - 如果我們只是改變約束並且讓我們的動畫製作人員稍後再進行操作,那麼下一個重繪週期可以調用自動佈局重新計算,並且這種改變不會被動畫化。

另外,請記住,一個動畫師是不可重用的 - 一旦你運行它,你不能“重新運行”它。 所以我猜想,除非我們用它來控制交互式動畫,否則沒有什麼好的理由讓動畫師保持活躍。


使用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上調用了該函數

希望這個幫助。


兩個重要提示:

  1. 您需要在動畫塊中調用layoutIfNeeded 。 Apple實際上建議您在動畫塊之前調用一次,以確保所有掛起的佈局操作都已完成

  2. 你需要在父視圖中專門調用它(例如self.view ),而不是具有附加約束的子視圖。 這樣做會更新所有受限制的視圖,包括動畫其他視圖,這些視圖可能會受限於您更改約束的視圖(例如,視圖B附加到視圖A的底部,並且您剛剛更改視圖A的頂部偏移並且您希望視圖B用它製作動畫)

嘗試這個:

Objective-C的

- (void)moveBannerOffScreen {
    [self.view layoutIfNeeded];

    _addBannerDistanceFromBottomConstraint.constant = -32;
    [UIView animateWithDuration:5
        animations:^{
            [self.view layoutIfNeeded]; // Called on parent view
        }];
    bannerIsVisible = FALSE;
}

- (void)moveBannerOnScreen { 
    [self.view layoutIfNeeded];

    _addBannerDistanceFromBottomConstraint.constant = 0;
    [UIView animateWithDuration:5
        animations:^{
            [self.view layoutIfNeeded]; // Called on parent view
        }];
    bannerIsVisible = TRUE;
}

斯威夫特3

_addBannerDistanceFromBottomConstraint.constant = 0

UIView.animate(withDuration: 5) {
    self.view.layoutIfNeeded()
}

我試圖讓Constraints動畫,並且很難找到一個好的解釋。

還有什麼其他答案是完全正確的:你需要調用[self.view layoutIfNeeded];animateWithDuration: animations:裡面animateWithDuration: animations: 。 然而,另一個重要的點是要為每個你想要動畫的NSLayoutConstraint指針。

我在GitHub中創建了一個例子


我非常感謝所提供的答案,但我認為這樣做會更好一點。

文檔中的基本塊動畫

[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指南留下了許多不足之處,但值得一讀。 我自己使用這個作為一個UISwitch一部分,它使用一對簡單的細微折疊動畫(0.2秒長)切換一對UITextField的子視圖。 如上所述,子視圖的約束正在UIView子類的updateConstraints方法中處理。


迅捷解決方案:

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

通常,您只需更新約束並在動畫塊內調用layoutIfNeeded 。 這可以是更改.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];
}];

樣本設置:

爭議

關於約束是應該在動畫塊之前還是內部進行更改存在一些問題(請參閱前面的答案)。

以下是教授iOS的Martin Pilkington和撰寫Auto Layout的Ken Ferry之間的Twitter對話。 Ken解釋說,儘管改變動畫塊之外的常量現在可以工作,但這並不安全,他們應該真的在動畫塊內部進行更改。 https://twitter.com/kongtomorrow/status/440627401018466305

動畫:

示例項目

這裡有一個簡單的項目,展示了一個視圖可以如何動畫。 它使用Objective C並通過更改多個約束的.active屬性來為視圖添加動畫。 https://github.com/shepting/SampleAutoLayoutAnimation





autolayout