Как самому реализовать такой же круговой виджет экрана блокировки iOS 16?

Я пытаюсь реализовать виджет экрана блокировки самостоятельно

Виджет, который я сейчас реализую

Я хочу реализовать этот виджет блокировки экрана ios16

Я сделал почти все, но не смог реализовать прозрачную рамку маленького круга. Я не мог найти способ сделать прозрачным даже фон кольца за ним.

Мой код

struct RingTipShape: Shape { // small circle
    var currentPercentage: Double
    var thickness: CGFloat

    func path(in rect: CGRect) -> Path {
        var path = Path()

        let angle = CGFloat((240 * currentPercentage) * .pi / 180)
        let controlRadius: CGFloat = rect.width / 2 - thickness / 2
        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
        let x = center.x + controlRadius * cos(angle)
        let y = center.y + controlRadius * sin(angle)
        let pointCenter = CGPoint(x: x, y: y)
        path.addEllipse(in:
            CGRect(
                x: pointCenter.x - thickness / 2,
                y: pointCenter.y - thickness / 2,
                width: thickness,
                height: thickness
            )
        )
        return path
    }
    
    var animatableData: Double {
        get { return currentPercentage }
        set { currentPercentage = newValue }
    }
}
struct RingShape: Shape {
    var currentPercentage: Double
    var thickness: CGFloat
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        path.addArc(center: CGPoint(x: rect.width / 2, y: rect.height / 2), radius: rect.width / 2 - (thickness / 2), startAngle: Angle(degrees: 0), endAngle: Angle(degrees: currentPercentage * 240), clockwise: false)
        
        return path.strokedPath(.init(lineWidth: thickness, lineCap: .round, lineJoin: .round))
    }
    var animatableData: Double {
        get { return currentPercentage}
        set { currentPercentage = newValue}
    }
}
struct CircularWidgetView: View { // My customizing widget view
    @State var percentage: Double = 1.0
    
    var body: some View {
    
        GeometryReader { geo in
            ZStack {
                RingBackgroundShape(thickness: 5.5)
                    .rotationEffect(Angle(degrees: 150))
                    .frame(width: geo.size.width, height: geo.size.height)
                    .foregroundColor(.white.opacity(0.21))
                RingShape(currentPercentage: 0.5, thickness: 5.5)
                    .rotationEffect(Angle(degrees: 150))
                    .frame(width: geo.size.width, height: geo.size.height)
                    .foregroundColor(.white.opacity(0.385))
                RingTipShape(currentPercentage: 0.5, thickness: 5.5)
                    .rotationEffect(Angle(degrees: 150))
                    .frame(width: geo.size.width, height: geo.size.height)
                    .foregroundColor(.white)
                    /* 
                    I want to make RingTipShape completely
                    transparent. Ignoring even the RingShape behind it
                    */
   
                VStack(spacing: 4) {
                    Image(systemName: "scooter")
                        .resizable()
                        .frame(width: 24, height: 24)
                    Text("hello")
                        .font(.system(size: 10, weight: .semibold))
                        .lineLimit(1)
                        .minimumScaleFactor(0.1)
                }
            }
        }
    }
}

Как я могу сделать прозрачную рамку, которая также игнорирует фон представления за ней?

Просто чтобы убедиться... есть ли причина, по которой вы не хотите использовать Датчик?

Alladinian 26.11.2022 07:45

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

msgu 26.11.2022 10:35

Я полностью уважаю причину (и согласен с тем, что этот опыт бесценен), поэтому попытался опубликовать ответ, имея это в виду. Надеюсь, это помогло!

Alladinian 26.11.2022 15:39
Стоит ли изучать 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
3
93
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Это отличное упражнение. Недостающая часть - маска.

Примечание. Несмотря на то, что существует множество способов улучшить существующий код, я постараюсь придерживаться исходного решения, так как смысл в том, чтобы набираться опыта на практике (на основе комментариев). Однако в конце я поделюсь некоторыми советами.

Таким образом, мы можем думать об этом в два этапа:

  1. Нам нужен какой-то способ сделать еще один RingTipShape в том же (по центру) положении, что и наш существующий, но немного больше.
  2. Нам нужно найти способ создать маску, которая удаляет только эту фигуру из другого контента (в нашем случае трек звенит).

Первый пункт прост, нам просто нужно определить внешнюю толщину, чтобы разместить эллипс поверх дорожки в правильном месте:

struct RingTipShape: Shape { // small circle
    //...
    let outerThickness: CGFloat
    //...
    let controlRadius: CGFloat = rect.width / 2 - outerThickness / 2
    //...
}

тогда наш существующий код изменится на:

RingTipShape(currentPercentage: percentage, thickness: 5.5, outerThickness: 5.5)

теперь для второй части нам нужно что-то, чтобы создать больший круг, что легко:

RingTipShape(currentPercentage: percentage, thickness: 10.0, outerThickness: 5.5)

Итак, теперь в заключительной части мы собираемся использовать эту (более крупную) форму, чтобы создать своего рода перевернутую маску:

private var thumbMask: some View {
    ZStack {
        Color.white // This part will be transparent
        RingTipShape(currentPercentage: percentage, thickness: 10.0, outerThickness: 5.5)
            .fill(Color.black) // This will be masked out
            .rotationEffect(Angle(degrees: 150))
    }
    .compositingGroup() // Rasterize the shape
    .luminanceToAlpha() // Map luminance to alpha values
}

и мы применяем маску следующим образом:

RingShape(currentPercentage: percentage, thickness: 5.5)
    .rotationEffect(Angle(degrees: 150))
    .foregroundColor(.white.opacity(0.385))
    .mask(thumbMask)

что приводит к этому:


Некоторые наблюдения/советы:

  1. Вам не нужны GeometryReader (и все модификаторы кадров) в вашем CircularWidgetView, ZStack предложит все доступное пространство для просмотра.
  2. Вы можете добавить .aspectRatio(contentMode: .fit) к своему изображению, чтобы избежать растяжения.
  3. Вы можете воспользоваться преимуществами существующих API для создания форм треков.

Например:

struct MyGauge: View {

    let value: Double = 0.5
    let range = 0.1...0.9

    var body: some View {
        ZStack {
            // Backing track
            track().opacity(0.2)

            // Value track
            track(showsProgress: true)
        }
    }

    private var mappedValue: Double {
        (range.upperBound + range.lowerBound) * value
    }

    private func track(showsProgress: Bool = false) -> some View {
        Circle()
            .trim(from: range.lowerBound, to: showsProgress ? mappedValue : range.upperBound)
            .stroke(.white, style: .init(lineWidth: 5.5, lineCap: .round))
            .rotationEffect(.radians(Double.pi / 2))
    }
}

приведет к:

что немного упрощает работу, используя модификатор trim.

Я надеюсь, что это имеет смысл.

Я решил это отлично. Я многому научился благодаря вам. Есть части, которые я не понимаю. Я не уверен, зачем вам нужно .luminanceToAlpha() в thumMask(). И очень сложно понять поведение, когда .luminanceToAlpha() и .mask() используются вместе.

msgu 27.11.2022 13:18

Я не знал, что маска также применяет прозрачность. Спасибо, все мои сомнения развеялись. Большое спасибо!

msgu 28.11.2022 08:44

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