简体   繁体   English

如何自己实现同一个 iOS 16 锁屏圆形小部件?

[英]How to implement the same iOS 16 lock screen circular widget myself?

I'm trying to implement lock screen widget myself我正在尝试自己实现锁屏小部件

Widget I currently implement我目前实现的小部件

我目前实现的小部件

I want to implement this ios16 lock screen widget我想实现这个ios16锁屏小部件

I've made almost everything, but I haven't been able to implement the small circle's transparent border.我几乎什么都做了,就是没能实现小圆圈的透明边框。 I couldn't find a way to make even the background of the ring behind it transparent.我找不到使它后面的环的背景变得透明的方法。

My code我的代码

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)
                }
            }
        }
    }
}

How can I make a transparent border that also ignores the background of the view behind it?如何制作透明边框,同时忽略其背后视图的背景?

This is a great exercise.这是一个很好的练习。 The missing piece is a mask.缺少的部分是面具。

Note: Despite the fact that there are numerous ways to improve the existing code, I will try to stick to the original solution since the point is to gain experience through practice (based on the comments).注意:尽管有很多方法可以改进现有代码,但我会尽量坚持原来的解决方案,因为重点是通过实践获得经验(基于评论)。 However I will share some tips at the end.不过,我会在最后分享一些技巧。

So we can think of it in two steps:所以我们可以分两步来思考:

  1. We need some way to make another RingTipShape at the same (centered) position as our existing but a bit larger.我们需要一些方法来制作另一个RingTipShape与我们现有的相同(居中)position 但更大一点。
  2. We need to find a way to create a mask that removes only that shape from other content (in our case the track rings)我们需要找到一种方法来创建一个仅从其他内容中删除该形状的蒙版(在我们的例子中是轨道环)

The first point is an easy one, we just need to define the outer thickness in order to place the ellipse on top of the track at the correct location:第一点很简单,我们只需要定义外部厚度,以便将椭圆放置在轨道顶部的正确位置:

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

then our existing code changes to:然后我们现有的代码更改为:

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

now for the second part we need something to create a larger circle, which is easy:现在对于第二部分,我们需要一些东西来创建一个更大的圆,这很容易:

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

ok so now for the final part, we are going to use this (larger) shape to create a kind of inverted mask:好的,现在是最后一部分,我们将使用这个(更大的)形状来创建一种倒置的蒙版:

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
}

and we apply the mask like this:我们像这样应用面具:

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

which results to this:结果是:


Some observations/tips:一些观察/提示:

  1. You don't need the GeometryReader (and all the frame modifiers) in your CircularWidgetView , the ZStack will offer all available space to views.您不需要CircularWidgetView中的GeometryReader (和所有框架修改器), ZStack将为视图提供所有可用空间。
  2. You can add .aspectRatio(contentMode: .fit) to your image in order to avoid stretching.您可以将.aspectRatio(contentMode: .fit)添加到图像中以避免拉伸。
  3. You could take advantage of existing apis for making your track shapes.您可以利用现有的 api 来制作您的轨道形状。

For example:例如:

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))
    }
}

would result to:会导致:

which simplifies things a bit by utilizing the trim modifier.这通过使用trim修饰符简化了一些事情。

I hope that this makes sense.我希望这是有道理的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM