繁体   English   中英

动画的完成块立即执行

[英]Completion block of animation is performed immediately

我试图在完成块中的动画结束后从超级视图中删除自定义视图,但它会立即被调用并且动画变得清晰。 我设法以一种不太好的方式解决了这个问题:只是增加了一个延迟来删除视图。

这是动画视图的函数:

private func animatedHideSoundView(toRight: Bool) {
   let translationX = toRight ? 0.0 : -screenWidth
   UIView.animate(withDuration: 0.5) {
        self.soundView.transform = CGAffineTransform(translationX: translationX, y: 0.0)
   } completion: { isFinished in
        if isFinished {
            self.soundView.removeFromSuperview()
            self.songPlayer.pause()
        }
    }
}

这一行的问题: self.soundView.removeFromSuperview()

当我在 switch Recognizer.state 完成块语句中调用此函数时,它会提前执行,并且在其他地方一切正常时执行。

@objc private func soundViewPanned(recognizer: UIPanGestureRecognizer) {
        let touchPoint = recognizer.location(in: view)
        switch recognizer.state {
        case .began:
            initialOffset = CGPoint(x: touchPoint.x - soundView.center.x, y: touchPoint.y - soundView.center.y)
        case .changed:
            soundView.center = CGPoint(x: touchPoint.x - initialOffset.x, y: touchPoint.y - initialOffset.y)
            if notHiddenSoundViewRect.minX > soundView.frame.minX {
                animatedHideSoundView(toRight: false)
            } else if notHiddenSoundViewRect.maxX < soundView.frame.maxX {
                animatedHideSoundView(toRight: true)
            }
        case .ended, .cancelled:
            let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
            let velocity = recognizer.velocity(in: view)
            let projectedPosition = CGPoint(
                x: soundView.center.x + project(initialVelocity: velocity.x, decelerationRate: decelerationRate),
                y: soundView.center.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
            )
            let nearestCornerPosition = nearestCorner(to: projectedPosition)
            let relativeInitialVelocity = CGVector(
                dx: relativeVelocity(forVelocity: velocity.x, from: soundView.center.x, to: nearestCornerPosition.x),
                dy: relativeVelocity(forVelocity: velocity.y, from: soundView.center.y, to: nearestCornerPosition.y)
            )
            let timingParameters = UISpringTimingParameters(dampingRatio: 0.8, initialVelocity: relativeInitialVelocity)
            let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: timingParameters)
            animator.addAnimations {
                self.soundView.center = nearestCornerPosition
            }
            animator.startAnimation()
        default: break
        }
    }

我希望用户能够将此声音视图从屏幕上滑出。

这就是为什么我在用户移动 soundView 时检查它的位置,这样如果他将 soundView 移动到屏幕边缘附近,我可以动画地隐藏 soundView。

也许我做错了,但我想不出别的,因为我没有太多经验。 有人可以给我一些建议吗?

我设法以这种方式解决它,但我不喜欢它:

private func animatedHideSoundView(toRight: Bool) {
    let translationX = toRight ? 0.0 : -screenWidth
    UIView.animate(withDuration: 0.5) {
        self.soundView.transform = CGAffineTransform(translationX: translationX, y: 0.0)
    }        
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.soundView.removeFromSuperview()
        self.songPlayer.pause()
    }
}

在此处输入图像描述

您可以在此处查看并运行所有代码: https ://github.com/swiloper/AnimationProblem

情侣笔记...

首先,在您的控制器代码中,每次移动 touch 时,您都会从平移手势识别器调用animatedHideSoundView() 这不太可能是您想要做的。

其次,如果您调用animatedHideSoundView(toRight: true)您的代码:

private func animatedHideSoundView(toRight: Bool) {
   let translationX = toRight ? 0.0 : -screenWidth
   UIView.animate(withDuration: 0.5) {
        self.soundView.transform = CGAffineTransform(translationX: translationX, y: 0.0)
   } completion: { isFinished in
        if isFinished {
            self.soundView.removeFromSuperview()
            self.songPlayer.pause()
        }
    }
}

translationX设置Zero ...当您尝试为变换设置动画时,动画将不会花费时间,因为您没有更改x

第三,我强烈建议你从简单开始。 您链接到的代码无法复制/粘贴/运行,这使得提供帮助变得困难。

这是您的UniversalTypesViewController类的最小版本(它使用您链接的SoundView类):

final class UniversalTypesViewController: UIViewController {
    
    // MARK: Properties
    
    private lazy var soundView = SoundView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
    private let panGestureRecognizer = UIPanGestureRecognizer()
    private var initialOffset: CGPoint = .zero
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemYellow
        panGestureRecognizer.addTarget(self, action: #selector(soundViewPanned(recognizer:)))
        soundView.addGestureRecognizer(panGestureRecognizer)
    }
    
