ios - CAShapeLayer() 绘制奇怪的线条/路径

标签 ios swift cashapelayer cabasicanimation

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

怎么会是这个样子?

我的代码:

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

最佳答案

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

enter image description here

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

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。或者,如果您这样做,请确保删除前一个。

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


就个人而言,如果您要将这段代码保留在 View Controller 中,我建议:

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

更好的是,所有这些动画代码根本不属于应用程序的 View Controller ,而是 UIView 子类(或 subview Controller )。

因此,也许:

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

那么 View Controller 只是:

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

关于ios - CAShapeLayer() 绘制奇怪的线条/路径,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56364941/

相关文章:

c++ - Objective-c iOS 中的二维码生成器

ios - iOS 和 Heroku 上的 FB 平台示例应用 WishList 失败

ios - URL 链接是具有多个部分的 UiTableView

ios - 圆形 GAGradient 层上的锯齿状边缘

ios - 如何在WKWebView中启用嵌入的YouTube视频?

objective-c - 如何识别从左到右的滑动 Action ?

swift - Firebase:访问通过 childByAutoID 存储的数据

swift - 如何在 Swift 中从 CMutablePointer<CGFloat> 到 CGFloat[]

ios - 在贝塞尔曲线内绘制子图层

ios - CAShapeLayer 没有在 uiview 中正确居中