绘制圆角矩形进度条,315°开始/结束

问题描述 投票:0回答:2

我通过

UIBezierPath
CAShapeLayer
有一个圆角矩形进度条。进度笔划动画当前从顶部中心开始顺时针绘制 360 度。

我目前的设置是笔划从 315 度开始,但在顶部中心结束,而且我承认我迷路了。我的目标是以 315 度开始/结束划水。任何指导将不胜感激!

class ProgressBarView: UIView {
    
    let progressLayer = CAShapeLayer()
    let cornerRadius: CGFloat = 20
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupProgressLayer()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupProgressLayer()
    }
    
    private func setupProgressLayer() {
        
        progressLayer.lineWidth = 6
        progressLayer.fillColor = nil
        progressLayer.strokeColor = Constants.style.offWhite.cgColor
        progressLayer.strokeStart = 135 / 360
        progressLayer.lineCap = .round
        
        
        let lineWidth: CGFloat = 6
        let radius = bounds.height / 2 - lineWidth / 2
        let progressPath = UIBezierPath()
        progressPath.move(to: CGPoint(x: lineWidth / 2 + cornerRadius, y: lineWidth / 2))
        progressPath.addLine(to: CGPoint(x: bounds.width - lineWidth / 2 - cornerRadius, y: lineWidth / 2))
        progressPath.addArc(withCenter: CGPoint(x: bounds.width - lineWidth / 2 - cornerRadius, y: lineWidth / 2 + cornerRadius), radius: cornerRadius, startAngle: -CGFloat.pi / 2, endAngle: 0, clockwise: true)
        progressPath.addLine(to: CGPoint(x: bounds.width - lineWidth / 2, y: bounds.height - lineWidth / 2 - cornerRadius))
        progressPath.addArc(withCenter: CGPoint(x: bounds.width - lineWidth / 2 - cornerRadius, y: bounds.height - lineWidth / 2 - cornerRadius), radius: cornerRadius, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true)
        progressPath.addLine(to: CGPoint(x: lineWidth / 2 + cornerRadius, y: bounds.height - lineWidth / 2))
        progressPath.addArc(withCenter: CGPoint(x: lineWidth / 2 + cornerRadius, y: bounds.height - lineWidth / 2 - cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: true)
        progressPath.addLine(to: CGPoint(x: lineWidth / 2, y: lineWidth / 2 + cornerRadius))
        progressPath.addArc(withCenter: CGPoint(x: lineWidth / 2 + cornerRadius, y: lineWidth / 2 + cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: -CGFloat.pi / 2, clockwise: true)
        progressPath.close()
        
        progressLayer.path = progressPath.cgPath
        
        
        layer.addSublayer(progressLayer)
    }
    
    func setProgress(_ progress: CGFloat) {
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = progressLayer.strokeStart
        animation.toValue = progress
        animation.duration = 1
        progressLayer.add(animation, forKey: "progressAnimation")
        progressLayer.strokeEnd = progress
    }
}
ios swift uibezierpath cashapelayer cabasicanimation
2个回答
0
投票

如果您愿意放弃圆形线帽,一种写法是使用遮罩层。这个想法是你的主层的路径总是完整的圆角矩形,而遮罩层是一个圆圈,其中一个楔子被取出来隐藏圆角矩形的一部分。它看起来像这样:

这是我修改后的

ProgressBarView
代码。请注意,绘制圆角矩形的代码要简单得多,因为我总是绘制整个东西,而且因为我使用
insetBy(dx:dy:)
而不是重复添加/减去
lineWidth / 2
.

class ProgressBarView: UIView {
    var progress: CGFloat = 0.5 {
        didSet {
            setMaskShape()
        }
    }

    let maskLayer = CAShapeLayer()

    var cornerRadius: CGFloat { 20 }
    var lineWidth: CGFloat { 6 }

    override class var layerClass: AnyClass { CAShapeLayer.self }

    override func layoutSubviews() {
        super.layoutSubviews()

        let layer = self.layer as! CAShapeLayer

        if layer.mask != maskLayer {
            layer.lineWidth = lineWidth
            layer.fillColor = nil
            layer.strokeColor = UIColor.gray.cgColor
            // layer.strokeStart = 135 / 360

            layer.mask = maskLayer
        }

        layer.path = CGPath(
            roundedRect: bounds.insetBy(dx: 0.5 * lineWidth, dy: 0.5 * lineWidth),
            cornerWidth: cornerRadius,
            cornerHeight: cornerRadius,
            transform: nil
        )
        maskLayer.frame = bounds

        setMaskShape()
    }

    private func setMaskShape() {
        let bounds = maskLayer.bounds
        let center = CGPoint(x: bounds.midX, y: bounds.midY)

        let path = CGMutablePath()
        path.move(to: center)
        path.addRelativeArc(
            center: center,
            radius: bounds.size.width + bounds.size.height,
            startAngle: 0.625 * 2 * .pi,
            delta: progress * 2 * .pi
        )
        path.closeSubpath()
        maskLayer.path = path
    }
}

这是我用来测试它的 SwiftUI 代码:

struct ContentView: View {
    @State var progress: CGFloat = 0.5

    var body: some View {
        VStack {
            ProgressBarViewRep(progress: progress)
                .aspectRatio(1, contentMode: .fit)

            Slider(value: $progress, in: 0 ... 1)
        }
        .padding()
    }
}

struct ProgressBarViewRep: UIViewRepresentable {
    var progress: CGFloat

    func makeUIView(context: Context) -> ProgressBarView {
        return ProgressBarView()
    }

    func updateUIView(_ uiView: ProgressBarView, context: Context) {
        uiView.progress = progress
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .padding()
    }
}

0
投票

如果您真的想要圆形线帽,或者您希望能够使用

UIView
CALayer
动画轻松地动画化进度,您需要绘制路径,使其以 315° 开始和结束。

因为315°在其中一个圆角的中间,所以你必须从一个跨越45°的圆弧开始绘图,并以另一个45°的圆弧结束它。但是,您可以使用专门为绘制圆角设计的

CGMutablePath
不同方法绘制其他三个角,从而简化代码。

class ProgressBarView: UIView {
    var progress: CGFloat {
        get { (layer as! CAShapeLayer).strokeEnd }
        set { (layer as! CAShapeLayer).strokeEnd = newValue }
    }

    var cornerRadius: CGFloat { 20 }
    var lineWidth: CGFloat { 6 }

    override class var layerClass: AnyClass { CAShapeLayer.self }

    override func layoutSubviews() {
        super.layoutSubviews()

        let layer = layer as! CAShapeLayer
        layer.lineCap = .round
        layer.lineWidth = lineWidth
        layer.fillColor = nil
        layer.strokeColor = UIColor.gray.cgColor

        let rect = layer.bounds.insetBy(dx: 0.5 * lineWidth, dy: 0.5 * lineWidth)
        let (x0, y0, x1, y1) = (rect.minX, rect.minY, rect.maxX, rect.maxY)

        let r = cornerRadius

        let path = CGMutablePath()
        // No prior move, so path automatically moves to the start of this arc.
        path.addRelativeArc(
            center: CGPoint(x: x0 + r, y: y0 + r),
            radius: r,
            startAngle: 0.625 * 2 * .pi, // 45° above -x axis
            delta: 0.125 * 2 * .pi // 45°
        )
        var current = CGPoint(x: x1, y: y0)
        for next in [
            CGPoint(x: x1, y: y1),
            CGPoint(x: x0, y: y1),
            CGPoint(x: x0, y: y0)
        ] {
            path.addArc(tangent1End: current, tangent2End: next, radius: r)
            current = next
        }

        path.addRelativeArc(
            center: CGPoint(x: x0 + r, y: y0 + r),
            radius: r,
            startAngle: 0.5 * 2 * .pi, // -x axis
            delta: 0.125 * 2 * .pi // 45°
        )
        path.closeSubpath()

        layer.path = path
    }
}

我没有将

CAShapeLayer
设置为视图层的子层,而是告诉视图直接使用
CAShapeLayer
。这样做的好处是,如果我们在
UIView.animate
块中进行这些更改,UIKit 将为图层属性的更改设置动画。这是演示代码。请注意,当我更新视图的
progress
属性时,我是如何在动画块内进行操作的。我不必直接创建
CABasicAnimation

struct ContentView: View {
    @State var isComplete = true

    var body: some View {
        VStack {
            ProgressBarViewRep(progress: isComplete ? 1 : 0)
                .aspectRatio(1, contentMode: .fit)

            Toggle("Complete", isOn: $isComplete)
                .toggleStyle(.button)
                .fixedSize()
        }
        .padding()
    }
}

struct ProgressBarViewRep: UIViewRepresentable {
    var progress: CGFloat

    func makeUIView(context: Context) -> ProgressBarView {
        let view = ProgressBarView()
        view.progress = progress
        return view
    }

    func updateUIView(_ uiView: ProgressBarView, context: Context) {
        UIView.animate(withDuration: 1, delay: 0, options: .curveLinear) {
            uiView.progress = progress
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .padding()
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.