下午好!
我在函数 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
}
}
}
这是目前的“闪烁”效果。
要为路径设置动画 - 无论是用于绘图还是用作遮罩 - 我们需要为动画设置
.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?()
}
}