    private func animatedShowSoundView() {
        // reset soundView's transform
        soundView.transform = .identity
        // add it to the view
        view.addSubview(soundView)
        // position soundView near bottom, but past the right side of view
        soundView.frame.origin = CGPoint(x: view.frame.width, y: view.frame.height - soundView.frame.height * 2.0)
        soundView.startSoundBarsAnimation()

        // animate soundView into view
        UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut) {
            self.soundView.transform = CGAffineTransform(translationX: -self.soundView.frame.width * 2.0, y: 0.0)
        }
    }
    
    private func animatedHideSoundView(toRight: Bool) {
        let translationX = toRight ? view.frame.width : -(view.frame.width + soundView.frame.width)
        UIView.animate(withDuration: 0.5) {
            self.soundView.transform = CGAffineTransform(translationX: translationX, y: 0.0)
        } completion: { isFinished in
            if isFinished {
                self.soundView.removeFromSuperview()
                //self.songPlayer.pause()
            }
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // if soundView is not in the view hierarchy,
        //  animate it into view - animatedShowSoundView() func adds it as a subview
        if soundView.superview == nil {
            animatedShowSoundView()
        } else {
            // unwrap the touch
            guard let touch = touches.first else { return }
            // get touch location
            let loc = touch.location(in: self.view)
            // if touch is inside the soundView frame,
            //  return, so pan gesture can move soundView
            if soundView.frame.contains(loc) { return }
            // if touch is on the left-half of the screen,
            //  animate soundView to the left and remove after animation
            if loc.x < view.frame.midX {
                animatedHideSoundView(toRight: false)
            } else {
                // touch is on the right-half of the screen,
                //  so just remove soundView
                animatedHideSoundView(toRight: true)
            }
        }
    }
    // MARK: Objc methods
    
    @objc private func soundViewPanned(recognizer: UIPanGestureRecognizer) {
        let touchPoint = recognizer.location(in: view)
        switch recognizer.state {
        case .began:
            initialOffset = CGPoint(x: touchPoint.x - soundView.center.x, y: touchPoint.y - soundView.center.y)
        case .changed:
            soundView.center = CGPoint(x: touchPoint.x - initialOffset.x, y: touchPoint.y - initialOffset.y)
        case .ended, .cancelled:
            ()
        default: break
        }
    }
    
}

如果你运行它,点击任何地方都会将soundView动画化到右下角的视图中。 然后,您可以拖动soundView

如果您在soundView框架之外点击,在屏幕的左半部分soundView将被动画到左侧并在动画完成后被移除。

如果您在soundView框架之外点击,在屏幕的右半部分soundView将被动画到右侧并在动画完成后被移除。

一旦你完成了这项工作,并且你看到了正在发生的事情,你就可以在其余的更复杂的代码中实现它。


编辑

看看你的代码的这个修改版本。

您的代码中的一个大问题是您多次调用animatedHideSoundView() 当拖动接近边缘时,您的代码会调用它……但随后又会再次调用它,因为拖动仍然是“活动的”。

因此,我添加了一个var isHideAnimationRunning: Bool标志,因此在拖动时调用定位和在“隐藏”动画时定位不会发生冲突。

其他一些变化:

  • 而不是混合变换与.center定位,摆脱变换,只使用.center
  • 我创建了一个具有逻辑命名角点的结构 - 使引用它们变得更加容易
  • 强烈推荐:在你的代码中添加注释!

所以,试试这个:

import UIKit

let screenWidth: CGFloat = UIScreen.main.bounds.width
let screenHeight: CGFloat = UIScreen.main.bounds.height

let sideSpacing: CGFloat = 32.0
let mediumSpacing: CGFloat = 16.0

var isNewIphone: Bool {
    return screenHeight / screenWidth > 1.8
}

extension CGPoint {
    func distance(to point: CGPoint) -> CGFloat {
        return sqrt(pow(point.x - x, 2) + pow(point.y - y, 2))
    }
}

// so we can refer to corner positions by logical names
struct CornerPoints {
    var topLeft: CGPoint = .zero
    var bottomLeft: CGPoint = .zero
    var bottomRight: CGPoint = .zero
    var topRight: CGPoint = .zero
}

final class ViewController: UIViewController {

    private var cornerPoints = CornerPoints()
    
    private let soundViewSide: CGFloat = 80.0
    private lazy var halfSoundViewWidth = soundViewSide / 2
    
    private lazy var newIphoneSpacing = isNewIphone ? mediumSpacing : 0.0
    
    private lazy var soundView = SoundView(frame: CGRect(origin: .zero, size: CGSize(width: soundViewSide, height: soundViewSide)))
    
    private lazy var notHiddenSoundViewRect = CGRect(x: mediumSpacing, y: 0.0, width: screenWidth - mediumSpacing * 2, height: screenHeight)
    
    private var initialOffset: CGPoint = .zero
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .yellow

        // setup corner points
        let left = sideSpacing + halfSoundViewWidth
        let right = view.frame.maxX - (sideSpacing + halfSoundViewWidth)
        let top = sideSpacing + halfSoundViewWidth - newIphoneSpacing
        let bottom = view.frame.maxY - (sideSpacing + halfSoundViewWidth - newIphoneSpacing)
        
