演练动画 CAShapeLayer x UIBezierPath

问题描述 投票:0回答:1

下午好!

我在函数 addHoleToMovableView 中遇到动画问题。

在尝试从A点到B点的慢速动画时,我没有获得满意的结果。我希望有一个非常短的动画类型,可以在 y 轴上从上到下或从下到上从一个地方移动到另一个地方。就好像它以规定的秒数从一个点移动到另一个点。

我的控制器:

import UIKit

class TutorialViewController: UIViewController {
    var tutorialView: TutorialView!
    var view1: UIView!
    var view2: UIView!
    var view3: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        tutorialView = TutorialView()
        tutorialView.translatesAutoresizingMaskIntoConstraints = false
        tutorialView.backgroundColor = .black.withAlphaComponent(0.7)
 
        setViews()
        view.addSubview(tutorialView)
        
        NSLayoutConstraint.activate([
            tutorialView.topAnchor.constraint(equalTo: view.topAnchor),
            tutorialView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tutorialView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tutorialView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        
        tutorialView.onNextButtonTapped = { [weak self] in
            self?.moveMovableViewToCurrentPosition()
        }
        
        tutorialView.onPreviousButtonTapped = { [weak self] in
            self?.moveMovableViewToCurrentPosition()
        }
        
        tutorialView.onOkButtonTapped = { [weak self] in
            self?.tutorialView.currentState += 1
            self?.removeTutorialViewFromSuperView()
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        moveMovableViewToCurrentPosition()
    }
    
    private func addHoleToMovableView(frame: CGRect) {
        let holeLayer = CAShapeLayer()
        let path = UIBezierPath(rect: self.tutorialView.bounds)
        let holePath = UIBezierPath(rect: frame)
        
        path.append(holePath.reversing())
        holeLayer.path = path.cgPath
        holeLayer.fillRule = .evenOdd
        self.tutorialView.layer.mask = holeLayer
        
        let animation = CABasicAnimation(keyPath: "patch")
        animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
        holeLayer.add(animation, forKey: "positionAnimation")
    }


    func setViews() {
        view1 = UIView()
        view2 = UIView()
        view3 = UIView()
        
        view1.translatesAutoresizingMaskIntoConstraints = false
        view2.translatesAutoresizingMaskIntoConstraints = false
        view3.translatesAutoresizingMaskIntoConstraints = false
        
        view1.backgroundColor = .blue
        view2.backgroundColor = .yellow
        view3.backgroundColor = .green
        
        view.addSubview(view1)
        view.addSubview(view2)
        view.addSubview(view3)
        
        NSLayoutConstraint.activate([
            view1.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
            view1.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            view1.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            view1.heightAnchor.constraint(equalToConstant: 60),
            
            view2.topAnchor.constraint(equalTo: view1.bottomAnchor, constant: 10),
            view2.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
            view2.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
            view2.heightAnchor.constraint(equalToConstant: 200),
            
            view3.topAnchor.constraint(equalTo: view2.bottomAnchor, constant: 10),
            view3.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            view3.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            view3.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            
            
        ])
    }
    
    func removeTutorialViewFromSuperView() {
        tutorialView.removeFromSuperview()
        view.layoutIfNeeded()
    }
    
    func moveMovableViewToCurrentPosition() {
        switch tutorialView.currentState {
        case 0:
            tutorialView.updateControlContainerConstraints(position: .below, referenceFrame: view1.frame)
            addHoleToMovableView(frame: view1.frame)
        case 1:
            tutorialView.updateControlContainerConstraints(position: .below, referenceFrame: view2.frame)
            addHoleToMovableView(frame: view2.frame)
        case 2:
            tutorialView.updateControlContainerConstraints(position: .above, referenceFrame: view3.frame)
            addHoleToMovableView(frame: view3.frame)
        default:
            break
        }
    }
    
}

这是目前的“闪烁”效果。

模拟器

ios swift mobile uikit ios-simulator
1个回答
0
投票

要为路径设置动画 - 无论是用于绘图还是用作遮罩 - 我们需要为动画设置

.toValue

keyPath
也需要是“路径”而不是“补丁”(但我猜这是一个错字):

    let animation = CABasicAnimation(keyPath: "path")
    animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
    
    // fromValue is not strictly necessary here, but we use it for consistency
    animation.fromValue = curPath
    animation.toValue = newPath
    
    animation.isRemovedOnCompletion = false
    
    // adjust animation speed as desired
    animation.duration = 0.75
    
    holeLayer.add(animation, forKey: "positionAnimation")

我们还想将

CATransaction
与完成块一起使用,因为我们需要在动画结束时更新
holeLayer.path

我认为您还会发现将“洞层”和动画代码保留在覆盖视图(您的TutorialView)中更容易。

这是一些示例代码,根据您的示例进行了修改:

enum ControlPosition { case above, below } class TutorialViewController: UIViewController { var tutorialView: TutorialView = TutorialView() var view1: UIView! var view2: UIView! var view3: UIView! override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white setViews() tutorialView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tutorialView) NSLayoutConstraint.activate([ tutorialView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0), tutorialView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0), tutorialView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0), tutorialView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0), ]) tutorialView.onNextButtonTapped = { [weak self] in self?.moveMovableViewToCurrentPosition() } tutorialView.onPrevButtonTapped = { [weak self] in self?.moveMovableViewToCurrentPosition() } tutorialView.onDoneButtonTapped = { [weak self] in self?.removeTutorialViewFromSuperView() } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // let the translucent tutorial view show up to begin with // start with the "hole" above the top of the vivew tutorialView.updateHole(.init(x: 0.0, y: -20.0, width: tutorialView.frame.width, height: 20.0), animated: false, cPos: .above) // then animate DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { self.moveMovableViewToCurrentPosition() }) } func setViews() { view1 = UIView() view2 = UIView() view3 = UIView() view1.translatesAutoresizingMaskIntoConstraints = false view2.translatesAutoresizingMaskIntoConstraints = false view3.translatesAutoresizingMaskIntoConstraints = false view1.backgroundColor = .blue view2.backgroundColor = .yellow view3.backgroundColor = .green view.addSubview(view1) view.addSubview(view2) view.addSubview(view3) let g = view.safeAreaLayoutGuide NSLayoutConstraint.activate([ view1.topAnchor.constraint(equalTo: g.topAnchor, constant: 20), view1.leadingAnchor.constraint(equalTo: g.leadingAnchor), view1.trailingAnchor.constraint(equalTo: g.trailingAnchor), view1.heightAnchor.constraint(equalToConstant: 60), view2.topAnchor.constraint(equalTo: view1.bottomAnchor, constant: 10), view2.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 10), view2.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -10), view2.heightAnchor.constraint(equalToConstant: 200), view3.topAnchor.constraint(equalTo: view2.bottomAnchor, constant: 10), view3.leadingAnchor.constraint(equalTo: g.leadingAnchor), view3.trailingAnchor.constraint(equalTo: g.trailingAnchor), view3.bottomAnchor.constraint(equalTo: g.bottomAnchor) ]) } func removeTutorialViewFromSuperView() { tutorialView.removeFromSuperview() view.layoutIfNeeded() } func moveMovableViewToCurrentPosition() { switch tutorialView.currentState { case 0: tutorialView.updateHole(view1.frame, animated: true, cPos: .below) case 1: tutorialView.updateHole(view2.frame, animated: true, cPos: .below) case 2: tutorialView.updateHole(view3.frame, animated: true, cPos: .above) default: break } } } class TutorialView: UIView { var onNextButtonTapped: (()->())? var onPrevButtonTapped: (()->())? var onDoneButtonTapped: (()->())? var currentState: Int = 0 let translucentLayer = CALayer() let holeLayer = CAShapeLayer() var curPath: CGPath! var newPath: CGPath! let controlContainer = UIView() let prevBtn = UIButton() let nextBtn = UIButton() let doneBtn = UIButton() let infoLabel = UILabel() var ccTop: NSLayoutConstraint! override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } func commonInit() -> Void { holeLayer.fillColor = UIColor.black.cgColor holeLayer.fillRule = .evenOdd translucentLayer.backgroundColor = UIColor.black.withAlphaComponent(0.7).cgColor layer.addSublayer(translucentLayer) controlContainer.translatesAutoresizingMaskIntoConstraints = false addSubview(controlContainer) ccTop = controlContainer.topAnchor.constraint(equalTo: topAnchor) NSLayoutConstraint.activate([ ccTop, controlContainer.leadingAnchor.constraint(equalTo: leadingAnchor), controlContainer.trailingAnchor.constraint(equalTo: trailingAnchor), controlContainer.heightAnchor.constraint(equalToConstant: 60.0), ]) prevBtn.setTitle("Voltar", for: []) nextBtn.setTitle("Próximo", for: []) doneBtn.setTitle("OK, entendi", for: []) infoLabel.textAlignment = .center infoLabel.textColor = .white for v in [prevBtn, nextBtn, doneBtn, infoLabel] { v.translatesAutoresizingMaskIntoConstraints = false controlContainer.addSubview(v) } NSLayoutConstraint.activate([ prevBtn.leadingAnchor.constraint(equalTo: controlContainer.leadingAnchor, constant: 8.0), prevBtn.bottomAnchor.constraint(equalTo: controlContainer.bottomAnchor, constant: -4.0), nextBtn.trailingAnchor.constraint(equalTo: controlContainer.trailingAnchor, constant: -8.0), nextBtn.bottomAnchor.constraint(equalTo: controlContainer.bottomAnchor, constant: -4.0), doneBtn.trailingAnchor.constraint(equalTo: controlContainer.trailingAnchor, constant: -8.0), doneBtn.bottomAnchor.constraint(equalTo: controlContainer.bottomAnchor, constant: -4.0), infoLabel.topAnchor.constraint(equalTo: controlContainer.topAnchor, constant: 8.0), infoLabel.leadingAnchor.constraint(equalTo: controlContainer.leadingAnchor, constant: 8.0), infoLabel.trailingAnchor.constraint(equalTo: controlContainer.trailingAnchor, constant: -8.0), ]) nextBtn.addTarget(self, action: #selector(nextTapped(_:)), for: .touchUpInside) prevBtn.addTarget(self, action: #selector(prevTapped(_:)), for: .touchUpInside) doneBtn.addTarget(self, action: #selector(doneTapped(_:)), for: .touchUpInside) controlContainer.backgroundColor = .systemRed // we start with control container alpha at Zero controlContainer.alpha = 0.0 } override func layoutSubviews() { super.layoutSubviews() translucentLayer.frame = bounds } public func updateHole(_ r: CGRect, animated: Bool, cPos: ControlPosition) { translucentLayer.mask = holeLayer let path = UIBezierPath(rect: bounds) // create a path for the "hole" in the layer var newR: CGRect = r newR.origin.x = 0.0 newR.size.width = bounds.width let holePath = UIBezierPath(rect: newR) // this "cuts a hole" in the path path.append(holePath) path.usesEvenOddFillRule = true newPath = path.cgPath if !animated { self.curPath = self.newPath self.holeLayer.path = self.curPath } else { // fade-out controlContainer UIView.animate(withDuration: 0.3, animations: { self.controlContainer.alpha = 0.0 }, completion: { _ in // update next/prev/done buttons visiblity self.nextBtn.isHidden = self.currentState == 2 ? true : false self.prevBtn.isHidden = self.currentState == 0 ? true : false self.doneBtn.isHidden = !self.nextBtn.isHidden self.infoLabel.text = "Current State: \(self.currentState)" // update control container position self.ccTop.constant = cPos == .below ? r.maxY : r.minY - self.controlContainer.frame.height self.animPath() }) } } private func animPath() { CATransaction.begin() CATransaction.setCompletionBlock({ // on animation completion, we want to // update the holeLayer's path, and // fade-in the controls container self.curPath = self.newPath self.holeLayer.path = self.curPath UIView.animate(withDuration: 0.3, animations: { self.controlContainer.alpha = 1.0 }) }) let animation = CABasicAnimation(keyPath: "path") animation.timingFunction = CAMediaTimingFunction(name: .easeIn) // fromValue is not strictly necessary here, but we use it for consistency animation.fromValue = curPath animation.toValue = newPath animation.isRemovedOnCompletion = false // adjust animation speed as desired animation.duration = 0.75 holeLayer.add(animation, forKey: "positionAnimation") CATransaction.commit() } @objc func nextTapped(_ sender: Any?) { currentState += 1 onNextButtonTapped?() } @objc func prevTapped(_ sender: Any?) { currentState -= 1 onPrevButtonTapped?() } @objc func doneTapped(_ sender: Any?) { onDoneButtonTapped?() } }
    
© www.soinside.com 2019 - 2024. All rights reserved.