【问题标题】:UIViewPropertyAnimator issue with Autolayout自动布局的 UIViewPropertyAnimator 问题
【发布时间】:2018-03-17 06:16:04
【问题描述】:

这是我尝试根据 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)
    }
}

现在我只是在测试点击手势。有两个主要问题:

1) 点击识别器在动画期间无法正常工作。但是在苹果 WWDC 中,他们改变了框架(不是像我这样的限制)并且点击识别器工作得很好

2) 如果我更改反向属性,它会更改约束非常糟糕。我有多余的条带等等

3) 我尝试了两种方法来在动画块之前和内部放置更改约束。没关系,都一样

任何帮助如何使用自动布局做到这一点?或者至少如何使用框架来做到这一点,但我的视图控制器是基于自动布局的,所以无论如何我都会对这个底部视图有限制。

【问题讨论】:

    标签: ios iphone uiview core-animation uiviewpropertyanimator


    【解决方案1】:

    设置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()
        }
    

    【讨论】:

      【解决方案2】:

      当你对动画使用自动布局时,你可以这样做:

      1. 确保自动布局已完成:

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

        someConstraint.constant = 0
        
      3. 然后在更改约束后,告诉自动布局约束已更改:

        self.view.setNeedsLayout()
        
      4. 然后您只需调用 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() 执行实际布局 - 它计算受影响视图的框架。因此,如果您直接设置帧,则将它们设置在动画块中。但是,自动布局会为您设置帧 - 因此您需要告诉自动布局在动画块中设置它们(如果您直接设置它们,您会这样做)。 layoutIfNeeded() 调用正是这样做的 - 它告诉自动布局引擎计算和设置新帧。

      关于逆转:

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

      Animator 只是将视图动画化到新的帧中。但是,无论是否反转,无论您是否反转了动画师,新的约束仍然有效。因此,在动画师完成后,如果稍后自动布局再次布置视图,我希望视图会进入当前活动约束设置的位置。简单地说:动画师动画帧的变化,而不是约束本身。这意味着反转动画师会反转帧,但它不会反转约束 - 一旦自动布局执行另一个布局循环,它们就会再次应用。

      【讨论】:

      • 但是用这种方法点击手势识别器无论如何都不起作用,请尝试
      • 您有我可以尝试的示例项目吗?
      • 对不起,我没有,但我决定不担心动画期间的交互
      • 此方法在您激活/停用约束时也有效。非常有用。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-03-18
      相关资源
      最近更新 更多