Обрезка и закругление lineCap для круга в SwiftUI

Я пытаюсь реализовать циклическое представление прогресса, которое выглядит так:

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, а другой .butt, верно? Однако на вашем скриншоте видно, что оба конца имеют .butt.

Sweeper 07.07.2024 11:42

Да все верно. Оба конца должны быть .round на 100% прогрессе, иначе только один конец. Скриншот был просто для иллюстрации, я не мог понять, как создать закругленные концы в Preview :)

batman 07.07.2024 11:46
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
2
97
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Если вы хотите, чтобы только один конец линии был .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 — это союз:

  • путь, созданный strodPath (обрезанного) кругового пути, с концом линии .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...
Ответ принят как подходящий

Этот эффект можно создать, просто наложив круглую точку на начало линии:

  • Если процент меньше 100%, используйте стыковую линию, в противном случае используйте круглую линию, чтобы конец тоже был закругленным.
  • Круглая точка в начале может быть получена путем наложения другой линии с круглым концом, но минимального размера.
  • Наличие точки в начале, вероятно, хорошо, когда процент равен 0, но вы можете сделать это при условии, что процент будет положительным, если это желательно.
  • Нанесите .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()
        }
    }
}

Animation

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