如何自己实现与iOS 16锁屏圆形小部件相同的功能?

3

我正在尝试自己实现锁屏小部件

我目前正在实现的小部件

Widget I currently implement

我想要实现这个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)
                }
            }
        }
    }
}


如何制作一个透明的边框,同时忽略其背后视图的背景?

只是为了确认一下...您不想使用Gauge有什么原因吗? - Alladinian
使用表盘没有问题。因为我认为亲手实现自己想要的小部件会是一次很好的经历,而且它也没有我想要的表盘界面。 - msgu
我非常尊重这个原因(并同意这种经验是无价的),所以在回答问题时考虑到了这一点。希望有所帮助! - Alladinian
1个回答

3

这是一个很棒的练习,缺失的部分是一个遮罩层。

注意:虽然有多种方法可以改进现有代码,但我将尝试坚持原始解决方案,因为重点是通过实践获得经验(基于评论)。不过,在最后我会分享一些提示。

所以我们可以分为两步考虑:

  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(和所有的帧修改器),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修饰符,这样可以简化事情。

我希望这有意义。


我已经完美地解决了。多亏了你,我学了不少东西。 有一些部分我不太理解。 我不确定为什么在 thumMask() 中需要 .luminanceToAlpha()。而且当 .luminanceToAlpha().mask() 一起使用时,行为非常难以理解。 - msgu
我不知道遮罩也可以应用透明度。谢谢,我的所有疑问都得到了解决。非常感谢! - msgu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接