UIViewPropertyAnimator проблема с Autolayout
Вот код того, что я пытался повторить в соответствии с Apple WWDC, но с автопоставкой:
extension AugmentedReallityViewController {
@objc func handlePan(recognizer: UIPanGestureRecognizer) {
// // hide languages and units anyway
// moveUnitView(show: false)
// moveLanguageView(show: false)
//
// let isNowExpanded = settingsPanelState == SettingsPanelState.expanded
// let newState = isNowExpanded ? SettingsPanelState.collapsed : SettingsPanelState.expanded
//
// switch recognizer.state {
// case .began:
// startInteractiveTransition(state: newState, duration: 1)
// isLastPanelUpdateToReachTheNewState = true // just in case, but we should change this property later
// case .changed:
// let translation = recognizer.translation(in: viewSettings)
// let fractionComplete = translation.y / viewSettings.frame.size.height
//
// // we will use this property when interaction ends
// if fractionComplete != 0 { // if it's == 0 , we need to use prev data
// isLastPanelUpdateToReachTheNewState = (newState == SettingsPanelState.expanded && fractionComplete < 0) || (newState == SettingsPanelState.collapsed && fractionComplete > 0)
// }
//
// updateInteractiveTransition(fractionComplete: fractionComplete)
// case .ended:
// continueInteractiveTransition(cancel: !isLastPanelUpdateToReachTheNewState)
// default:
// break
// }
}
@objc func handleSettingsTap() {
// hide languages and units anyway
moveUnitView(show: false)
moveLanguageView(show: false)
let isNowExpanded = settingsPanelState == SettingsPanelState.expanded
let newState = isNowExpanded ? SettingsPanelState.collapsed : SettingsPanelState.expanded
animateOrReverseRunningTransition(state: newState, duration: 10)
}
// perform all animations with animators if not already running
private func animateTransitionIfNeeded(state: SettingsPanelState, duration: TimeInterval) {
if runningAnimators.isEmpty {
// // define constraint for frame animation
// // update constraints
// switch state {
// case .expanded:
// constraint_settingsView_bottom.constant = 0
// case .collapsed:
// constraint_settingsView_bottom.constant = -constraint_height_settingViewWhitePart.constant
// }
// animate that
let frameAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear, animations: { [weak self] in
if let strongSelf = self {
// define constraint for frame animation
// update constraints
switch state {
case .expanded:
strongSelf.constraint_settingsView_bottom.constant = 0
case .collapsed:
strongSelf.constraint_settingsView_bottom.constant = -(strongSelf.constraint_height_settingViewWhitePart.constant)
}
}
self?.view.layoutIfNeeded()
})
frameAnimator.startAnimation()
runningAnimators.append(frameAnimator)
frameAnimator.addCompletion({ [weak self] (position) in
if position == UIViewAnimatingPosition.end { // need to remove this animator from array
if let index = self?.runningAnimators.index(of: frameAnimator) {
print("removed animator because of completion")
self?.runningAnimators.remove(at: index)
// we can change state to a new one
self?.settingsPanelState = state
}
else {
print("animator completion with state = \(position)")
}
}
})
}
}
// starts transition if neccessary or reverses it on tap
private func animateOrReverseRunningTransition(state: SettingsPanelState, duration: TimeInterval) {
if runningAnimators.isEmpty { // start transition from start to end
animateTransitionIfNeeded(state: state, duration: duration)
}
else { // reverse all animators
for animator in runningAnimators {
animator.stopAnimation(true)
animator.isReversed = !animator.isReversed
// test
print("tried to reverse")
}
}
}
// called only on pan .begin
// starts transition if neccessary and pauses (on pan .begin)
private func startInteractiveTransition(state: SettingsPanelState, duration: TimeInterval) {
animateTransitionIfNeeded(state: state, duration: duration)
for animator in runningAnimators {
animator.pauseAnimation()
// save progress of any item
progressWhenInterrupted = animator.fractionComplete
}
}
// scrubs transition on pan .changed
private func updateInteractiveTransition(fractionComplete: CGFloat) {
for animator in runningAnimators {
animator.fractionComplete = fractionComplete + progressWhenInterrupted
}
}
// continue or reverse transition on pan .ended
private func continueInteractiveTransition(cancel: Bool) {
for animator in runningAnimators {
// need to continue or reverse
if !cancel {
let timing = UICubicTimingParameters(animationCurve: .easeOut)
animator.continueAnimation(withTimingParameters: timing, durationFactor: 0)
}
else {
animator.isReversed = true
}
}
}
private func addPanGustureRecognizerToSettings() {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(AugmentedReallityViewController.handlePan(recognizer:)))
// panGestureRecognizer.cancelsTouchesInView = false
viewSettings.addGestureRecognizer(panGestureRecognizer)
}
private func addTapGestureRecognizerToSettings() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(AugmentedReallityViewController.handleSettingsTap))
tapGestureRecognizer.cancelsTouchesInView = false
viewSettingsTopTriangle.addGestureRecognizer(tapGestureRecognizer)
}
}
Сейчас я просто тестирую жесты. И есть 2 основных вопроса:
1) Распознаватель касаний не работает должным образом во время анимации. Но в Apple WWDC они меняли фреймы (а не ограничения, как в моем случае), и распознаватели касаний работали отлично
2) Если я изменяю обратное свойство, оно действительно очень плохо меняет ограничения. У меня есть лишние полоски и тд
3) Я попробовал оба способа поставить изменяющее ограничение перед блоком анимации и внутри. Это не имеет значения, работает так же
Любая помощь, как это сделать с autolayout? Или, по крайней мере, как это сделать с фреймами, но мой контроллер представления основан на автоматической компоновке, так что в любом случае у меня будут ограничения для этого нижнего представления.
1 ответ
Когда вы используете autolayout для анимации, вы делаете это следующим образом:
Убедитесь, что автопоставка выполнена:
self.view.layoutIfNeeded()
Затем вы изменяете ограничения ДО блока анимации. Так, например:
someConstraint.constant = 0
Затем после изменения ограничения вы сообщаете autolayout, что ограничения были изменены:
self.view.setNeedsLayout()
А затем вы добавляете блок анимации с простым вызовом
layoutIfNeeded()
:UIView.animate(withDuration: 1, animations: { self.view.layoutIfNeeded() })
То же самое относится, когда вы используете UIViewPropertyAnimator
- изменить ограничения в анимационном блоке. Например:
self.view.layoutIfNeeded()
someConstraint.constant = 0
self.view.setNeedsLayout()
let animator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
self.view.layoutIfNeeded()
}
animator.startAnimation()
Это происходит потому, что layoutIfNeeded()
делает фактическое расположение - это вычисляет кадры затронутых представлений. Поэтому, если вы устанавливаете кадры напрямую, вы устанавливаете их в блоке анимации. Тем не менее, Autolayout устанавливает кадры для вас - поэтому вам нужно указать autolayout установить их в блоке анимации (как вы бы сделали, если бы вы установили их напрямую). layoutIfNeeded()
call делает именно это - он говорит механизму автоматического размещения вычислять и устанавливать новые кадры.
Об обращении:
Хотя у меня нет достаточного опыта, чтобы быть уверенным на 100%, я ожидаю, что простой настройки аниматора для реверса будет недостаточно. Так как вы применяете ограничения перед запуском анимации, а затем просто указываете автоматической раскладке обновлять кадры в соответствии с ограничениями - я бы предположил, что при обратном обращении к аниматору вам также необходимо будет обратить вспять и ограничения, которые управляют анимацией.
Аниматор просто оживляет взгляды в новые кадры. Тем не менее, в обратном порядке или нет, новые ограничения остаются в силе независимо от того, изменили ли вы аниматор или нет. Поэтому после того, как аниматор завершит работу, если позднее в результате автоматического размещения снова появятся представления, я ожидаю, что представления перейдут в места, заданные в настоящее время активными ограничениями. Проще говоря: аниматор оживляет изменения кадра, но не ограничивает себя. Это означает, что реверсивный аниматор реверсирует кадры, но он не отменяет ограничения - как только autolayout выполнит другой цикл компоновки, они будут снова применены.
Важно установить self.view.layoutIfNeeded()
анимация случится
private func animateCard(with topOffset: CGFloat) {
let animator = UIViewPropertyAnimator(duration: 1, curve: .easeOut)
animator.addAnimations {
self.topCardConstraint?.constant = topOffset
self.view.layoutIfNeeded()
}
animator.startAnimation()
}