【问题标题】:Custom shape to UISlider and update progress in Swift 5UISlider 的自定义形状并在 Swift 5 中更新进度
【发布时间】:2021-02-13 15:27:10
【问题描述】:

我已经检查了一些关于自定义 UIView 滑块的 StackOverflow 答案,但使用它们我无法制作这样的滑块。这会形成一个圆圈或半个圆圈。我想出了一些使用 UIView 制作圆形滑块的库,但它对我没有帮助,所以任何人都可以帮助我。如何制作 UIImage 下面的滑块?谢谢!

【问题讨论】:

    标签: swift uiview swift5 uislider


    【解决方案1】:

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

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

    导致:

    【讨论】:

    • 您好@Rob,非常感谢您抽出宝贵时间。我将此代码添加到我的项目中,一切正常,但面临一个问题,进度 = 0.35,但它显示为 0。如果我在情节提要中更改此进度值,那么它不能以编程方式工作,它不起作用?
    • 我检查了 prepareForInterfaceBuilder 这个方法没有调用。
    • 你好@Rob,我在移动进度方面遇到了一些问题,你能帮我解决这个问题吗?我尝试使用 UIView Touch 操作,但总是得到 CALayer 并混淆是否需要添加位置或什么?并尝试 CATransaction 但不工作。如果你能帮我解决这个问题,我会更新这个问题。谢谢!
    • 关于不更新的问题,我不知道为什么它不会为您更新。请参阅此示例,如上图所示:github.com/robertmryan/ArcView
    • @YogeshPatel - 重新接触,我刚刚推送了提交,展示了该模式的示例。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-13
    • 1970-01-01
    相关资源
    最近更新 更多