CAShapeLayer с разными цветами

У меня есть CAShapeLayer, основанный на этом отвечать, который анимируется вместе с UISlider.

CAShapeLayer с разными цветами

Он отлично работает, но поскольку shapeLayer следует за своим только 1 красным цветом CAGradientLayer. Я хочу, чтобы shapeLayer менял цвета в зависимости от определенных точек ползунка. Пример на 0.4 - 0.5 красный, 0.7-0.8 красный, 0.9-0.95 красный. Это не фактические значения, фактические значения могут отличаться. Я полагаю, что каждый раз, когда он не соответствует условию стать красным, он, вероятно, должен быть просто четким цветом, который просто покажет черную дорожку под ним. Результат будет выглядеть примерно так (не говоря уже о форме)

CAShapeLayer с разными цветами

Красные цвета основаны на том, что пользователь прокручивает ползунок и отпускает его. Различные положения ползунка, определяющие красный цвет, зависят от любых условий. Как я могу это сделать.

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

Вы пробовали просто добавить эти цвета в свой градиентный слой и добавить больше мест?

Lalo 22.03.2022 04:53

Это часть того, что я потерял. Как сопоставить местоположения в GrandentProgressView с соответствующими значениями ползунка.

Lance Samaria 22.03.2022 05:14

Наверняка они уже нанесены на карту? Оба находятся в диапазоне от 0 до 1.

matt 22.03.2022 05:50

@matt Я только что читал этот ikyle.me/blog/2020/cagradientlayer-объяснение. Он объясняет координаты градиента по отношению к представлению, в котором находится градиент. Насколько я понимаю, эти координаты 0-1 охватывают весь вид. Я не понимаю, как я могу изменить эти координаты на места, где начинается и останавливается очистка.

Lance Samaria 22.03.2022 05:53

@matt Я слежу за этим stackoverflow.com/a/33800041/4833705. Я разберусь с этим. Спасибо за помощь!

Lance Samaria 22.03.2022 06:14

@LanceSamaria - вы упоминаете «градиент», но неясно, к чему вы действительно стремитесь ... это что-то вроде этого? i.stack.imgur.com/4t8xM.gif

DonMag 22.03.2022 17:32

@DonMag Спасибо за ответ. Да, это ИМЕННО то, что я пытаюсь сделать. На самом деле я буквально просто работал над этим, и, поскольку я не мог понять, как сделать то, что вы только что сделали, я решил сделать то, что просто создает представление и добавляет в массив после нажатия ползунка. Я установил представления x, y в центр thumbRect, его высоту на что угодно, а его ширину на ноль. Когда ползунок расширяется, я просто вычитаю разницу между X thumbRect и X представления и использую это для увеличения ширины представлений. Я понятия не имею, как сделать то, что вы только что сделали с градиентом. Пожалуйста, опубликуйте его, если у вас есть время.

Lance Samaria 22.03.2022 17:38
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
71
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Чтобы получить это:

Мы можем использовать 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:
            ()
        }
    }
    
}

Для справки в будущем, когда вы публикуете здесь, вероятно, хорошей идеей будет полностью объяснить то, что вы пытаетесь сделать. Демонстрация кода, над которым вы работаете, важна, но если это действительно только типа, связанный с вашей реальной целью, это делает этот процесс довольно сложным.

БЛАГОДАРЮ ВАС!!! Я работал над этим часами вчера и все это утро и не мог понять. Очень признателен. Я дал вам оба :)

Lance Samaria 22.03.2022 17:48

Я только что понял кое-что. Вы жестко закодировали значения let steps: [[CGFloat]] = [...]. Чего я пытаюсь добиться, так это того, что когда пользователь скользит, любое условие превращает градиент в красный. Например. скажем, условия начинаются со значений ползунка 0,1, 0,4, 0,7 и 0,9, но продолжаются до тех пор, пока пользователь не уберет палец, что будет равно 0,3, 0,5, 0,8 и 0,95. В приведенном вами примере пользователь постучал/сдвинул/поднял 4 раза. Но на практике пользователь может нажать/сдвинуть/влево 9 раз в любой момент. Для простой демонстрации условием могут быть начальные точки касания, но точки подъема могут варьироваться.

Lance Samaria 22.03.2022 18:11

@LanceSamaria - хорошо, это сбивает с толку ... Вы хотите начать с пустой «полосы» ... пользователь перемещает ползунок, скажем, на 0,1 и отпускает. Затем пользователь перемещает большой палец на 0,4 и отпускает. Полоса теперь получает красную рамку от 0,1 до 0,4? И так далее? Могут ли коробки пересекаться? Должны ли они идти слева направо (от 0 до 1)? Или пользователь может создать ящик 0.8-0.9, а потом создать ящик 0.4-0.55?

DonMag 22.03.2022 19:17

Я предположил, что это сбивает с толку. Проблема в том, что условие плохо объяснено. Определенно начинается с пустого бара. Для простоты предположим, что только 2 условия становятся красными: 0,4 и 0,8. Когда пользователь начинает очистку с 0, если он поднимает палец на 0,3999, условие ложно. А вот если поскребут и попадут на 0,4, то условие выполнено, краснеет. Они продолжают тереть и поднимают палец на 0,7, затем на 0,4-07 он становится красным. Они скрабят еще немного, от от 0,71 до 0,79 он не красный, но как только он попадает в 0,8, он становится красным, пока они не отпустят или не дойдут до конца.

Lance Samaria 22.03.2022 19:24

Перекрытие - хороший вопрос. В реальном проекте у меня есть проверки, чтобы убедиться, что поля не перекрываются. Для этого примера, если проще не делать так, чтобы они не перекрывались, то точно нет. Но если это сложнее, то не беспокойтесь об этом.

Lance Samaria 22.03.2022 19:28

Они могут создать коробку в любое время, когда условие будет выполнено. Позвольте мне объяснить проект. Это запись. Когда ползунок скользит, они могут записывать, как только они начинают запись, поле начинает становиться красным на частях ползунка, чтобы пользователь знал, что он записал в эти конкретные временные рамки, например, 0,4-07. Как только этот ящик появится, они не смогут снова записывать эти временные рамки. Они могут пролистывать вперед и назад, и ящик всегда будет там. Они могут записывать в любой другой момент временной шкалы скруббера, если он не перекрывает поле. Это условие. Не перекрывайте существующие поля

Lance Samaria 22.03.2022 19:34

Пользователь на самом деле не выполняет очистку, это играет игрок, и пока игрок играет, PeriodTimeObserver обновляет ползунок. Для простоты я только что создал этот пример, чтобы сказать, что пользователь чистит, но на самом деле чистит игрок. Они нажимают кнопку записи и могут записывать звук, пока проигрыватель играет/прокручивает. Пока они записывают, поле становится красным. Как только они нажимают кнопку, чтобы остановить запись, или ползунок достигает конца, это конечная точка красного прямоугольника.

Lance Samaria 22.03.2022 19:39

@LanceSamaria - давайте посмотрим, понимаю ли я, что вы пытаетесь сделать ... Вы начнете с «пустого» бара ... происходит что-то, из-за чего прогресс начинает двигаться от 0 (левый конец) до 1,0 (правый конец) ... пока он движется, пользователь может приземлиться, и в точке прогресса начинается красный прямоугольник? Затем он расширяется, пока касание не нажато ... пользователь поднимает касание, и «прогресс» продолжается, но красный ящик останавливается? Пользователь снова приземляется, и начинается новое красное поле, расширяющееся до касания? Или...

DonMag 22.03.2022 19:55

касание пользователя, скажем, 0,1... красное поле расширяется до тех пор, пока прогресс ИЛИ касания не достигнет 0,3? Прогресс продолжается, касание все еще нажато, но нет нового красного поля, пока пользователь не коснется, а затем снова не нажмет?

DonMag 22.03.2022 19:56

Да, точно. Вы попали на чтение.

Lance Samaria 22.03.2022 19:57

В реальном проекте условием для запуска красного поля является нажатие кнопки записи, но это было излишним для этого, поэтому я использовал 0,4 и т. д. В качестве условий запуска. В реальном проекте красный прямоугольник останавливается, когда пользователь снова нажимает кнопку записи, но для этого я просто решил использовать 0,7 и т. д. или до тех пор, пока палец не будет снят. Единственная превентивная вещь в обоих случаях — это отсутствие записи поверх существующего красного ящика, что означает, что другой красный ящик не будет проходить поверх существующего красного ящика. Но это не большая проблема, потому что в реальном проекте у меня есть массив и время начала/остановки для его проверки.

Lance Samaria 22.03.2022 20:03

@LanceSamaria - см. Редактировать к моему ответу.

DonMag 22.03.2022 22:00

Спасибо за это. Я просматриваю его на своем телефоне, мне нужно добраться до ноутбука, чтобы полностью понять и попробовать. Насколько я могу судить, ваша кнопка работает именно так, как я описал. Причина, по которой я не опубликовал проект более подробно, заключается в том, что я иногда замечаю, что чем меньше, тем лучше. Я столкнулся с несколькими вопросами без ответа, и я объяснил это тем, что их было слишком много. В этой ситуации я мог бы использовать тот же код, но объяснить лучше. Спасибо. Я собираюсь хорошенько взглянуть на это позже сегодня вечером, протестировать и сообщить вам результаты. Удачного дня. ПОГОВОРИМ ПОЗЖЕ

Lance Samaria 22.03.2022 22:08

Давайте продолжить обсуждение в чате.

Lance Samaria 23.03.2022 17:16

@LanceSamaria - посмотрите здесь 3 разных подхода: gist.github.com/DonMag/45092f7c305968650da5896bbe045873

DonMag 23.03.2022 20:42

Другие вопросы по теме