xcode 带条纹的半圆进度条

dgsult0t  于 2023-02-25  发布在  其他
关注(0)|答案(1)|浏览(119)

我想画一个进度条的如下部分:

用我的代码到目前为止,我可以画半圈的条纹,但我不能让条纹停止在1/3的圆圈,他们去所有的方式结束。另外,我如何添加白色背景?以下是我目前的代码:

class HalfCircleProgressView: UIView {
    
    var progress: CGFloat = 0.0 {
        didSet {
            setNeedsDisplay()
        }
    }
    
    private let backgroundLayerOne = CAShapeLayer()
    
    override func draw(_ rect: CGRect) {
        
        
        let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2 + 50)
        let radius = min(bounds.width + 150, bounds.height + 150) / 2 - 50
        let startAngle = Double.pi
        let endAngle = startAngle + progress * CGFloat(Double.pi)
        
       
    
        let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        path.lineWidth = 15
        UIColor.white.setStroke()
        path.stroke()
        
        let stripePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        stripePath.lineWidth = 15
        let dashes: [CGFloat] = [52.5, 2]
        stripePath.setLineDash(dashes, count: dashes.count, phase: 0)
        UIColor.red.setStroke()
        stripePath.stroke()
    }
}

这是我当前进度条在进度达到80%时的样子:

nafvub8i

nafvub8i1#

几个建议...
首先,不要硬编码大小/位置值,而是将HalfCircleProgressView保持为2:1比率,并使弧完全适合视图:

然后,您可以将该视图嵌入到“容器”中:

并相对于HalfCircleProgressView

并将背景设置为清晰:

接下来,使用CAShapeLayer而不是重写draw()

  • 一层用于白色“背景”弧
  • 红色“前景”弧一层
  • 一层用于“刻度标记”

使用CAShapeLayer有几个优点--对于您的特殊需要,最大的优点是我们可以使用.strokeEnd属性“自动管理”前景弧及其完成的“百分比”。
因此,如果背景和前景“arc”图层使用相同的path和lineWidth属性:

当我们想要将红色“进度”弧设置为,比如说,25%时,我们设置:

foregroundLayer.strokeEnd = 0.25

对于80%:

foregroundLayer.strokeEnd = 0.8

不需要从百分比计算Angular 和重新绘制一切。
那么,“勾号”是怎么回事?
好吧,看起来你想在(我猜):

210° / 230° / 250° / 270° / 290° / 310° / 330°

我们将使用带有线段的贝塞尔曲线路径作为“刻度线”形状层的.cgPath,而不是在前景弧线上使用蒙版图案。

形状层路径是“在中线上”描边的,因此我们需要循环“间隙”步骤,计算15点粗弧的“外边缘”上的Angular 点,计算“内边缘”上的Angular 点,然后计算.move(to:).addLine(to:)
我们 * 可以 * 为这些计算编写一些数学公式...或者,我们可以***“作弊”***并利用UIBezierPath.currentPoint属性!
当我们像这样操纵路径时:

let bez = UIBezierPath()
bez.move(to: pt)
bez.addArc(withCenter: c, radius: r, startAngle: a1, endAngle: a2, clockwise: true)

我们可以得到该弧的“终点”:

bez.currentPoint

因此,为了添加线段,我们将创建半径为radius PLUS lineWidth * 0.5的“outerPath”和半径为radius MINUS lineWidth * 0.5的“innerPath”,然后我们以.addArc开始每条路径,到第一个刻度的Angular ,并循环七次(7个刻度线)......每次通过循环增加Angular 并添加线段:

// 20-degree tick spacing
    let angleInc: CGFloat = .pi / 9.0

    // start at 270-degrees minus 4 * spacing
    var angle: CGFloat = .pi * 1.5 - angleInc * 4.0

    for _ in 1...7 {
        tickOuterPath.addArc(withCenter: center, radius: tickOuterRadius, startAngle: angle, endAngle: angle + angleInc, clockwise: true)
        tickInnerPath.addArc(withCenter: center, radius: tickInnerRadius, startAngle: angle, endAngle: angle + angleInc, clockwise: true)
        tickPath.move(to: tickOuterPath.currentPoint)
        tickPath.addLine(to: tickInnerPath.currentPoint)
        angle += angleInc
    }

一直到第7圈:

当然,我们不会看到那些额外的路径:

因此,我们的自定义视图现在在100%时如下所示:

占33%:

占78.27%:

下面是一些可以使用的示例代码:

class HalfCircleProgressView: UIView {
    
    public var progress: CGFloat = 0.0 {
        didSet {
            // keep progress between 0.0 and 1.0
            progress = max(0.0, min(1.0, progress))
            // update layer stroke end
            foregroundLayer.strokeEnd = progress
        }
    }
    
    public func setProgress(_ v: CGFloat, animated: Bool) {
        
        CATransaction.begin()
        if !animated {
            // disable CALayer "built-in" animation
            CATransaction.setDisableActions(true)
        }
        self.progress = v
        CATransaction.commit()
        
    }
    
    private let backgroundLayer = CAShapeLayer()
    private let foregroundLayer = CAShapeLayer()
    private let ticksLayer = CAShapeLayer()
    
    private let lineWidth: CGFloat = 15
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        
        // properties common to all layers
        [backgroundLayer, foregroundLayer, ticksLayer].forEach { lay in
            lay.fillColor = UIColor.clear.cgColor
            layer.addSublayer(lay)
        }
        
        backgroundLayer.strokeColor = UIColor.white.cgColor
        foregroundLayer.strokeColor = UIColor.red.cgColor
        ticksLayer.strokeColor = UIColor.white.cgColor
        
        backgroundLayer.lineWidth = lineWidth
        foregroundLayer.lineWidth = lineWidth
        ticksLayer.lineWidth = 2.0
        
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        let center: CGPoint = CGPoint(x: bounds.midX, y: bounds.maxY)
        let w: CGFloat = bounds.width - lineWidth
        let h: CGFloat = bounds.height - lineWidth * 0.5
        let radius: CGFloat = min(w * 0.5, h)
        let startAngle: CGFloat = .pi
        let endAngle: CGFloat = 0.0
        
        let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        
        backgroundLayer.path = path.cgPath
        foregroundLayer.path = path.cgPath
        
        let tickInnerRadius: CGFloat = radius - lineWidth * 0.5
        let tickOuterRadius: CGFloat = radius + lineWidth * 0.5
        
        let tickInnerPath = UIBezierPath()
        let tickOuterPath = UIBezierPath()
        let tickPath = UIBezierPath()
        
        // 20-degree tick spacing
        let angleInc: CGFloat = .pi / 9.0
        
        // start at 270-degrees minus 4 * spacing
        var angle: CGFloat = .pi * 1.5 - angleInc * 4.0
        
        for _ in 1...7 {
            tickOuterPath.addArc(withCenter: center, radius: tickOuterRadius, startAngle: angle, endAngle: angle + angleInc, clockwise: true)
            tickInnerPath.addArc(withCenter: center, radius: tickInnerRadius, startAngle: angle, endAngle: angle + angleInc, clockwise: true)
            tickPath.move(to: tickOuterPath.currentPoint)
            tickPath.addLine(to: tickInnerPath.currentPoint)
            angle += angleInc
        }
        
        ticksLayer.path = tickPath.cgPath
        
        foregroundLayer.strokeEnd = progress
        
    }
    
}

以及一个示例控制器,其中包含一些“百分比按钮”和一个用于更改“进度”百分比的滑块:

class HalfCircleTestVC: UIViewController {
    
    let hcpView = HalfCircleProgressView()
    
    // add a label to show the progress percent
    let pctLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        
        // we want the arc to be inset a bit, so we'll embed it in a "container"
        let container = UIView()
        container.backgroundColor = .systemYellow
        
        container.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(container)
        
        hcpView.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(hcpView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            container.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            container.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            container.heightAnchor.constraint(equalTo: container.widthAnchor, multiplier: 0.6),
            container.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            // let's inset our progress view by 40-points on each side
            hcpView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 40.0),
            hcpView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -40.0),
            
            // give hcpView a 2:1 ratio
            hcpView.heightAnchor.constraint(equalTo: hcpView.widthAnchor, multiplier: 1.0 / 2.0),
            
            hcpView.centerYAnchor.constraint(equalTo: container.centerYAnchor),
            
        ])
        
        pctLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(pctLabel)
        // add the pctLabel in the middle of the arc
        NSLayoutConstraint.activate([
            pctLabel.centerXAnchor.constraint(equalTo: hcpView.centerXAnchor),
            pctLabel.centerYAnchor.constraint(equalTo: hcpView.centerYAnchor),
        ])
        
        // let's add some percent / progress buttons
        let btnStack = UIStackView()
        btnStack.spacing = 4
        btnStack.distribution = .fillEqually
        [0.0, 0.1, 0.25, 0.33, 0.61, 0.8, 1.0].forEach { v in
            let b = UIButton()
            b.setTitle("\(v)", for: [])
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.lightGray, for: .highlighted)
            b.titleLabel?.font = .systemFont(ofSize: 13.0, weight: .bold)
            b.backgroundColor = .systemGreen
            b.layer.cornerRadius = 6
            b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
            btnStack.addArrangedSubview(b)
        }
        
        btnStack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(btnStack)
        
        // let's add a slider to set the progress
        let slider = UISlider()
        slider.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(slider)
        
        NSLayoutConstraint.activate([
            
            btnStack.topAnchor.constraint(equalTo: container.bottomAnchor, constant: 20.0),
            btnStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            btnStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            slider.topAnchor.constraint(equalTo: btnStack.bottomAnchor, constant: 20.0),
            slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
        ])
        
        slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
        
        sliderChanged(slider)
        
        container.backgroundColor = .lightGray
    }
    
    func updatePercentLabel() {
        let pct = hcpView.progress
        pctLabel.text = String(format: "%0.2f %%", pct * 100.0)
    }
    
    @objc func btnTapped(_ sender: UIButton) {
        if let t = sender.currentTitle {
            let v = (t as NSString).floatValue
            // set progress directly
            //  this will use CALayer "built-in" animation
            hcpView.progress = CGFloat(v)
            updatePercentLabel()
        }
    }
    @objc func sliderChanged(_ sender: UISlider) {
        // we want to update the progress WHILE dragging the slider
        //  so, set progress WITHOUT animation
        //  otherwise, we get a "lag"
        hcpView.setProgress(CGFloat(sender.value), animated: false)
        updatePercentLabel()
    }
    
}

运行时将如下所示:

相关问题