为 UISlider 自定义形状并在 Swift 5 中更新进度

Custom shape to UISlider and update progress in Swift 5

我已经检查了一些关于自定义 UIView 滑块的 Whosebug 答案,但是使用它们我无法像这样制作滑块。这样就形成了一个圆或半圆。我已经找到了一些使用 UIView 制作圆形滑块的库,但它对我没有帮助所以任何人都可以帮助我。我怎样才能像下面的 UIImage 那样制作滑块?谢谢!

您可能会自行推出。 (您显然可以搜索第三方实现,但这超出了 Whosebug 的范围。)有很多方法可以解决这个问题,但这里的基本要素是:

  1. 整个路径的粉红色圆弧。就个人而言,我会为此使用 CAShapeLayer
  2. 从开始到当前进度(从0到1测量)的白色圆弧。同样,CAShapeLayer 是合乎逻辑的。
  3. 当前进度处的白点。下面我创建了一个白色背景的 CALayer,然后应用 CAGradientLayer 作为遮罩。您也可以为此创建一个 UIImage
  4. 关于如何设置进度,你会把粉色和白色圆弧的路径设置成相同的路径,只是更新白色圆弧的strokeEnd。您也可以相应地调整白点图层的位置。
  5. 这里唯一复杂的事情是找出圆弧的中心。在我下面的例子中,我根据视图的边界用一些三角函数计算它,这样弧从左下角到顶部,然后回到右下角。或者您可以将圆弧的中心作为参数传递。

无论如何,它可能看起来像:

@IBDesignable
class ArcView: UIView {
    @IBInspectable
    var lineWidth: CGFloat = 7 { didSet { updatePaths() } }

    @IBInspectable
    var progress: CGFloat = 0 { didSet { updatePaths() } }

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()

        progress = 0.35
    }

    lazy var currentPositionDotLayer: CALayer = {
        let layer = CALayer()
        layer.backgroundColor = UIColor.white.cgColor
        let rect = CGRect(x: 0, y: 0, width: lineWidth * 3, height: lineWidth * 3)
        layer.frame = rect

        let gradientLayer = CAGradientLayer()
        gradientLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
        gradientLayer.type = .radial
        gradientLayer.frame = rect
        gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        gradientLayer.locations = [0.5, 1]

        layer.mask = gradientLayer

        return layer
    }()

    lazy var progressShapeLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.lineCap = .round
        shapeLayer.lineWidth = lineWidth
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
        return shapeLayer
    }()

    lazy var totalShapeLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.lineCap = .round
        shapeLayer.lineWidth = lineWidth
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = #colorLiteral(red: 0.9439327121, green: 0.5454334617, blue: 0.6426400542, alpha: 1)
        return shapeLayer
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        configure()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        updatePaths()
    }
}

// MARK: - Private utility methods

private extension ArcView {

    func configure() {
        layer.addSublayer(totalShapeLayer)
        layer.addSublayer(progressShapeLayer)
        layer.addSublayer(currentPositionDotLayer)
    }

    func updatePaths() {
        let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
        let halfWidth = rect.width / 2
        let height = rect.height
        let theta = atan(halfWidth / height)
        let radius = sqrt(halfWidth * halfWidth + height * height) / 2 / cos(theta)
        let center = CGPoint(x: rect.midX, y: rect.minY + radius)
        let delta = (.pi / 2 - theta) * 2
        let startAngle = .pi * 3 / 2 - delta
        let endAngle = .pi * 3 / 2 + delta

        let path = UIBezierPath(arcCenter: center,
                                radius: radius,
                                startAngle: startAngle,
                                endAngle: endAngle,
                                clockwise: true)

        progressShapeLayer.path = path.cgPath // white arc
        totalShapeLayer.path = path.cgPath    // pink arc
        progressShapeLayer.strokeEnd = progress

        let currentAngle = (endAngle - startAngle) * progress + startAngle
        let dotCenter = CGPoint(x: center.x + radius * cos(currentAngle),
                                y: center.y + radius * sin(currentAngle))
        currentPositionDotLayer.position = dotCenter
    }
}

上面,我设置了 ArcView 的背景颜色,所以你可以看到它的 bounds,但显然你会把背景颜色设置为透明的。

现在您可以添加大量附加功能(例如,添加用户交互以便您可以“清理”它等)。例如,参见 https://github.com/robertmryan/ArcView。但是设计这类东西的关键是将其分解成组成元素,将一个元素叠加在另一个元素之上。


您可以通过编程方式设置 arcViewprogress 以使其在值 0 和 1 之间更改当前值:

func startUpdating() {
    arcView.progress = 0

    var current = 0

    Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] timer in
        current += 1

        guard let self = self, current <= 10 else {
            timer.invalidate()
            return
        }

        self.arcView.progress = CGFloat(current) / 10
    }
}

导致: