在其绘制方法中为 CALayer 内容设置动画
Animate a CALayer contents in its draw method
好的,这个问题可能已经在 SO 上被问过 1000 次了。我一定已经尝试了 500 个答案。没有任何效果,这让我觉得这可能是不可能的。
在我开始之前,我不想要动画自定义 属性,我只想利用边界变化。大多数答案只针对前者。但是,如果我的内容仅取决于图层的大小,仅此而已呢?
这真的不可能吗?
我试过了:
override class func needsDisplay(forKey key: String) -> Bool
needsDisplayOnBoundsChange
- 覆盖每一种
action(for:)
或animation(for:)
方法
contentMode = .redraw
- 大量技巧,例如覆盖
setBounds
和添加隐式动画(顺便说一句,这是无用的,因为图层有动画,它只是不会在调整大小时重绘)
- 也许我忘记了其他一些事情,花了很长时间在这上面
我达到的最远:内容在动画开始时绘制,然后在结束时绘制。当从大变小时,哪个当然看起来很糟糕(也像素化)。
不一定重要,但这里是绘制方法供参考:
override func draw(in ctx: CGContext) {
UIGraphicsPushContext(ctx)
paths = [Dimension: [UIBezierPath]]()
cuts = [Dimension: [UIBezierPath]]()
let centerTranslate = CGAffineTransform.init(translationX: frame.width * 0.5,
y: frame.height * 0.5)
let dimRotateAndTranslate = centerTranslate.rotated(by: CGFloat.pi / 2)
let needlePath = UIBezierPath(rect: CGRect(x: 0,
y: 0,
width: circularSpacing,
height: frame.height * 0.5 - centerRadius))
let piProgress = CGFloat(progress * 2 * Float.pi)
var needleRotateAndTranslate = CGAffineTransform(rotationAngle: piProgress)
needleRotateAndTranslate = needleRotateAndTranslate.translatedBy(x: -needlePath.bounds.width * 0.5,
y: -needlePath.bounds.height - centerRadius)
needlePath.apply(needleRotateAndTranslate.concatenating(centerTranslate))
let sortedDims = states.sorted(by: {[=10=].key < .key})
for (offset: i, element: (key: dim, value: beats)) in sortedDims.enumerated() {
var pathsAtDim = paths[dim] ?? []
var cutAtDim = cuts[dim] ?? []
let thickness = (frame.width * 0.5 - centerRadius - circularSpacing * CGFloat(states.count - 1)) / CGFloat(states.count)
let xSpacing = thickness + circularSpacing
let radius = centerRadius + thickness * 0.5 + CGFloat(i) * xSpacing
for (offset: j, beat) in beats.enumerated() {
let length = CGFloat.pi * 2 / CGFloat(beats.count) * CGFloat(beat.relativeDuration)
var path = UIBezierPath(arcCenter: .zero,
radius: radius,
startAngle: CGFloat(j) * length,
endAngle: CGFloat(j + 1) * length,
clockwise: true)
path = UIBezierPath(cgPath: path.cgPath.copy(strokingWithWidth: thickness,
lineCap: .butt,
lineJoin: .miter,
miterLimit: 0,
transform: dimRotateAndTranslate))
func cutAtIndex(index: Int, offset: CGFloat) -> UIBezierPath {
let cut = UIBezierPath(rect: CGRect(x: 0,
y: 0,
width: thickness,
height: circularSpacing))
let offsetRadians = offset / radius
let x = (radius - thickness / 2) * cos(CGFloat(index) * length + offsetRadians)
let y = (radius - thickness / 2) * sin(CGFloat(index) * length + offsetRadians)
let translate = CGAffineTransform.init(translationX: x,
y: y)
let rotate = CGAffineTransform.init(rotationAngle: CGFloat(index) * length)
cut.apply(rotate
.concatenating(translate)
.concatenating(dimRotateAndTranslate))
return cut
}
cutAtDim.append(cutAtIndex(index: j, offset: -circularSpacing * 0.5))
pathsAtDim.append(path)
}
paths[dim] = pathsAtDim
cuts[dim] = cutAtDim
}
let clippingMask = UIBezierPath.init(rect: bounds)
for (dim, _) in states {
for cut in cuts[dim] ?? [] {
clippingMask.append(cut.reversing())
}
}
clippingMask.addClip()
for (dim, states) in states {
for i in 0..<states.count {
let state = states[i]
if let path = paths[dim]?[i] {
let color = dimensionsColors[dim]?[state.state] ?? .darkGray
color.setFill()
path.fill()
}
}
}
ctx.resetClip()
needleColor.setFill()
needlePath.fill()
UIGraphicsPopContext()
}
首先,这个问题在很多方面都不是很清楚。如果我的理解是正确的,您希望更改边界会在动画期间自动重绘 CALayer。
方法一:
它可以通过一般方式实现,包括设置计时器和稍微更改边界(从 CGRECT 数组)并每次重绘。由于可以访问绘图功能,所以自己开发这样的动画应该不是问题。
方法二:
这个想法和以前一样,但是如果你想利用 CAAnimation 的方式。以下代码段可以为您提供帮助。
在这一个中,使用了一个外部定时器 (CADisplayLink)。此外,为了方便起见,必须通过函数 updateBounds
实现边界更改。
请记住,应谨慎对待片段的结构。任何修改都可能导致动画丢失。
成功的结果会提示你在bounds changing的过程中继续重绘
测试viewController:
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
layer1 = MyLayer()
layer1.frame = CGRect.init(x: 0, y: 0, width: 300, height: 500)
layer1.backgroundColor = UIColor.red.cgColor
view.layer.addSublayer(layer1)
layer1.setNeedsDisplay()
}
@IBAction func updateLayer1(){
CATransaction.begin()
CATransaction.setAnimationDuration(3.0)
let nextBounds = CGRect.init(origin: layer1.bounds.origin, size: CGSize.init(width: layer1.bounds.width + 100, height: layer1.bounds.height + 150))
layer1.updateBounds(nextBounds)
CATransaction.commit()
}
}
CALayer的子类,只列出边界变化动画部分
class MyLayer: CALayer, CAAnimationDelegate{
func updateBounds(_ bounds: CGRect){
nextBounds = bounds
self.bounds = bounds
}
private var nextBounds : CGRect!
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let keyPath = (anim as! CABasicAnimation).keyPath , let displayLink = animationLoops[keyPath]{
displayLink?.invalidate()
animationLoops.removeValue(forKey: keyPath) }
}
@objc func updateDrawing(displayLink: CADisplayLink){
bounds = presentation()!.bounds
setNeedsDisplay()
}
var animationLoops: [String : CADisplayLink?] = [:]
override func action(forKey event: String) -> CAAction? {
let result = super.action(forKey: event)
if(event == "bounds"){
guard let result = result as? CABasicAnimation,
animationLoops[event] == nil
else{return nil}
let anim = CABasicAnimation.init(keyPath: result.keyPath)
anim.fromValue = result.fromValue
anim.toValue = nextBounds
let caDisplayLink = CADisplayLink.init(target: self, selector: #selector(updateDrawing))
caDisplayLink.add(to: .main, forMode: RunLoop.Mode.default)
anim.delegate = self
animationLoops[event] = caDisplayLink
return anim
}
return result
}
希望这就是您想要的。祝你有个愉快的一天。
比较动画:
没有定时器:
添加 CADsiaplayLinker 计时器:
好的,这个问题可能已经在 SO 上被问过 1000 次了。我一定已经尝试了 500 个答案。没有任何效果,这让我觉得这可能是不可能的。
在我开始之前,我不想要动画自定义 属性,我只想利用边界变化。大多数答案只针对前者。但是,如果我的内容仅取决于图层的大小,仅此而已呢?
这真的不可能吗? 我试过了:
override class func needsDisplay(forKey key: String) -> Bool
needsDisplayOnBoundsChange
- 覆盖每一种
action(for:)
或animation(for:)
方法 contentMode = .redraw
- 大量技巧,例如覆盖
setBounds
和添加隐式动画(顺便说一句,这是无用的,因为图层有动画,它只是不会在调整大小时重绘) - 也许我忘记了其他一些事情,花了很长时间在这上面
我达到的最远:内容在动画开始时绘制,然后在结束时绘制。当从大变小时,哪个当然看起来很糟糕(也像素化)。
不一定重要,但这里是绘制方法供参考:
override func draw(in ctx: CGContext) {
UIGraphicsPushContext(ctx)
paths = [Dimension: [UIBezierPath]]()
cuts = [Dimension: [UIBezierPath]]()
let centerTranslate = CGAffineTransform.init(translationX: frame.width * 0.5,
y: frame.height * 0.5)
let dimRotateAndTranslate = centerTranslate.rotated(by: CGFloat.pi / 2)
let needlePath = UIBezierPath(rect: CGRect(x: 0,
y: 0,
width: circularSpacing,
height: frame.height * 0.5 - centerRadius))
let piProgress = CGFloat(progress * 2 * Float.pi)
var needleRotateAndTranslate = CGAffineTransform(rotationAngle: piProgress)
needleRotateAndTranslate = needleRotateAndTranslate.translatedBy(x: -needlePath.bounds.width * 0.5,
y: -needlePath.bounds.height - centerRadius)
needlePath.apply(needleRotateAndTranslate.concatenating(centerTranslate))
let sortedDims = states.sorted(by: {[=10=].key < .key})
for (offset: i, element: (key: dim, value: beats)) in sortedDims.enumerated() {
var pathsAtDim = paths[dim] ?? []
var cutAtDim = cuts[dim] ?? []
let thickness = (frame.width * 0.5 - centerRadius - circularSpacing * CGFloat(states.count - 1)) / CGFloat(states.count)
let xSpacing = thickness + circularSpacing
let radius = centerRadius + thickness * 0.5 + CGFloat(i) * xSpacing
for (offset: j, beat) in beats.enumerated() {
let length = CGFloat.pi * 2 / CGFloat(beats.count) * CGFloat(beat.relativeDuration)
var path = UIBezierPath(arcCenter: .zero,
radius: radius,
startAngle: CGFloat(j) * length,
endAngle: CGFloat(j + 1) * length,
clockwise: true)
path = UIBezierPath(cgPath: path.cgPath.copy(strokingWithWidth: thickness,
lineCap: .butt,
lineJoin: .miter,
miterLimit: 0,
transform: dimRotateAndTranslate))
func cutAtIndex(index: Int, offset: CGFloat) -> UIBezierPath {
let cut = UIBezierPath(rect: CGRect(x: 0,
y: 0,
width: thickness,
height: circularSpacing))
let offsetRadians = offset / radius
let x = (radius - thickness / 2) * cos(CGFloat(index) * length + offsetRadians)
let y = (radius - thickness / 2) * sin(CGFloat(index) * length + offsetRadians)
let translate = CGAffineTransform.init(translationX: x,
y: y)
let rotate = CGAffineTransform.init(rotationAngle: CGFloat(index) * length)
cut.apply(rotate
.concatenating(translate)
.concatenating(dimRotateAndTranslate))
return cut
}
cutAtDim.append(cutAtIndex(index: j, offset: -circularSpacing * 0.5))
pathsAtDim.append(path)
}
paths[dim] = pathsAtDim
cuts[dim] = cutAtDim
}
let clippingMask = UIBezierPath.init(rect: bounds)
for (dim, _) in states {
for cut in cuts[dim] ?? [] {
clippingMask.append(cut.reversing())
}
}
clippingMask.addClip()
for (dim, states) in states {
for i in 0..<states.count {
let state = states[i]
if let path = paths[dim]?[i] {
let color = dimensionsColors[dim]?[state.state] ?? .darkGray
color.setFill()
path.fill()
}
}
}
ctx.resetClip()
needleColor.setFill()
needlePath.fill()
UIGraphicsPopContext()
}
首先,这个问题在很多方面都不是很清楚。如果我的理解是正确的,您希望更改边界会在动画期间自动重绘 CALayer。
方法一: 它可以通过一般方式实现,包括设置计时器和稍微更改边界(从 CGRECT 数组)并每次重绘。由于可以访问绘图功能,所以自己开发这样的动画应该不是问题。
方法二:
这个想法和以前一样,但是如果你想利用 CAAnimation 的方式。以下代码段可以为您提供帮助。
在这一个中,使用了一个外部定时器 (CADisplayLink)。此外,为了方便起见,必须通过函数 updateBounds
实现边界更改。
请记住,应谨慎对待片段的结构。任何修改都可能导致动画丢失。
成功的结果会提示你在bounds changing的过程中继续重绘
测试viewController:
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
layer1 = MyLayer()
layer1.frame = CGRect.init(x: 0, y: 0, width: 300, height: 500)
layer1.backgroundColor = UIColor.red.cgColor
view.layer.addSublayer(layer1)
layer1.setNeedsDisplay()
}
@IBAction func updateLayer1(){
CATransaction.begin()
CATransaction.setAnimationDuration(3.0)
let nextBounds = CGRect.init(origin: layer1.bounds.origin, size: CGSize.init(width: layer1.bounds.width + 100, height: layer1.bounds.height + 150))
layer1.updateBounds(nextBounds)
CATransaction.commit()
}
}
CALayer的子类,只列出边界变化动画部分
class MyLayer: CALayer, CAAnimationDelegate{
func updateBounds(_ bounds: CGRect){
nextBounds = bounds
self.bounds = bounds
}
private var nextBounds : CGRect!
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let keyPath = (anim as! CABasicAnimation).keyPath , let displayLink = animationLoops[keyPath]{
displayLink?.invalidate()
animationLoops.removeValue(forKey: keyPath) }
}
@objc func updateDrawing(displayLink: CADisplayLink){
bounds = presentation()!.bounds
setNeedsDisplay()
}
var animationLoops: [String : CADisplayLink?] = [:]
override func action(forKey event: String) -> CAAction? {
let result = super.action(forKey: event)
if(event == "bounds"){
guard let result = result as? CABasicAnimation,
animationLoops[event] == nil
else{return nil}
let anim = CABasicAnimation.init(keyPath: result.keyPath)
anim.fromValue = result.fromValue
anim.toValue = nextBounds
let caDisplayLink = CADisplayLink.init(target: self, selector: #selector(updateDrawing))
caDisplayLink.add(to: .main, forMode: RunLoop.Mode.default)
anim.delegate = self
animationLoops[event] = caDisplayLink
return anim
}
return result
}
希望这就是您想要的。祝你有个愉快的一天。
比较动画: 没有定时器:
添加 CADsiaplayLinker 计时器: