【问题标题】:Half circle progression bar with stripes带条纹的半圆进度条
【发布时间】:2023-02-23 04:07:22
【问题描述】:

我想为进度条绘制以下组件:

到目前为止,用我的代码我可以画出带条纹的半圆,但我不能让条纹停在圆的 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% 进度时的样子:

【问题讨论】:

    标签: ios swift xcode uikit


    【解决方案1】:

    几个建议...

    首先,不要对大小/位置值进行硬编码,而是将 HalfCircleProgressView 保持在 2:1 比率,并使您的弧线完全适合视图:

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

    并相对于 HalfCircleProgressView 放置其他元素(标签等):

    并将背景设置为清除:

    接下来,使用 CAShapeLayer 而不是覆盖 draw()

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

    使用 CAShapeLayer 有几个优点——对于您的特定需求,最大的一个是我们可以使用 .strokeEnd 属性“自动”管理前景弧。

    因此,如果背景和前景“弧形”图层使用相同的路径和线宽属性:

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

    foregroundLayer.strokeEnd = 0.25
    

    对于 80%:

    foregroundLayer.strokeEnd = 0.8
    

    那么,“刻度线”呢?

    好吧,看起来你想要标记(我猜):

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

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

    形状图层路径在“中线上”描边,因此我们需要循环遍历“间隙”步骤,计算 15 点厚弧的“外边缘”上的角度点,计算角度点“内边缘”上的角度,然后是.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 开始每条路径到第一个刻度线的角度并循环七次(对于 7 个刻度线)...每次通过循环递增角度并添加线段:

        // 20-degree tick spacing
        let angleInc: CGFloat = .pi / 9.0
        var angle: CGFloat = .pi * 1.5 - angleInc * 4.0
    
        for _ in 1...7 {
            tickInnerPath.addArc(withCenter: center, radius: tickInnerRadius, startAngle: angle, endAngle: angle + angleInc, clockwise: true)
            tickOuterPath.addArc(withCenter: center, radius: tickOuterRadius, startAngle: angle, endAngle: angle + angleInc, clockwise: true)
            tickPath.move(to: tickInnerPath.currentPoint)
            tickPath.addLine(to: tickOuterPath.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 {
                tickInnerPath.addArc(withCenter: center, radius: tickInnerRadius, startAngle: angle, endAngle: angle + angleInc, clockwise: true)
                tickOuterPath.addArc(withCenter: center, radius: tickOuterRadius, startAngle: angle, endAngle: angle + angleInc, clockwise: true)
                tickPath.move(to: tickInnerPath.currentPoint)
                tickPath.addLine(to: tickOuterPath.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()
        }
        
    }
    

    运行时它看起来像这样:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-09
      • 2022-07-19
      • 1970-01-01
      • 1970-01-01
      • 2017-07-27
      • 1970-01-01
      • 1970-01-01
      • 2016-08-06
      相关资源
      最近更新 更多