Я пытаюсь реализовать циклическое представление прогресса, которое выглядит так:
struct SpeedometerView: View {
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
speedProgressView(width: 300)
}
}
private func speedProgressView(width: CGFloat) -> some View {
ZStack {
Circle()
.trim(from: 0, to: 0.2)
.stroke(.red, style: StrokeStyle(lineWidth: 4, lineCap: .round))
.shadow(color: .green, radius: 10, x: 0, y: 0)
.shadow(color: .green, radius: 2, x: 0, y: 0)
}
.frame(width: width)
.rotationEffect(.degrees(100))
}
}
#Preview {
SpeedometerView()
}
Оба конца на данный момент закруглены из-за стиля обводки. Как обрезать/обрезать только тот конец круга, который движется по часовой стрелке, если прогресс не равен 100%? Любая помощь приветствуется.
Да все верно. Оба конца должны быть .round
на 100% прогрессе, иначе только один конец. Скриншот был просто для иллюстрации, я не мог понять, как создать закругленные концы в Preview :)
Если вы хотите, чтобы только один конец линии был .round, а другой - .butt. Итак, для этого вам понадобится Двойной круг. Я надеюсь, что это помогает
struct SpeedometerView: View {
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
speedProgressView(width: 100)
}
}
private func speedProgressView(width: CGFloat) -> some View {
ZStack {
Circle()
.trim(from: 0, to: 0.1)
.stroke(Color.red, style: StrokeStyle(lineWidth: 4, lineCap: .round))
.shadow(color: .green, radius: 10, x: 0, y: 0)
.shadow(color: .green, radius: 2, x: 0, y: 0)
Circle()
.trim(from: 0.1, to: 0.2)
.stroke(Color.red, style: StrokeStyle(lineWidth: 4, lineCap: .butt))
.shadow(color: .green, radius: 10, x: 0, y: 0)
.shadow(color: .green, radius: 2, x: 0, y: 0)
}
.frame(width: width)
.rotationEffect(.degrees(100))
}
}
Я бы написал это как закрашенный Shape
, а не обведенный Path
.
Путь Shape
— это союз:
.butt
.struct OneEndRoundedCircle: Shape {
var trim: CGFloat
let lineWidth: CGFloat
func path(in rect: CGRect) -> Path {
let radius = min(rect.width, rect.height) / 2
let center = CGPoint(x: rect.midX, y: rect.midY)
let circlePath = Path(
ellipseIn: CGRect(origin: center, size: .zero)
.insetBy(dx: -radius, dy: -radius)
).trimmedPath(from: 0, to: trim)
let stroked = circlePath.strokedPath(StrokeStyle(lineWidth: lineWidth, lineCap: .butt))
// this is where we want the rounded end to be.
// If your path starts somewhere else, adjust the angle accordingly
let roundedPoint = center.applying(.init(
translationX: radius * sin(.pi / 2),
y: radius * cos(.pi / 2)
))
let littleCircle = Path(
ellipseIn: .init(origin: roundedPoint, size: .zero)
.insetBy(dx: -lineWidth / 2, dy: -lineWidth / 2)
)
return stroked.union(littleCircle)
}
}
// Animatable conformance in case you want to animate the amount trimmed
extension OneEndRoundedCircle: Animatable {
var animatableData: CGFloat {
get { trim }
set { trim = newValue }
}
}
private func speedProgressView(width: CGFloat) -> some View {
ZStack {
// intentionally increased lineWidth to make the rounded end more visible
OneEndRoundedCircle(trim: 0.2, lineWidth: 10)
.fill(.red)
.shadow(color: .green, radius: 10, x: 0, y: 0)
.shadow(color: .green, radius: 2, x: 0, y: 0)
}
.frame(width: width)
.rotationEffect(.degrees(100))
}
Пример вывода:
Для iOS 17 или более ранней версии union
недоступен, но вместо этого вы можете просто работать с CGPath
и использовать вместо этого его метод Union.
Чтобы добиться результата, показанного на скриншоте, где оба конца закруглены, когда спидометр заполнен, вы можете просто заранее проверить значение trim
и return
.
// assuming you want to leave the bottom 20 degrees of the circle always empty,
// the speedometer is full when the trim ends at 17/18
if abs(trim - 17 / 18) < 0.00001 {
return circlePath.strokedPath(StrokeStyle(lineWidth: lineWidth, lineCap: .round))
}
let stroked = circlePath.strokedPath(StrokeStyle(lineWidth: lineWidth, lineCap: .butt))
// add the little circle like before...
Этот эффект можно создать, просто наложив круглую точку на начало линии:
.compositingGroup()
перед применением эффекта тени, чтобы эффект тени распространялся на объединенную форму.struct SpeedometerView: View {
let lineWidth: CGFloat = 10
@Binding var fraction: Double
private var speedProgressView: some View {
ZStack {
Circle()
.trim(from: 0, to: fraction * (340 / 360))
.stroke(.red, style: StrokeStyle(lineWidth: lineWidth, lineCap: fraction == 1 ? .round : .butt))
Circle()
.trim(from: 0, to: 0.001)
.stroke(.red, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
}
.compositingGroup()
.shadow(color: .green, radius: 10, x: 0, y: 0)
.shadow(color: .green, radius: 2, x: 0, y: 0)
.rotationEffect(.degrees(100))
}
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
speedProgressView
.frame(width: 300)
}
}
}
struct ContentView: View {
@State private var fraction = 0.0
var body: some View {
VStack {
SpeedometerView(fraction: $fraction)
Slider(value: $fraction, in: 0...1)
.padding()
}
}
}
Вы хотите, чтобы только один конец линии был
.round
, а другой.butt
, верно? Однако на вашем скриншоте видно, что оба конца имеют.butt
.