具有不同颜色的 CAShapeLayer

CAShapeLayer with different Colors

我有一个基于此 的 CAShapeLayer,它与 UISlider 一起设置动画。

它工作正常,但随着 shapeLayer 跟随它只有 1 个红色 CAGradientLayer 颜色。我想要的是根据滑块的某些点更改颜色的 shapeLayer。一个例子是 0.4 - 0.5 是红色,0.7-0.8 是红色,0.9-0.95 是红色。这些不是实际值,实际值会有所不同。我认为任何时候它不满足变红的条件它应该只是一种清晰的颜色,它只会显示它下面的黑色轨道。结果看起来像这样(不要管形状)

红色基于用户擦洗滑块和放手。确定红色的滑块的不同位置取决于任何条件。我该怎么做。

UISlider

lazy var slider: UISlider = {
    let s = UISlider()
    s.translatesAutoresizingMaskIntoConstraints = false
    s.minimumTrackTintColor = .blue
    s.maximumTrackTintColor = .white
    s.minimumValue = 0
    s.maximumValue = 1
    s.addTarget(self, action: #selector(onSliderChange), for: .valueChanged)
    return s
    s.addTarget(self, action: #selector(onSliderEnded), for: [.touchUpInside, .touchUpOutside, .touchCancel])
    return s
}()

lazy var progressView: GradientProgressView = {
    let v = GradientProgressView()
    v.translatesAutoresizingMaskIntoConstraints = false
    return v
}()

@objc fileprivate func onSliderChange(_ slider: UISlider) {

    let condition: Bool = // ...

    let value = slider.value
    progressView.setProgress(CGFloat(value), someCondition: condition, slider_X_Position: slider_X_PositionInView())
}

@objc fileprivate func onSliderEnded(_ slider: UISlider) {

    let value = slider.value
    progressView.resetProgress(CGFloat(value))
}

// ... progressView is the same width as the the slider

func slider_X_PositionInView() -> CGFloat {
    
    let trackRect = slider.trackRect(forBounds: slider.bounds)
    let thumbRect = slider.thumbRect(forBounds: slider.bounds,
                                           trackRect: trackRect,
                                           value: slider.value)

    let convertedThumbRect = slider.convert(thumbRect, to: self.view)
    
    return convertedThumbRect.midX
}

GradientProgressView:

public class GradientProgressView: UIView {

    var shapeLayer: CAShapeLayer = {
       // ...
    }()

    private var trackLayer: CAShapeLayer = {
        let trackLayer = CAShapeLayer()
        trackLayer.strokeColor = UIColor.black.cgColor
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineCap = .round
        return trackLayer
    }()

    private var gradient: CAGradientLayer = {
        let gradient = CAGradientLayer()
        let redColor = UIColor.red.cgColor
        gradient.colors = [redColor, redColor]
        gradient.locations = [0.0, 1.0]
        gradient.startPoint = CGPoint(x: 0, y: 0)
        gradient.endPoint = CGPoint(x: 1, y: 0)
        return gradient
    }()

    // ... add the above layers as subLayers to self ...

    func updatePaths() { // added in layoutSubviews

        let lineWidth = bounds.height / 2
        trackLayer.lineWidth = lineWidth * 0.75
        shapeLayer.lineWidth = lineWidth

        let path = UIBezierPath()
        path.move(to: CGPoint(x: bounds.minX + lineWidth / 2, y: bounds.midY))
        path.addLine(to: CGPoint(x: bounds.maxX - lineWidth / 2, y: bounds.midY))

        trackLayer.path = path.cgPath
        shapeLayer.path = path.cgPath

        gradient.frame = bounds
        gradient.mask = shapeLayer
        
        shapeLayer.duration = 1
        shapeLayer.strokeStart = 0
        shapeLayer.strokeEnd = 0
    }

    public func setProgress(_ progress: CGFloat, someCondition: Bool, slider_X_Position: CGFloat) {

        // slider_X_Position might help with shapeLayer's x position for the colors ???  

        if someCondition {
             // redColor until the user lets go
        } else {
            // otherwise always a clearColor
        }

        shapeLayer.strokeEnd = progress
    }
}

    public func resetProgress(_ progress: CGFloat) {

        // change to clearColor after finger is lifted
    }
}

得到这个:

我们可以在该形状层上使用 CAShapeLayer 作为红色“框”,使用 CALayer 作为 .mask

为了显示/覆盖框,我们将遮罩层的框架设置为边界宽度的百分比。

这是一个完整的例子:

class StepView: UIView {
    public var progress: CGFloat = 0 {
        didSet {
            setNeedsLayout()
        }
    }
    public var steps: [[CGFloat]] = [[0.0, 1.0]] {
        didSet {
            setNeedsLayout()
        }
    }
    public var color: UIColor = .red {
        didSet {
            stepLayer.fillColor = color.cgColor
        }
    }
    
    private let stepLayer = CAShapeLayer()
    private let maskLayer = CALayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        backgroundColor = .black
        layer.addSublayer(stepLayer)
        stepLayer.fillColor = color.cgColor
        stepLayer.mask = maskLayer
        // mask layer can use any solid color
        maskLayer.backgroundColor = UIColor.white.cgColor
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        
        stepLayer.frame = bounds
        
        let pth = UIBezierPath()
        steps.forEach { pair in
            // rectangle for each "percentage pair"
            let w = bounds.width * (pair[1] - pair[0])
            let b = UIBezierPath(rect: CGRect(x: bounds.width * pair[0], y: 0, width: w, height: bounds.height))
            pth.append(b)
        }
        stepLayer.path = pth.cgPath
        
        // update frame of mask layer
        var r = bounds
        r.size.width = bounds.width * progress
        maskLayer.frame = r
        
    }
}

