在不使用 CAShapeLayer 的情况下冲破 UIBezierPath 的笔划并为此笔划设置动画
Dash the stroke of UIBezierPath without using the CAShapeLayer and animate this stroke
UIBezierPath
仅在 UIView
中的 drawRect()
方法中使用时才会变成虚线,如下所示:
override func draw(_ rect: CGRect)
{
let path = UIBezierPath()
let p0 = CGPoint(x: self.bounds.minX, y: self.bounds.midY)
path.move(to: p0)
let p1 = CGPoint(x: self.bounds.maxX, y: self.bounds.midY)
path.addLine(to: p1)
let dashes: [ CGFloat ] = [ 0.0, 16.0 ]
path.setLineDash(dashes, count: dashes.count, phase: 0.0)
path.lineWidth = 8.0
path.lineCapStyle = .round
UIColor.red.set()
path.stroke()
}
如果我想为这个线条添加动画效果,我需要像这样使用 CAShapeLayer
override func draw(_ rect: CGRect)
{
let path = UIBezierPath()
let p0 = CGPoint(x: self.bounds.minX, y: self.bounds.midY)
path.move(to: p0)
let p1 = CGPoint(x: self.bounds.maxX, y: self.bounds.midY)
path.addLine(to: p1)
let dashes: [ CGFloat ] = [ 0.0, 16.0 ]
path.setLineDash(dashes, count: dashes.count, phase: 0.0)
path.lineWidth = 8.0
path.lineCapStyle = .round
UIColor.red.set()
path.stroke()
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = UIColor.black.cgColor
layer.lineWidth = 3
layer.fillColor = UIColor.clear.cgColor
layer.lineJoin = kCALineCapButt
self.layer.addSublayer(layer)
animateStroke(layer: layer)
}
func animateStroke(layer:CAShapeLayer)
{
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 10
pathAnimation.fromValue = 0
pathAnimation.toValue = 1
pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
layer.add(pathAnimation, forKey: "strokeEnd")
}
CAShapeLayer
的黑线动画化了。
我需要的是,将虚线的 UIBezierpath 添加到 CAShapeLayer,以便我可以对其进行动画处理。
注意:我不想使用 CAShapeLayer 的 lineDashPattern
方法,因为我要附加多个路径,有些路径需要虚线,有些不需要。
您不应从 draw(_:)
调用动画。 draw(_:)
用于渲染单帧。
你说你不想使用 lineDashPattern
,但我个人会,为每个图案使用不同的形状图层。因此,例如,这是一个动画,在没有虚线模式的情况下抚摸一条路径,在另一条路径上以虚线模式抚摸,并且在第一个完成时触发第二个:
struct Stroke {
let start: CGPoint
let end: CGPoint
let lineDashPattern: [NSNumber]?
var length: CGFloat {
return hypot(start.x - end.x, start.y - end.y)
}
}
class CustomView: UIView {
private var strokes: [Stroke]?
private var strokeIndex = 0
private let strokeSpeed = 200.0
func startAnimation() {
strokes = [
Stroke(start: CGPoint(x: bounds.minX, y: bounds.midY),
end: CGPoint(x: bounds.midX, y: bounds.midY),
lineDashPattern: nil),
Stroke(start: CGPoint(x: bounds.midX, y: bounds.midY),
end: CGPoint(x: bounds.maxX, y: bounds.midY),
lineDashPattern: [0, 16])
]
strokeIndex = 0
animateStroke()
}
private func animateStroke() {
guard let strokes = strokes, strokeIndex < strokes.count else { return }
let stroke = strokes[strokeIndex]
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = kCALineCapRound
shapeLayer.lineDashPattern = strokes[strokeIndex].lineDashPattern
shapeLayer.lineWidth = 8
shapeLayer.strokeColor = UIColor.red.cgColor
layer.addSublayer(shapeLayer)
let path = UIBezierPath()
path.move(to: stroke.start)
path.addLine(to: stroke.end)
shapeLayer.path = path.cgPath
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = Double(stroke.length) / strokeSpeed
animation.delegate = self
shapeLayer.add(animation, forKey: nil)
}
}
extension CustomView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
guard flag else { return }
strokeIndex += 1
animateStroke()
}
}
如果你真的想使用 draw(_:)
方法,你不会使用 CABasicAnimation
,而是可能会使用 CADisplayLink
,重复调用 setNeedsDisplay()
,并有一个 draw(_:)
方法根据已经过去了多少时间来呈现视图。但是 draw(_:)
渲染单帧动画,不应启动任何 CoreAnimation 调用。
如果你真的不想使用形状图层,你可以使用前面提到的 CADisplayLink
根据经过的时间和所需的持续时间来更新完成百分比,并且 draw(_:)
仅将笔划作为适用于任何给定时刻的许多单独路径:
struct Stroke {
let start: CGPoint
let end: CGPoint
let length: CGFloat // in this case, because we're going call this a lot, let's make this stored property
let lineDashPattern: [CGFloat]?
init(start: CGPoint, end: CGPoint, lineDashPattern: [CGFloat]?) {
self.start = start
self.end = end
self.lineDashPattern = lineDashPattern
self.length = hypot(start.x - end.x, start.y - end.y)
}
}
class CustomView: UIView {
private var strokes: [Stroke]?
private let duration: CGFloat = 3.0
private var start: CFTimeInterval?
private var percentComplete: CGFloat?
private var totalLength: CGFloat?
func startAnimation() {
strokes = [
Stroke(start: CGPoint(x: bounds.minX, y: bounds.midY),
end: CGPoint(x: bounds.midX, y: bounds.midY),
lineDashPattern: nil),
Stroke(start: CGPoint(x: bounds.midX, y: bounds.midY),
end: CGPoint(x: bounds.maxX, y: bounds.midY),
lineDashPattern: [0, 16])
]
totalLength = strokes?.reduce(0.0) { [=11=] + .length }
start = CACurrentMediaTime()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .commonModes)
}
@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
percentComplete = min(1.0, CGFloat(CACurrentMediaTime() - start!) / duration)
if percentComplete! >= 1.0 {
displayLink.invalidate()
percentComplete = 1
}
setNeedsDisplay()
}
// Note, no animation is in the following routine. This just stroke your series of paths
// until the total percent of the stroked path equals `percentComplete`. The animation is
// achieved above, by updating `percentComplete` and calling `setNeedsDisplay`. This method
// only draws a single frame of the animation.
override func draw(_ rect: CGRect) {
guard let totalLength = totalLength,
let strokes = strokes,
strokes.count > 0,
let percentComplete = percentComplete else { return }
UIColor.red.setStroke()
// Don't get lost in the weeds here; the idea is to simply stroke my paths until the
// percent of the lengths of all of the stroked paths reaches `percentComplete`. Modify
// the below code to match whatever model you use for all of your stroked paths.
var lengthSoFar: CGFloat = 0
var percentSoFar: CGFloat = 0
var strokeIndex = 0
while lengthSoFar / totalLength < percentComplete && strokeIndex < strokes.count {
let stroke = strokes[strokeIndex]
let endLength = lengthSoFar + stroke.length
let endPercent = endLength / totalLength
let percentOfThisStroke = (percentComplete - percentSoFar) / (endPercent - percentSoFar)
var end: CGPoint
if percentOfThisStroke < 1 {
let angle = atan2(stroke.end.y - stroke.start.y, stroke.end.x - stroke.start.x)
let distance = stroke.length * percentOfThisStroke
end = CGPoint(x: stroke.start.x + distance * cos(angle),
y: stroke.start.y + distance * sin(angle))
} else {
end = stroke.end
}
let path = UIBezierPath()
if let pattern = stroke.lineDashPattern {
path.setLineDash(pattern, count: pattern.count, phase: 0)
}
path.lineWidth = 8
path.lineCapStyle = .round
path.move(to: stroke.start)
path.addLine(to: end)
path.stroke()
strokeIndex += 1
lengthSoFar = endLength
percentSoFar = endPercent
}
}
}
这实现了与第一个代码片段相同的效果,但可能不会像第一个代码片段那样有效。
UIBezierPath
仅在 UIView
中的 drawRect()
方法中使用时才会变成虚线,如下所示:
override func draw(_ rect: CGRect)
{
let path = UIBezierPath()
let p0 = CGPoint(x: self.bounds.minX, y: self.bounds.midY)
path.move(to: p0)
let p1 = CGPoint(x: self.bounds.maxX, y: self.bounds.midY)
path.addLine(to: p1)
let dashes: [ CGFloat ] = [ 0.0, 16.0 ]
path.setLineDash(dashes, count: dashes.count, phase: 0.0)
path.lineWidth = 8.0
path.lineCapStyle = .round
UIColor.red.set()
path.stroke()
}
如果我想为这个线条添加动画效果,我需要像这样使用 CAShapeLayer
override func draw(_ rect: CGRect)
{
let path = UIBezierPath()
let p0 = CGPoint(x: self.bounds.minX, y: self.bounds.midY)
path.move(to: p0)
let p1 = CGPoint(x: self.bounds.maxX, y: self.bounds.midY)
path.addLine(to: p1)
let dashes: [ CGFloat ] = [ 0.0, 16.0 ]
path.setLineDash(dashes, count: dashes.count, phase: 0.0)
path.lineWidth = 8.0
path.lineCapStyle = .round
UIColor.red.set()
path.stroke()
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = UIColor.black.cgColor
layer.lineWidth = 3
layer.fillColor = UIColor.clear.cgColor
layer.lineJoin = kCALineCapButt
self.layer.addSublayer(layer)
animateStroke(layer: layer)
}
func animateStroke(layer:CAShapeLayer)
{
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 10
pathAnimation.fromValue = 0
pathAnimation.toValue = 1
pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
layer.add(pathAnimation, forKey: "strokeEnd")
}
CAShapeLayer
的黑线动画化了。
我需要的是,将虚线的 UIBezierpath 添加到 CAShapeLayer,以便我可以对其进行动画处理。
注意:我不想使用 CAShapeLayer 的 lineDashPattern
方法,因为我要附加多个路径,有些路径需要虚线,有些不需要。
您不应从 draw(_:)
调用动画。 draw(_:)
用于渲染单帧。
你说你不想使用 lineDashPattern
,但我个人会,为每个图案使用不同的形状图层。因此,例如,这是一个动画,在没有虚线模式的情况下抚摸一条路径,在另一条路径上以虚线模式抚摸,并且在第一个完成时触发第二个:
struct Stroke {
let start: CGPoint
let end: CGPoint
let lineDashPattern: [NSNumber]?
var length: CGFloat {
return hypot(start.x - end.x, start.y - end.y)
}
}
class CustomView: UIView {
private var strokes: [Stroke]?
private var strokeIndex = 0
private let strokeSpeed = 200.0
func startAnimation() {
strokes = [
Stroke(start: CGPoint(x: bounds.minX, y: bounds.midY),
end: CGPoint(x: bounds.midX, y: bounds.midY),
lineDashPattern: nil),
Stroke(start: CGPoint(x: bounds.midX, y: bounds.midY),
end: CGPoint(x: bounds.maxX, y: bounds.midY),
lineDashPattern: [0, 16])
]
strokeIndex = 0
animateStroke()
}
private func animateStroke() {
guard let strokes = strokes, strokeIndex < strokes.count else { return }
let stroke = strokes[strokeIndex]
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = kCALineCapRound
shapeLayer.lineDashPattern = strokes[strokeIndex].lineDashPattern
shapeLayer.lineWidth = 8
shapeLayer.strokeColor = UIColor.red.cgColor
layer.addSublayer(shapeLayer)
let path = UIBezierPath()
path.move(to: stroke.start)
path.addLine(to: stroke.end)
shapeLayer.path = path.cgPath
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = Double(stroke.length) / strokeSpeed
animation.delegate = self
shapeLayer.add(animation, forKey: nil)
}
}
extension CustomView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
guard flag else { return }
strokeIndex += 1
animateStroke()
}
}
如果你真的想使用 draw(_:)
方法,你不会使用 CABasicAnimation
,而是可能会使用 CADisplayLink
,重复调用 setNeedsDisplay()
,并有一个 draw(_:)
方法根据已经过去了多少时间来呈现视图。但是 draw(_:)
渲染单帧动画,不应启动任何 CoreAnimation 调用。
如果你真的不想使用形状图层,你可以使用前面提到的 CADisplayLink
根据经过的时间和所需的持续时间来更新完成百分比,并且 draw(_:)
仅将笔划作为适用于任何给定时刻的许多单独路径:
struct Stroke {
let start: CGPoint
let end: CGPoint
let length: CGFloat // in this case, because we're going call this a lot, let's make this stored property
let lineDashPattern: [CGFloat]?
init(start: CGPoint, end: CGPoint, lineDashPattern: [CGFloat]?) {
self.start = start
self.end = end
self.lineDashPattern = lineDashPattern
self.length = hypot(start.x - end.x, start.y - end.y)
}
}
class CustomView: UIView {
private var strokes: [Stroke]?
private let duration: CGFloat = 3.0
private var start: CFTimeInterval?
private var percentComplete: CGFloat?
private var totalLength: CGFloat?
func startAnimation() {
strokes = [
Stroke(start: CGPoint(x: bounds.minX, y: bounds.midY),
end: CGPoint(x: bounds.midX, y: bounds.midY),
lineDashPattern: nil),
Stroke(start: CGPoint(x: bounds.midX, y: bounds.midY),
end: CGPoint(x: bounds.maxX, y: bounds.midY),
lineDashPattern: [0, 16])
]
totalLength = strokes?.reduce(0.0) { [=11=] + .length }
start = CACurrentMediaTime()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .commonModes)
}
@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
percentComplete = min(1.0, CGFloat(CACurrentMediaTime() - start!) / duration)
if percentComplete! >= 1.0 {
displayLink.invalidate()
percentComplete = 1
}
setNeedsDisplay()
}
// Note, no animation is in the following routine. This just stroke your series of paths
// until the total percent of the stroked path equals `percentComplete`. The animation is
// achieved above, by updating `percentComplete` and calling `setNeedsDisplay`. This method
// only draws a single frame of the animation.
override func draw(_ rect: CGRect) {
guard let totalLength = totalLength,
let strokes = strokes,
strokes.count > 0,
let percentComplete = percentComplete else { return }
UIColor.red.setStroke()
// Don't get lost in the weeds here; the idea is to simply stroke my paths until the
// percent of the lengths of all of the stroked paths reaches `percentComplete`. Modify
// the below code to match whatever model you use for all of your stroked paths.
var lengthSoFar: CGFloat = 0
var percentSoFar: CGFloat = 0
var strokeIndex = 0
while lengthSoFar / totalLength < percentComplete && strokeIndex < strokes.count {
let stroke = strokes[strokeIndex]
let endLength = lengthSoFar + stroke.length
let endPercent = endLength / totalLength
let percentOfThisStroke = (percentComplete - percentSoFar) / (endPercent - percentSoFar)
var end: CGPoint
if percentOfThisStroke < 1 {
let angle = atan2(stroke.end.y - stroke.start.y, stroke.end.x - stroke.start.x)
let distance = stroke.length * percentOfThisStroke
end = CGPoint(x: stroke.start.x + distance * cos(angle),
y: stroke.start.y + distance * sin(angle))
} else {
end = stroke.end
}
let path = UIBezierPath()
if let pattern = stroke.lineDashPattern {
path.setLineDash(pattern, count: pattern.count, phase: 0)
}
path.lineWidth = 8
path.lineCapStyle = .round
path.move(to: stroke.start)
path.addLine(to: end)
path.stroke()
strokeIndex += 1
lengthSoFar = endLength
percentSoFar = endPercent
}
}
}
这实现了与第一个代码片段相同的效果,但可能不会像第一个代码片段那样有效。