ios - 在不使用 CAShapeLayer 的情况下对 UIBezierPath 的笔划进行划线并为该笔划设置动画

标签 ios swift animation uibezierpath cabasicanimation

UIBezierPath 仅在 UIViewdrawRect() 方法中使用时才会出现虚线,如下所示:

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()
    }

enter image description here

如果我想为这条线描画动画,我需要像这样使用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 的黑线已动画化。

enter image description here

我需要的是,将虚线 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()
    }
}

enter image description here

如果您确实想使用draw(_:)方法,你不会使用 CABasicAnimation ,但可能会使用 CADisplayLink ,反复调用setNeedsDisplay() ,并且有 draw(_:)根据耗时呈现 View 的方法。但是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) { $0 + $1.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
        }
    }
}

这实现了与第一个代码片段相同的效果,尽管它可能不会那么高效。

关于ios - 在不使用 CAShapeLayer 的情况下对 UIBezierPath 的笔划进行划线并为该笔划设置动画,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47730808/

相关文章:

android - 像facebook android一样创建登录动画

iphone - 刷新自定义 tableView 中的单元格

iphone - 确定 iPhone 的大致位置

ios - Storyboard View 树中的不可见 subview

ios - 执行segues到非默认选项卡 View Controller 模态不同的 Storyboard

Swift 只调用递归函数 X 次

ios - CGAffineTransformMakeTranslation 与 CGRectOffset 动画

ios - 无法加载 UITests 包,因为它已损坏或缺少必要的资源。尝试重新安装 bundle

ios - 如何正确设置和使用字典?

ios - 暂停 UIView 的关键帧动画