【问题标题】:CAShapeLayer() draws weird line/pathCAShapeLayer() 绘制奇怪的线/路径
【发布时间】:2019-05-29 16:28:10
【问题描述】:

我有一个顶部有渐变的CAShapeLayer(),它正在被动画化,但不知何故,它看起来如下图所示:

怎么会这样?

我的代码:

override func viewDidLayoutSubviews() {
    displayLine()
}

override func viewDidAppear(_ animated: Bool) {
    animateStroke()
}

func displayLine() {
    let trackLayer = CAShapeLayer()
    let rect = CGRect(x: topView.frame.width * 0.15, y: topView.frame.size.height / 1.5, width: topView.frame.width * 0.7, height: 2)
    let path = UIBezierPath(roundedRect: rect, cornerRadius: 1)

    trackLayer.path = path.cgPath
    trackLayer.strokeColor = UIColor.groupTableViewBackground.cgColor
    trackLayer.lineWidth = 3
    trackLayer.fillColor = UIColor.clear.cgColor

    shapeLayer.path = path.cgPath
    shapeLayer.strokeColor = UIColor.green.cgColor
    shapeLayer.lineWidth = 4
    shapeLayer.fillColor = UIColor.clear.cgColor
    shapeLayer.strokeEnd = 0

    topView.layer.addSublayer(trackLayer)
    topView.layer.addSublayer(shapeLayer)

    let color = UIColor(red: 11/255, green: 95/255, blue: 244/255, alpha: 1).cgColor
    let sndColor = UIColor(red: 255/255, green: 87/255, blue: 87/255, alpha: 1).cgColor

    gradient.colors = [color, sndColor]
    gradient.locations = [0.0, 1.0]
    gradient.startPoint = CGPoint(x: 0, y: 0)
    gradient.endPoint = CGPoint(x: 1, y: 0)
    gradient.frame = topView.bounds

    gradient.mask = shapeLayer
    topView.layer.addSublayer(gradient)
}

func animateStroke() {
    if !animated {
        animated = true

        let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
        var value: Double?

        let distance = currLeasingCar!.currentKm - currLeasing!.startKm
        value = Double(distance) / Double(finalKm)

        basicAnimation.toValue = value
        basicAnimation.duration = 1.5
        basicAnimation.fillMode = .forwards
        basicAnimation.isRemovedOnCompletion = false
        basicAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

        shapeLayer.add(basicAnimation, forKey: "lineStrokeAnimation")
    }
}

