У меня есть 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
}
}
Это часть того, что я потерял. Как сопоставить местоположения в GrandentProgressView с соответствующими значениями ползунка.
Наверняка они уже нанесены на карту? Оба находятся в диапазоне от 0 до 1.
@matt Я только что читал этот ikyle.me/blog/2020/cagradientlayer-объяснение. Он объясняет координаты градиента по отношению к представлению, в котором находится градиент. Насколько я понимаю, эти координаты 0-1 охватывают весь вид. Я не понимаю, как я могу изменить эти координаты на места, где начинается и останавливается очистка.
@matt Я слежу за этим stackoverflow.com/a/33800041/4833705. Я разберусь с этим. Спасибо за помощь!
@LanceSamaria - вы упоминаете «градиент», но неясно, к чему вы действительно стремитесь ... это что-то вроде этого? i.stack.imgur.com/4t8xM.gif
@DonMag Спасибо за ответ. Да, это ИМЕННО то, что я пытаюсь сделать. На самом деле я буквально просто работал над этим, и, поскольку я не мог понять, как сделать то, что вы только что сделали, я решил сделать то, что просто создает представление и добавляет в массив после нажатия ползунка. Я установил представления x, y в центр thumbRect, его высоту на что угодно, а его ширину на ноль. Когда ползунок расширяется, я просто вычитаю разницу между X thumbRect и X представления и использую это для увеличения ширины представлений. Я понятия не имею, как сделать то, что вы только что сделали с градиентом. Пожалуйста, опубликуйте его, если у вас есть время.
Чтобы получить это:
Мы можем использовать 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:
()
}
}
}
Для справки в будущем, когда вы публикуете здесь, вероятно, хорошей идеей будет полностью объяснить то, что вы пытаетесь сделать. Демонстрация кода, над которым вы работаете, важна, но если это действительно только типа, связанный с вашей реальной целью, это делает этот процесс довольно сложным.
БЛАГОДАРЮ ВАС!!! Я работал над этим часами вчера и все это утро и не мог понять. Очень признателен. Я дал вам оба :)
Я только что понял кое-что. Вы жестко закодировали значения let steps: [[CGFloat]] = [...]
. Чего я пытаюсь добиться, так это того, что когда пользователь скользит, любое условие превращает градиент в красный. Например. скажем, условия начинаются со значений ползунка 0,1, 0,4, 0,7 и 0,9, но продолжаются до тех пор, пока пользователь не уберет палец, что будет равно 0,3, 0,5, 0,8 и 0,95. В приведенном вами примере пользователь постучал/сдвинул/поднял 4 раза. Но на практике пользователь может нажать/сдвинуть/влево 9 раз в любой момент. Для простой демонстрации условием могут быть начальные точки касания, но точки подъема могут варьироваться.
@LanceSamaria - хорошо, это сбивает с толку ... Вы хотите начать с пустой «полосы» ... пользователь перемещает ползунок, скажем, на 0,1 и отпускает. Затем пользователь перемещает большой палец на 0,4 и отпускает. Полоса теперь получает красную рамку от 0,1 до 0,4? И так далее? Могут ли коробки пересекаться? Должны ли они идти слева направо (от 0 до 1)? Или пользователь может создать ящик 0.8-0.9
, а потом создать ящик 0.4-0.55
?
Я предположил, что это сбивает с толку. Проблема в том, что условие плохо объяснено. Определенно начинается с пустого бара. Для простоты предположим, что только 2 условия становятся красными: 0,4 и 0,8. Когда пользователь начинает очистку с 0, если он поднимает палец на 0,3999, условие ложно. А вот если поскребут и попадут на 0,4, то условие выполнено, краснеет. Они продолжают тереть и поднимают палец на 0,7, затем на 0,4-07 он становится красным. Они скрабят еще немного, от от 0,71 до 0,79 он не красный, но как только он попадает в 0,8, он становится красным, пока они не отпустят или не дойдут до конца.
Перекрытие - хороший вопрос. В реальном проекте у меня есть проверки, чтобы убедиться, что поля не перекрываются. Для этого примера, если проще не делать так, чтобы они не перекрывались, то точно нет. Но если это сложнее, то не беспокойтесь об этом.
Они могут создать коробку в любое время, когда условие будет выполнено. Позвольте мне объяснить проект. Это запись. Когда ползунок скользит, они могут записывать, как только они начинают запись, поле начинает становиться красным на частях ползунка, чтобы пользователь знал, что он записал в эти конкретные временные рамки, например, 0,4-07. Как только этот ящик появится, они не смогут снова записывать эти временные рамки. Они могут пролистывать вперед и назад, и ящик всегда будет там. Они могут записывать в любой другой момент временной шкалы скруббера, если он не перекрывает поле. Это условие. Не перекрывайте существующие поля
Пользователь на самом деле не выполняет очистку, это играет игрок, и пока игрок играет, PeriodTimeObserver обновляет ползунок. Для простоты я только что создал этот пример, чтобы сказать, что пользователь чистит, но на самом деле чистит игрок. Они нажимают кнопку записи и могут записывать звук, пока проигрыватель играет/прокручивает. Пока они записывают, поле становится красным. Как только они нажимают кнопку, чтобы остановить запись, или ползунок достигает конца, это конечная точка красного прямоугольника.
@LanceSamaria - давайте посмотрим, понимаю ли я, что вы пытаетесь сделать ... Вы начнете с «пустого» бара ... происходит что-то, из-за чего прогресс начинает двигаться от 0 (левый конец) до 1,0 (правый конец) ... пока он движется, пользователь может приземлиться, и в точке прогресса начинается красный прямоугольник? Затем он расширяется, пока касание не нажато ... пользователь поднимает касание, и «прогресс» продолжается, но красный ящик останавливается? Пользователь снова приземляется, и начинается новое красное поле, расширяющееся до касания? Или...
касание пользователя, скажем, 0,1... красное поле расширяется до тех пор, пока прогресс ИЛИ касания не достигнет 0,3? Прогресс продолжается, касание все еще нажато, но нет нового красного поля, пока пользователь не коснется, а затем снова не нажмет?
Да, точно. Вы попали на чтение.
В реальном проекте условием для запуска красного поля является нажатие кнопки записи, но это было излишним для этого, поэтому я использовал 0,4 и т. д. В качестве условий запуска. В реальном проекте красный прямоугольник останавливается, когда пользователь снова нажимает кнопку записи, но для этого я просто решил использовать 0,7 и т. д. или до тех пор, пока палец не будет снят. Единственная превентивная вещь в обоих случаях — это отсутствие записи поверх существующего красного ящика, что означает, что другой красный ящик не будет проходить поверх существующего красного ящика. Но это не большая проблема, потому что в реальном проекте у меня есть массив и время начала/остановки для его проверки.
@LanceSamaria - см. Редактировать к моему ответу.
Спасибо за это. Я просматриваю его на своем телефоне, мне нужно добраться до ноутбука, чтобы полностью понять и попробовать. Насколько я могу судить, ваша кнопка работает именно так, как я описал. Причина, по которой я не опубликовал проект более подробно, заключается в том, что я иногда замечаю, что чем меньше, тем лучше. Я столкнулся с несколькими вопросами без ответа, и я объяснил это тем, что их было слишком много. В этой ситуации я мог бы использовать тот же код, но объяснить лучше. Спасибо. Я собираюсь хорошенько взглянуть на это позже сегодня вечером, протестировать и сообщить вам результаты. Удачного дня. ПОГОВОРИМ ПОЗЖЕ
Давайте продолжить обсуждение в чате.
@LanceSamaria - посмотрите здесь 3 разных подхода: gist.github.com/DonMag/45092f7c305968650da5896bbe045873
Вы пробовали просто добавить эти цвета в свой градиентный слой и добавить больше мест?