Autolayout的UIViewPropertyAnimator问题

以下是我根据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) // } // // 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) } } 

现在我只是测试轻拍手势。 并且有两个主要问题:

1)在动画期间,Tap识别器无法正常工作。 但是在苹果WWDC中,他们改变了帧(不是像我的情况那样的约束),并且点击识别器工作得很好

2)如果我改变反向属性,那么改变约束确实非常糟糕。 我有额外的条带等等

3)我尝试了两种方法在动画块和内部之前放置更改约束。 它并不重要,工作原理相同

任何帮助如何使用autolayout做到这一点? 或者至少如何使用框架,但我的视图控制器基于自动布局,所以无论如何我将对此底部视图有约束。

当您使用autolayout进行动画制作时,请执行以下操作:

  1. 确保自动布局完成:

     self.view.layoutIfNeeded() 
  2. 然后在动画块之前更改约束。 例如:

     someConstraint.constant = 0 
  3. 然后在更改约束后,告诉autolayout已更改约束:

     self.view.setNeedsLayout() 
  4. 然后添加一个动画块,只需调用layoutIfNeeded()

     UIView.animate(withDuration: 1, animations: { self.view.layoutIfNeeded() }) 

使用UIViewPropertyAnimatorUIViewPropertyAnimator – 更改动画块中的约束。 例如:

 self.view.layoutIfNeeded() someConstraint.constant = 0 self.view.setNeedsLayout() let animator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut) { self.view.layoutIfNeeded() } animator.startAnimation() 

发生这种情况是因为layoutIfNeeded()执行实际布局 – 它会计算受影响视图的帧。 因此,如果您直接设置框架,则在动画块中设置它们。 但是,Autolayout会为您设置帧 – 因此您需要告诉自动布局将它们设置在动画块中(如果您直接设置它们,就像那样)。 layoutIfNeeded()调用就是这样 – 它告诉autolayout引擎计算和设置新帧。

关于逆转:

虽然我没有足够的经验来100%肯定,但我希望简单地将动画师设置为反转是不够的。 由于您在开始动画之前应用约束,然后您只是告诉自动布局根据约束更新帧 – 我会假设当您反转动画师时,您还需要反转驱动动画的约束。

Animator只是将视图设置为新帧的动画。 然而,无论是否反转,无论你是否颠倒了动画师,新约束仍然存在。 因此,在动画师完成之后,如果稍后的自动布局再次布局视图,我会期望视图进入由当前活动约束设置的位置。 简单地说: 动画师动画帧变化,但不限制自身。 这意味着反转动画师会反转帧,但它不会反转约束 – 只要autolayout执行另一个布局周期,它们就会再次应用。