        cornerPoints.topLeft        = CGPoint(x: left, y: top)
        cornerPoints.bottomLeft     = CGPoint(x: left, y: bottom)
        cornerPoints.bottomRight    = CGPoint(x: right, y: bottom)
        cornerPoints.topRight       = CGPoint(x: right, y: top)

        let panGestureRecognizer = UIPanGestureRecognizer()
        panGestureRecognizer.addTarget(self, action: #selector(soundViewPanned(recognizer:)))
        soundView.addGestureRecognizer(panGestureRecognizer)
        
        // for development, let's add a double-tap recognizer to
        //  add the soundView again (if it's been removed)
        let dt = UITapGestureRecognizer(target: self, action: #selector(showAgain(_:)))
        dt.numberOfTapsRequired = 2
        view.addGestureRecognizer(dt)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.animatedShowSoundView()
        }
    }
    
    @objc func showAgain(_ f: UITapGestureRecognizer) {
        // if soundView has been removed
        if soundView.superview == nil {
            // add it
            animatedShowSoundView()
        }
    }
    
    private func animatedShowSoundView() {
        // start at bottom-right, off-screen to the right
        let pt: CGPoint = cornerPoints.bottomRight
        soundView.center = CGPoint(x: screenWidth + soundViewSide, y: pt.y)
        
        view.addSubview(soundView)
        soundView.startSoundBarsAnimation()
        
        // animate to bottom-right corner
        UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut) {
            self.soundView.center = pt
        }
    }
    
    // flag so we know if soundView is currently
    //  "hide" animating
    var isHideAnimationRunning: Bool = false
    
    private func animatedHideSoundView(toRight: Bool) {
        
        // only execute if soundView is not currently "hide" animating
        if !isHideAnimationRunning {
            // set flag to true
            isHideAnimationRunning = true
            
            // target center X
            let targetX: CGFloat = toRight ? screenWidth + soundViewSide : -soundViewSide
            
            UIView.animate(withDuration: 0.5) {
                self.soundView.center.x = targetX
            } completion: { isFinished in
                self.isHideAnimationRunning = false
                if isFinished {
                    self.soundView.removeFromSuperview()
                    //self.songPlayer.pause()
                }
            }
        }
        
    }
    
    @objc private func soundViewPanned(recognizer: UIPanGestureRecognizer) {
        let touchPoint = recognizer.location(in: view)
        switch recognizer.state {
        case .began:
            // only execute if soundView is not currently "hide" animating
            if !isHideAnimationRunning {
                initialOffset = CGPoint(x: touchPoint.x - soundView.center.x, y: touchPoint.y - soundView.center.y)
            }
        case .changed:
            // only execute if soundView is not currently "hide" animating
            if !isHideAnimationRunning {
                soundView.center = CGPoint(x: touchPoint.x - initialOffset.x, y: touchPoint.y - initialOffset.y)
                if notHiddenSoundViewRect.minX > soundView.frame.minX {
                    animatedHideSoundView(toRight: false)
                } else if notHiddenSoundViewRect.maxX < soundView.frame.maxX {
                    animatedHideSoundView(toRight: true)
                }
            }
        case .ended, .cancelled:
            // only execute if soundView is not currently "hide" animating
            if !isHideAnimationRunning {
                let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
                let velocity = recognizer.velocity(in: view)
                let projectedPosition = CGPoint(
                    x: soundView.center.x + project(initialVelocity: velocity.x, decelerationRate: decelerationRate),
                    y: soundView.center.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
                )
                let nearestCornerPosition = nearestCorner(to: projectedPosition)
                let relativeInitialVelocity = CGVector(
                    dx: relativeVelocity(forVelocity: velocity.x, from: soundView.center.x, to: nearestCornerPosition.x),
                    dy: relativeVelocity(forVelocity: velocity.y, from: soundView.center.y, to: nearestCornerPosition.y)
                )
                let timingParameters = UISpringTimingParameters(dampingRatio: 0.8, initialVelocity: relativeInitialVelocity)
                let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: timingParameters)
                animator.addAnimations {
                    self.soundView.center = nearestCornerPosition
                }
                animator.startAnimation()
            }
        default: break
        }
    }
    
    private func project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
        return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate)
    }
    
    private func nearestCorner(to point: CGPoint) -> CGPoint {
        var minDistance = CGFloat.greatestFiniteMagnitude
        var nearestPosition = CGPoint.zero
        
        for position in [cornerPoints.topLeft, cornerPoints.bottomLeft, cornerPoints.bottomRight, cornerPoints.topRight] {
            let distance = point.distance(to: position)
            if distance < minDistance {
                nearestPosition = position
                minDistance = distance
            }
        }
        return nearestPosition
    }
    
    /// Calculates the relative velocity needed for the initial velocity of the animation.
    private func relativeVelocity(forVelocity velocity: CGFloat, from currentValue: CGFloat, to targetValue: CGFloat) -> CGFloat {
        guard currentValue - targetValue != 0 else { return 0 }
        return velocity / (targetValue - currentValue)
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM