Как применить тень к каждому фрагменту, сохранив текущую анимацию? Я попробовал поместить его на сам слой, но тень не видна. Я также попробовал создать еще один слой с прозрачным фоном, но с чистым фоном тень не работает.
override func draw(_ rect: CGRect) {
super.draw(rect)
layer.sublayers?.filter { $0 is CAShapeLayer }.forEach { $0.removeFromSuperlayer() }
let totalValue = data.reduce(0.0) { $0 + $1.value }
let center = CGPoint(x: rect.midX, y: rect.midY)
var startAngle: CGFloat = -CGFloat.pi / 2
var cumulativeDelay: CFTimeInterval = 0
gapSizeDegrees = data.count == 1 ? 0 : gapSizeDegrees
data.forEach { value in
let segmentValue = CGFloat(value.value)
let color = value.color
let endAngle = startAngle + (segmentValue / CGFloat(totalValue)) * 2 * .pi
let outerRadius = rect.width / 2
let outerStartAngle = startAngle + gapSizeDegrees.toRadians()
let outerEndAngle = endAngle - gapSizeDegrees.toRadians()
let path = UIBezierPath(arcCenter: center, radius: outerRadius, startAngle: outerStartAngle, endAngle: outerEndAngle, clockwise: true)
let innerRadius = outerRadius - strokeWidth
path.addArc(withCenter: center, radius: innerRadius, startAngle: outerEndAngle - gapSizeDegrees / rect.width, endAngle: outerStartAngle + gapSizeDegrees / rect.width , clockwise: false)
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = color.cgColor
shapeLayer.strokeColor = color.cgColor
layer.addSublayer(shapeLayer)
animateSliceFilling(sliceLayer: shapeLayer, center: center, radius: outerRadius, innerRadius: innerRadius, startAngle: startAngle, endAngle: endAngle, delay: cumulativeDelay)
cumulativeDelay += Consts.Appearence.animationDuration / Double(data.count)
startAngle = endAngle
}
}
private func animateSliceFilling(sliceLayer: CAShapeLayer, center: CGPoint, radius: CGFloat, innerRadius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, delay: CFTimeInterval) {
let maskLayer = CAShapeLayer()
let maskPath = UIBezierPath(arcCenter: center, radius: (radius + innerRadius) / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
maskLayer.path = maskPath.cgPath
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.strokeColor = UIColor.black.cgColor
maskLayer.lineWidth = strokeWidth
maskLayer.strokeEnd = 0.0
sliceLayer.mask = maskLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = Consts.Appearence.animationDuration / Double(data.count)
animation.beginTime = CACurrentMediaTime() + delay
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
maskLayer.add(animation, forKey: "strokeEndAnimation")
}
Да, именно это я и пытаюсь сделать, с той лишь разницей, что тень должна быть всегда черной.
@pistifeju - что значит «тень всегда должна быть черной»?
Возможно, качество присланной вами GIF-ки плохое, но на моем мониторе тень как бы желтоватая или немного меняется. Это не имеет значения; Мне просто нужна простая черная тень под моими фигурами.
Не стоит пытаться делать анимацию с помощью draw(_:). Вместо этого вам следует настроить слои как CALayers, а затем добавить к слоям один или несколько CAAnimation. Вероятно, вы могли бы настроить свои слои так, чтобы они имели тени, а затем анимировать обводку на ваших путях, и система отрисовывает тени за вас.
Пожалуйста, не портите свои посты. Размещая информацию в сети Stack Exchange, вы предоставляете SE безотзывное право на распространение этого контента (по лицензии CC BY-SA 4.0). Согласно политике SE, любой вандализм будет пресечен.





Несколько наблюдений:
Я бы поместил тень на отдельный слой (в случае, если тень больше угла зазора; вы хотите убедиться, что тень одного слоя не перекрывает данные другого слоя).
Я бы создал слои-фигуры для слоя тени; и каждый из слоев данных.
Затем я бы анимировал маску на всем родительском слое.
Нам не следует добавлять слои или выполнять анимацию из draw(rect:). (Это используется при рендеринге вашей собственной анимации и для рендеринга одного кадра; но мы полагаемся, что CoreGraphics сделает анимацию за нас, поэтому draw(rect:) — это просто не подходящее место для создания этой анимации.
Использование layoutSubviews было бы более логичным.
Кроме того, всякий раз, когда вы реализуете draw(rect:) (чего мы здесь делать не будем), никогда не предполагайте, что rect представляет собой полное представление. Он показывает, какая часть представления рисуется. Вместо этого используйте bounds.
Лично я бы рассчитывал дельту для каждой дуги, а затем вставлял начальный и конечный углы для каждой на половину радиуса зазора.
Я был бы склонен визуализировать каждую дугу как одну дугу (не внутреннюю в одном направлении и внешнюю дугу в другом), если только вам это абсолютно не нужно (например, вы хотите сделать какое-то скругление углов самих дуг). Это не критично к данному вопросу, но немного упрощает код.
Я бы вычислил радиус по минимуму ширины и высоты.
Это дает:
class AnimatedDataView: UIView {
let strokeWidth: CGFloat = 30
var gapSizeDegrees: CGFloat = 5
let shadowRadius: CGFloat = 5
var data: [Value] = [
Value(value: 0.25, color: .red),
Value(value: 0.33, color: .green),
Value(value: 1 - 0.25 - 0.33, color: .blue)
]
override func layoutSubviews() {
super.layoutSubviews()
start()
}
func start() {
let rect = bounds
layer.mask = nil
layer.sublayers?.filter { $0 is CAShapeLayer }.forEach { $0.removeFromSuperlayer() }
let totalValue = data.reduce(0) { $0 + $1.value }
let center = CGPoint(x: rect.midX, y: rect.midY)
var angle: CGFloat = -.pi / 2
var cumulativeDelay: CFTimeInterval = 0
gapSizeDegrees = data.count == 1 ? 0 : gapSizeDegrees
let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2
let shadowPath = UIBezierPath()
let shadowLayer = CAShapeLayer()
shadowLayer.fillColor = UIColor.clear.cgColor
shadowLayer.strokeColor = UIColor.black.cgColor
shadowLayer.lineWidth = strokeWidth
shadowLayer.shadowOpacity = 1
shadowLayer.shadowRadius = shadowRadius
layer.addSublayer(shadowLayer)
data.forEach { value in
let segmentValue = CGFloat(value.value)
let color = value.color
let delta = (segmentValue / CGFloat(totalValue)) * 2 * .pi
let endAngle = angle + delta - gapSizeDegrees.toRadians() / 2
let startAngle = angle + gapSizeDegrees.toRadians() / 2
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
shadowPath.append(path)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = color.cgColor
shapeLayer.lineWidth = strokeWidth
layer.addSublayer(shapeLayer)
cumulativeDelay += Consts.Appearence.animationDuration / Double(data.count)
angle += delta
}
shadowLayer.path = shadowPath.cgPath
let maskLayer = CAShapeLayer()
let maskPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: -.pi / 2, endAngle: 3 * .pi / 2, clockwise: true)
maskLayer.path = maskPath.cgPath
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.strokeColor = UIColor.black.cgColor
maskLayer.lineWidth = strokeWidth + shadowRadius
layer.mask = maskLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = Consts.Appearence.animationDuration / Double(data.count)
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
maskLayer.add(animation, forKey: "strokeEndAnimation")
}
}
Если вы хотите, чтобы промежутки между дугами были параллельны, а углы закруглены, вы можете сделать что-то вроде:
class AnimatedDataView: UIView {
let arcWidth: CGFloat = 150
let cornerRadius: CGFloat = 20
var gapSize: CGFloat = 8
let shadowRadius: CGFloat = 4
var data: [Value] = [
Value(value: 0.25, color: .red),
Value(value: 0.33, color: .green),
Value(value: 1 - 0.25 - 0.33, color: .blue)
]
override func layoutSubviews() {
super.layoutSubviews()
start()
}
func start() {
layer.mask = nil
layer.sublayers?.filter { $0 is CAShapeLayer }.forEach { $0.removeFromSuperlayer() }
let totalValue = data.reduce(0) { $0 + $1.value }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
var angle: CGFloat = -.pi / 2
let gapSize = data.count == 1 ? 0 : self.gapSize
let radius = (min(bounds.width, bounds.height) - arcWidth) / 2
let shadowPath = UIBezierPath()
let shadowLayer = CAShapeLayer()
shadowLayer.fillColor = UIColor.black.cgColor
shadowLayer.strokeColor = UIColor.black.cgColor
shadowLayer.lineWidth = cornerRadius * 2
shadowLayer.lineJoin = .round
shadowLayer.shadowOpacity = 1
shadowLayer.shadowRadius = shadowRadius
shadowLayer.shadowOffset = .zero
layer.addSublayer(shadowLayer)
data.forEach { value in
let segmentValue = CGFloat(value.value)
let color = value.color
let delta = (segmentValue / CGFloat(totalValue)) * 2 * .pi
let endAngle = angle + delta
let path = UIBezierPath(center: center, startAngle: angle, endAngle: endAngle, radius: radius, arcWidth: arcWidth, spacing: gapSize, cornerRadius: cornerRadius)
shadowPath.append(path)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = color.cgColor
shapeLayer.strokeColor = color.cgColor
shapeLayer.lineWidth = cornerRadius * 2
shapeLayer.lineJoin = .round
layer.addSublayer(shapeLayer)
angle = endAngle
}
shadowLayer.path = shadowPath.cgPath
let maskStrokeWidth = sqrt(bounds.width * bounds.width + bounds.height * bounds.height)
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(arcCenter: center, radius: maskStrokeWidth / 2, startAngle: -.pi / 2, endAngle: 3 * .pi / 2, clockwise: true).cgPath
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.strokeColor = UIColor.black.cgColor
maskLayer.lineWidth = maskStrokeWidth
layer.mask = maskLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = Consts.Appearance.animationDuration
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
maskLayer.add(animation, forKey: "strokeEndAnimation")
}
}
См. первый пункт в разделе Установите конкретное значение для закругленных углов UIBezierPath, чтобы увидеть, как я визуализирую закругленные углы.
В любом случае, здесь используется расширение UIBezierPath:
extension UIBezierPath {
/// Create arc path with corner rounding and spacing between arcs.
///
/// Note, this path assumes that the “corner radius” will be achieved by setting the `lineWidth` to
/// two times the “corner radius”. We're setting that lineWidth here, but if using in a `CAShapeLayer`
/// you will need to explicitly set that `lineWidth` for that shape layer, too.
///
/// - Parameters:
/// - center: Center of the arc.
/// - startAngle: The start angle of the arc. Note, this will be inset by 1/2 of the `spacing` value.
/// - endAngle: The end angle of the arc. Note, this will be inset by 1/2 of the `spacing` value.
/// - clockwise: Should this be rendered clockwise or counter clockwise. Defaults to `true`.
/// - radius: The radius of the arc (the midpoint between the outer edge of the arc and the inner edge).
/// - arcWidth: The width of this arc.
/// - spacing: The spacing between this arc and the next. This arc is inset by half of this spacing at the start and end of the arc. Defaults to zero.
/// - cornerRadius: The corner radius of this thick arc. Defaults to zero.
convenience init(
center: CGPoint,
startAngle: CGFloat,
endAngle: CGFloat,
clockwise: Bool = true,
radius: CGFloat,
arcWidth: CGFloat,
spacing: CGFloat = 0,
cornerRadius: CGFloat = 0
) {
let innerRadius = radius - arcWidth / 2 + cornerRadius
let innerAngularDelta = asin((cornerRadius + spacing / 2) / innerRadius) * (clockwise ? 1 : -1)
let outerRadius = radius + arcWidth / 2 - cornerRadius
let outerAngularDelta = asin((cornerRadius + spacing / 2) / outerRadius) * (clockwise ? 1 : -1)
self.init(arcCenter: center, radius: innerRadius, startAngle: startAngle + innerAngularDelta, endAngle: endAngle - innerAngularDelta, clockwise: clockwise)
addArc(withCenter: center, radius: outerRadius, startAngle: endAngle - outerAngularDelta, endAngle: startAngle + outerAngularDelta, clockwise: !clockwise)
lineWidth = cornerRadius * 2
close()
}
}
Это дает:
(Обратите внимание: я преувеличил ширину этих дуг, чтобы было ясно видно расстояние между параллельными линиями.)
Спасибо, это работает, а также спасибо за наблюдения. Изначально я хотел сделать одну дугу, но мне также нужны закругленные углы, а с одной дугой я не смог найти способ сделать промежутки параллельными между срезами. (Ширина зазора была меньше, когда она была ближе к внутреннему радиусу, но шире, когда она была ближе к внешнему радиусу.)
Буду признателен, если вы сделаете версию, где зазоры параллельны, спасибо!
Возможно ли, что в последнем коде есть ошибка, которая делает закругленные углы и параллельные зазоры? Скопировал полный код и добавил его в качестве подпредставления в viewController, но диаграмму вообще не вижу. Если я установлю фоновый цвет для представления, я увижу прямоугольник. Сделал то же самое с другим кодом, который вы отправили ранее, и он работает нормально.
Интересный. Это то, что я делаю, и ничего не вижу на экране. Я что-то пропустил? Пытался вернуться до маскировки, но это не помогло. class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let dataView = AnimatedDataView(frame: CGRect(x: 50, y: 50, width: 250, height: 250)) view.addSubview(dataView) } }
Давайте продолжим обсуждение в чате.
Вы пытаетесь пойти на что-то подобное? i.sstatic.net/QcTEu.gif