class StepVC: UIViewController {
    let stepView = StepView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        stepView.translatesAutoresizingMaskIntoConstraints = false
        
        let slider = UISlider()
        slider.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(stepView)
        view.addSubview(slider)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
            stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            stepView.heightAnchor.constraint(equalToConstant: 40.0),

            slider.topAnchor.constraint(equalTo: stepView.bottomAnchor, constant: 40.0),
            slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),

        ])
        
        let steps: [[CGFloat]] = [
            [0.1, 0.3],
            [0.4, 0.5],
            [0.7, 0.8],
            [0.9, 0.95],
        ]
        stepView.steps = steps

        slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
        
    }
    
    @objc func sliderChanged(_ sender: UISlider) {
        
        // disable CALayer "built-in" animations
        CATransaction.setDisableActions(true)
        stepView.progress = CGFloat(sender.value)
        CATransaction.commit()
        
    }
}

编辑

我仍然不清楚你的 0.4 - 0.8 要求,但也许这会帮助你上路:

请注意:这只是 示例代码!!!

struct RecordingStep {
    var color: UIColor = .black
    var start: Float = 0
    var end: Float = 0
    var layer: CALayer!
}

class StepView2: UIView {
    
    public var progress: Float = 0 {
        didSet {
            // move the progress layer
            progressLayer.position.x = bounds.width * CGFloat(progress)
            // if we're recording
            if isRecording {
                let i = theSteps.count - 1
                guard i > -1 else { return }
                // update current "step" end
                theSteps[i].end = progress
                setNeedsLayout()
            }
        }
    }
    
    private var isRecording: Bool = false
    
    private var theSteps: [RecordingStep] = []

    private let progressLayer = CAShapeLayer()
    
    public func startRecording(_ color: UIColor) {
        // create a new "Recording Step"
        var st = RecordingStep()
        st.color = color
        st.start = progress
        st.end = progress
        let l = CALayer()
        l.backgroundColor = st.color.cgColor
        layer.insertSublayer(l, below: progressLayer)
        st.layer = l
        theSteps.append(st)
        isRecording = true
    }
    public func stopRecording() {
        isRecording = false
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() {
        backgroundColor = .black
        progressLayer.lineWidth = 3
        progressLayer.strokeColor = UIColor.green.cgColor
        progressLayer.fillColor = UIColor.clear.cgColor
        layer.addSublayer(progressLayer)
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // only set the progessLayer frame if the bounds height has changed
        if progressLayer.frame.height != bounds.height + 7.0 {
            let r: CGRect = CGRect(origin: .zero, size: CGSize(width: 7.0, height: bounds.height + 7.0))
            let pth = UIBezierPath(roundedRect: r, cornerRadius: 3.5)
            progressLayer.frame = r
            progressLayer.position = CGPoint(x: 0, y: bounds.midY)
            progressLayer.path = pth.cgPath
        }
        
        theSteps.forEach { st in
            let x = bounds.width * CGFloat(st.start)
            let w = bounds.width * CGFloat(st.end - st.start)
            let r = CGRect(x: x, y: 0.0, width: w, height: bounds.height)
            st.layer.frame = r
        }
        
    }
}

class Step2VC: UIViewController {
    
    let stepView = StepView2()
    
    let actionButton: UIButton = {
        let b = UIButton()
        b.backgroundColor = .lightGray
        b.setImage(UIImage(systemName: "play.fill"), for: [])
        b.tintColor = .systemGreen
        return b
    }()
    
    var timer: Timer!
    
    let colors: [UIColor] = [
        .red, .systemBlue, .yellow, .cyan, .magenta, .orange,
    ]
    var colorIdx: Int = -1
    var action: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        stepView.translatesAutoresizingMaskIntoConstraints = false
        actionButton.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(stepView)
        view.addSubview(actionButton)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            stepView.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
            stepView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            stepView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            stepView.heightAnchor.constraint(equalToConstant: 40.0),
            
            actionButton.topAnchor.constraint(equalTo: stepView.bottomAnchor, constant: 40.0),
            actionButton.widthAnchor.constraint(equalToConstant: 80.0),
            actionButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
        ])

        actionButton.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
        
    }
    
    @objc func timerFunc(_ timer: Timer) {

        // don't set progress > 1.0
        stepView.progress = min(stepView.progress + 0.005, 1.0)

        if stepView.progress >= 1.0 {
            timer.invalidate()
            actionButton.isHidden = true
        }
        
    }
    
    @objc func btnTap(_ sender: UIButton) {
        switch action {
        case 0:
            // this will run for 15 seconds
            timer = Timer.scheduledTimer(timeInterval: 0.075, target: self, selector: #selector(timerFunc(_:)), userInfo: nil, repeats: true)
            stepView.stopRecording()
            actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
            actionButton.tintColor = .red
            action = 1
        case 1:
            colorIdx += 1
            stepView.startRecording(colors[colorIdx % colors.count])
            actionButton.setImage(UIImage(systemName: "stop.circle"), for: [])
            actionButton.tintColor = .black
            action = 2
        case 2:
            stepView.stopRecording()
            actionButton.setImage(UIImage(systemName: "record.circle"), for: [])
            actionButton.tintColor = .red
            action = 1
        default:
            ()
        }
    }
    
}

为了将来参考,在此处发帖时,最好充分解释您要做什么。显示您正在处理的代码很重要,但如果它真的只是 与您的实际目标相关,那么这个过程就会变得非常困难。