【问题讨论】:

    标签: ios swift cashapelayer cabasicanimation


    【解决方案1】:

    问题是您的路径是圆角矩形。在您与我们分享的图片中,大概有 2-3% 的笔触。将其更改为 90% 的描边,您会看到它试图绘制一个宽且极短的圆角矩形,例如:

    相反,只需将路径设为一条线,它就会按预期工作:

    let path = UIBezierPath()
    
    let bounds = topView.bounds
    path.move(to:    CGPoint(x: bounds.minX + bounds.width * 0.15, y: bounds.minY + bounds.height / 1.5))
    path.addLine(to: CGPoint(x: bounds.minX + bounds.width * 0.85, y: bounds.minY + bounds.height / 1.5))
    

    您可能还希望将形状图层的顶部变圆:

    trackLayer.lineCap = .round  // or whatever you want
    shapeLayer.lineCap = .round
    

    当然,这个改变失去了原来路径的 2 点高度,所以如果你想让这些形状层更厚,只需增加它们各自的 lineWidth 值。


    一些不相关的观察:

    • viewDidLayoutSubviews()viewDidAppear(_:) 应该调用它们的 super 实现。

    • viewDidLayoutSubviews() 可以被多次调用,所以你不想每次都实例化一个新的trackLayer。或者,如果您这样做,请确保删除之前的。

    • 添加子视图/子层时,谨慎使用bounds 而不是frame。在这种情况下,它可能无关紧要,但在某些情况下,您可能会遇到各种奇怪的问题,因为frame 位于视图的父视图的坐标系中,而bounds 是相关视图的坐标系。


    就个人而言,如果您要将这段代码保留在视图控制器中,我建议:

    • viewDidLoad中添加形状图层和渐变;
    • viewDidLayoutSubviews更新梯度的路径和边界;
    • 我会将这些不同的形状层和渐变方法放在它们自己的私有扩展中。

    更好的是,所有这些动画代码根本不属于应用的视图控制器,而是一个UIView 子类(或子视图控制器)。

    因此,也许:

    @IBDesignable
    public class GradientProgressView: UIView {
    
        private var shapeLayer: CAShapeLayer = {
            let shapeLayer = CAShapeLayer()
            shapeLayer.strokeColor = UIColor.green.cgColor
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.lineCap = .round
            return shapeLayer
        }()
    
        private var trackLayer: CAShapeLayer = {
            let trackLayer = CAShapeLayer()
            trackLayer.strokeColor = UIColor.groupTableViewBackground.cgColor
            trackLayer.fillColor = UIColor.clear.cgColor
            trackLayer.lineCap = .round
            return trackLayer
        }()
    
        private var gradient: CAGradientLayer = {
            let gradient = CAGradientLayer()
    
            let color = UIColor(red: 11/255, green: 95/255, blue: 244/255, alpha: 1).cgColor
            let sndColor = UIColor(red: 255/255, green: 87/255, blue: 87/255, alpha: 1).cgColor
    
            gradient.colors = [color, sndColor]
            gradient.locations = [0.0, 1.0]
            gradient.startPoint = CGPoint(x: 0, y: 0)
            gradient.endPoint = CGPoint(x: 1, y: 0)
    
            return gradient
        }()
    
        override init(frame: CGRect = .zero) {
            super.init(frame: frame)
            addSubLayers()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            addSubLayers()
        }
    
        override public func layoutSubviews() {
            super.layoutSubviews()
            updatePaths()
        }
    
        override public func prepareForInterfaceBuilder() {
            super.prepareForInterfaceBuilder()
            setProgress(0.75, animated: false)
        }
    
        public func setProgress(_ progress: CGFloat, animated: Bool = true) {
            if animated {
                animateStroke(to: progress)
            } else {
                shapeLayer.strokeEnd = progress
            }
        }
    }
    
    private extension GradientProgressView {
        func addSubLayers() {
            layer.addSublayer(trackLayer)
            layer.addSublayer(shapeLayer)
            layer.addSublayer(gradient)
        }
    
        func updatePaths() {
            let lineWidth = bounds.height / 2
            trackLayer.lineWidth = lineWidth * 0.75
            shapeLayer.lineWidth = lineWidth
    
            let path = UIBezierPath()
            path.move(to:    CGPoint(x: bounds.minX + lineWidth / 2, y: bounds.midY))
            path.addLine(to: CGPoint(x: bounds.maxX - lineWidth / 2, y: bounds.midY))
    
            trackLayer.path = path.cgPath
            shapeLayer.path = path.cgPath
    
            gradient.frame = bounds
            gradient.mask = shapeLayer
        }
    
        func animateStroke(to progress: CGFloat) {
            let key = "lineStrokeAnimation"
    
            layer.removeAnimation(forKey: key)
    
            let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
    
            basicAnimation.toValue = progress
            basicAnimation.duration = 1.5
            basicAnimation.fillMode = .forwards
            basicAnimation.isRemovedOnCompletion = false
            basicAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    
            shapeLayer.add(basicAnimation, forKey: key)
        }
    }
    

    那么视图控制器仅仅是:

    class ViewController: UIViewController {
    
        @IBOutlet weak var gradientProgressView: GradientProgressView!
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            updateProgress()
        }
    
        ...
    }
    
    // MARK: - Progress related methods
    
    private extension ViewController {
        func updateProgress() {
            let distance = currLeasingCar!.currentKm - currLeasing!.startKm
            let value = CGFloat(distance) / CGFloat(finalKm)
            gradientProgressView.setProgress(value)
        }
    }
    

    【讨论】:

    • 谢谢!工作正常 - 关于viewDidLayoutSubviews(),应该在哪里调用displayLine()
    • @Tim - 作为一个更高级的观察,无论如何,我不相信任何这段代码真的属于视图控制器。最好让它成为自己的类。
    • 我将您的代码用于我正在处理的项目。它很好用,但我有一个关于更改 CAShapeLayer 颜色的问题。你能不能看看它。 stackoverflow.com/q/71566636/4833705。它基于您的回答并进行了一些调整
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多