Как мне анимировать изменения ограничений?
Я обновляю старое приложение с 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 " Best Practices для освоения автоматической компоновки", которое охватывает анимацию. Здесь обсуждается, как обновить ограничения с помощью 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 ответов
Два важных замечания:
Вам нужно позвонить
layoutIfNeeded
в блоке анимации. Apple фактически рекомендует вызывать его один раз перед блоком анимации, чтобы убедиться, что все ожидающие операции макета были завершеныВы должны вызывать его конкретно в родительском представлении (например,
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;
}
Свифт 3
UIView.animate(withDuration: 5) {
self._addBannerDistanceFromBottomConstraint.constant = 0
self.view.layoutIfNeeded()
}
Я ценю предоставленный ответ, но я думаю, что было бы неплохо пойти еще дальше.
Основной блок анимации из документации
[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
}];
но на самом деле это очень упрощенный сценарий. Что делать, если я хочу анимировать ограничения subview через 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
что переключает подпредставление с парой UITextField
с простой и тонкой анимацией коллапса (0,2 секунды). Ограничения для подпредставления обрабатываются в методах updateConstraints подклассов UIView, как описано выше.
Как правило, вам просто нужно обновить ограничения и вызвать 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];
}];
Пример настройки:
полемика
Есть несколько вопросов о том, следует ли изменить ограничение перед блоком анимации или внутри него (см. Предыдущие ответы).
Ниже приводится беседа в Твиттере между Мартином Пилкингтоном, который преподает iOS, и Кеном Ферри, который написал Auto Layout. Кен объясняет, что хотя изменение констант за пределами блока анимации может в настоящее время работать, это небезопасно, и их действительно следует изменять внутри блока анимации. https://twitter.com/kongtomorrow/status/440627401018466305
Анимация:
Пример проекта
Вот простой проект, показывающий, как можно анимировать вид. Он использует Objective C и оживляет вид, изменяя .active
свойство нескольких ограничений. https://github.com/shepting/SampleAutoLayoutAnimation
// 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];
}];
Swift 4 решение
Три простых шага:
Измените ограничения, например:
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()
}
Примечание
Существует необязательный 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()
}
Так как с 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()
Порядок изменения ограничений и запуска аниматора важен - если мы просто изменим ограничения и оставим наш аниматор для более поздней точки, следующий цикл перерисовки может вызвать пересчет автоматического размещения, и изменение не будет анимированным.
Кроме того, помните, что один аниматор не предназначен для повторного использования - как только вы запустите его, вы не сможете его "перезапустить". Так что я думаю, что на самом деле нет веской причины держать аниматора рядом, если только мы не используем его для управления интерактивной анимацией.
Быстрое решение:
yourConstraint.constant = 50
UIView.animateWithDuration(1, animations: yourView.layoutIfNeeded)
Раскадровка, код, советы и несколько ошибок
Другие ответы просто хороши, но этот выдвигает на первый план несколько довольно важных моментов анимации ограничений. Я прошел через множество вариаций, прежде чем понял следующее:
Установите ограничения, на которые вы хотите нацелить переменные класса, чтобы они содержали сильную ссылку. В 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
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
}
}
}
Много неправильных поворотов
Эти заметки действительно набор советов, которые я написал для себя. Я сделал все, что не делал лично и мучительно. Надеюсь, это руководство может пощадить других.
Остерегайтесь zPosition. Иногда, когда ничего не происходит, вам следует скрыть некоторые другие виды или использовать отладчик, чтобы найти анимированный вид. Я даже нашел случаи, когда определяемый пользователем атрибут времени выполнения терялся в xml раскадровки и приводил к покрытию анимированного представления (во время работы).
Всегда уделяйте минуту чтению документации (новой и старой), быстрой справки и заголовков. Apple продолжает вносить множество изменений, чтобы лучше управлять ограничениями AutoLayout (см. Стековые представления). Или, по крайней мере, поваренная книга AutoLayout. Имейте в виду, что иногда лучшие решения находятся в старой документации / видео.
Поиграйте со значениями в анимации и рассмотрите возможность использования других вариантов animateWithDuration.
Не указывайте конкретные значения макета в качестве критериев для определения изменений других констант, вместо этого используйте значения, которые позволяют вам определить местоположение представления.
CGRectContainsRect
это один пример- При необходимости не стесняйтесь использовать поля макета, связанные с представлением, участвующим в определении ограничения.
let viewMargins = self.webview.layoutMarginsGuide
: находится на примере - Не выполняйте работу, которую не нужно делать, все представления с ограничениями на раскадровке имеют ограничения, связанные с свойством self.viewName.constraints
- Измените ваши приоритеты для любых ограничений на менее чем 1000. Я установил свой на 250 (низкий) или 750 (высокий) на раскадровке; (если вы попытаетесь изменить приоритет 1000 на что-либо в коде, приложение завершится сбоем, потому что требуется 1000)
- Не стоит сразу пытаться использовать activConstraints и de activateConstraints (они имеют свое место, но когда вы просто учитесь или если вы используете раскадровку, используя их, вероятно, это означает, что вы делаете слишком много - у них действительно есть место, хотя, как показано ниже)
- Не используйте addConstraints / removeConstraints, если вы действительно не добавляете новое ограничение в коде. Я обнаружил, что в большинстве случаев я размещаю представления в раскадровке с желаемыми ограничениями (помещая представление за пределы экрана), затем в коде я анимирую ограничения, ранее созданные в раскадровке, для перемещения представления.
- Я потратил много времени на создание ограничений с помощью нового класса и подклассов 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, скорее всего, ничего не произойдет: в этом случае у вас может получиться полуобжаренное решение, подобное следующему
NB. Найдите минутку, чтобы прочитать раздел 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.
Обратить пристальное внимание
Часто в документации есть небольшие заметки или фрагменты кода, которые приводят к большим идеям. Например, присоединение ограничений автоматического размещения к динамическим аниматорам - большая идея.
Удачи и да пребудет с тобой Сила.
Рабочий раствор 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.
}
Я пытался оживить ограничения, и мне было нелегко найти хорошее объяснение.
То, что говорят другие ответы, совершенно верно: вам нужно позвонить [self.view layoutIfNeeded];
внутри animateWithDuration: animations:
, Тем не менее, другой важный момент заключается в том, чтобы указатели для каждого NSLayoutConstraint
Вы хотите оживить.
Рабочее и только что протестированное решение для Swift 3 с Xcode 8.3.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). Я вызвал.layoutIfNeeded() для self.view, а НЕ для self.calendarView
Надеюсь, это поможет.
Есть статья об этом: 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.
Мне не удалось добиться согласованной плавной анимации ограничений непосредственно в уведомлении клавиатуры. Примерно в половине случаев изображение просто перейдет на новую позицию - без анимации.
Мне пришло в голову, что в результате открытия клавиатуры может возникнуть дополнительная раскладка. Добавление простого блока dispatch_after с задержкой в 10 мс заставляло анимацию запускаться каждый раз - без прыжков.
Для поддерживающих Xamarians, вот версия Xamarin.iOS/C#:
UIView.Animate(5, () =>
{
_addBannerDistanceFromBottomConstraint.Constant = 0;
View.LayoutIfNeeded();
});