【问题标题】:SwiftUI add inverted maskSwiftUI 添加倒置遮罩
【发布时间】:2020-04-26 14:16:45
【问题描述】:

我正在尝试为两个形状添加一个遮罩,以便第二个形状遮盖第一个形状。如果我执行Circle().mask(Circle().offset(…)) 之类的操作,则会产生相反的效果:防止第一个圆圈之外的任何内容可见。

UIView 的答案在这里:iOS invert mask in drawRect

但是,尝试在 SwiftUI 中实现这一点没有 UIView 让我望而却步。我尝试实现一个 InvertedShape,然后我可以将其用作掩码:

struct InvertedShape<OriginalType: Shape>: Shape {
    let originalShape: OriginalType

    func path(in rect: CGRect) -> Path {
        let mutableOriginal = originalShape.path(in: rect).cgPath.mutableCopy()!
        mutableOriginal.addPath(Path(rect).cgPath)
        return Path(mutableOriginal)
            .evenOddFillRule()
    }
}

不幸的是,SwiftUI 路径没有addPath(Path)(因为它们是不可变的)或evenOddFillRule()。您可以访问路径的 CGPath 并制作可变副本,然后添加两个路径,但是,evenOddFillRule 需要在 CGLayer 上设置,而不是 CGPath。所以除非我能到达 CGLayer,否则我不确定如何继续。

这是 Swift 5。

【问题讨论】:

  • SwiftUI 路径 do 具有 addPath(Path) 以及奇偶填充规则,请参见下面我的答案。
  • @Asperi 我可以发誓我检查了 addPath,我想我有一个不可变的副本出于某种原因......
  • @Asperi 看起来像 Paths 没有填充规则,但 Shapes 有。
  • 在 SwiftUI 路径中'是一个'形状。
  • 类似问题已经回答here: - Inverted mask swiftui with system image

标签: swift swiftui


【解决方案1】:

基于this article,您可以使用.reverseMask 修饰符代替.mask。我对其进行了修改以支持 iOS 13 及更高版本。

extension View {
    @inlinable func reverseMask<Mask: View>(
        alignment: Alignment = .center,
        @ViewBuilder _ mask: () -> Mask
    ) -> some View {
            self.mask(
                ZStack {
                    Rectangle()

                    mask()
                        .blendMode(.destinationOut)
                }
            )
        }
}

用法:

ViewToMask()
.reverseMask {
    MaskView()
}

【讨论】:

    【解决方案2】:

    在接受的答案中使用掩码是一种好方法。不幸的是,掩码不会影响命中测试。可以通过以下方式制作带孔的形状。

    extension Path {
        var reversed: Path {
            let reversedCGPath = UIBezierPath(cgPath: cgPath)
                .reversing()
                .cgPath
            return Path(reversedCGPath)
        }
    }
    
    struct ShapeWithHole: Shape {
        func path(in rect: CGRect) -> Path {
            var path = Rectangle().path(in: rect)
            let hole = Circle().path(in: rect).reversed
            path.addPath(hole)
            return path
        }
    }
    

    诀窍是反转洞的路径。不幸的是,Path (还)不支持开箱即用的反转路径,因此扩展名(使用UIBezierPath)。然后该形状可用于剪辑和命中测试目的:

    struct MaskedView: View {
    
        var body: some View {
            Rectangle()
                .fill(Color.blue)
                .frame(width: 300, height: 100)
                .clipShape(ShapeWithHole())    // clips or masks the view
                .contentShape(ShapeWithHole()) // needed for hit-testing
        }
    }
    

    【讨论】:

    • 这将创建单个形状中路径的排除。它不会影响重叠的多个形状(尽管您可以使用此方法获得相同的视觉效果)。
    • 酷。对于我的用例,虽然我认为仍然需要一个蒙版,因为我需要掩盖一个描边形状,并且描边需要完全适合剪辑蒙版。 (没有我的代码 ATM 来玩技术,但我喜欢这个主意)
    • 蒙版很好,如果您需要命中测试以尊重形状,您仍然可以结合这两种方法。
    【解决方案3】:

    这是另一种方法,更快捷。

    诀窍是使用:

    YourMaskView()
       .compositingGroup()
       .luminanceToAlpha() 
    

    maskedView.mask(YourMaskView())

    只需使用黑白形状创建您的蒙版,黑色将是透明的,白色将是不透明的,介于两者之间的任何东西都将是半透明的。

    .compositingView(),类似于.drawingGroup(),栅格化视图(将其转换为位图纹理)。顺便说一句,当您 .blur 或执行任何其他像素级操作时也会发生这种情况。

    .luminanceToAlpha() 获取 RGB 亮度级别(我猜是通过平均 RGB 值),并将它们映射到位图的 Alpha(不透明度)通道。

    【讨论】:

    • 当 YourMaskView 为黑白时,这非常有用,谢谢
    • 也许 SwiftUI 已经更新,但我只是尝试遵循这一点,并且不再需要 .compositingGroup().luminanceToAlpha()。这导致它在我的情况下不起作用。相反,只需确保我的蒙版视图是黑白的,然后调用 maskedView.mask(YourMaskView()) 就可以了。
    • 我的蒙版视图包括绘制路径。它可能有内置的 alpha。
    【解决方案4】:

    这是一个仅通过 SwiftUI 创建倒置蒙版的可能方法的演示(以在视图中打洞为例)

    func HoleShapeMask(in rect: CGRect) -> Path {
        var shape = Rectangle().path(in: rect)
        shape.addPath(Circle().path(in: rect))
        return shape
    }
    
    struct TestInvertedMask: View {
    
        let rect = CGRect(x: 0, y: 0, width: 300, height: 100)
        var body: some View {
            Rectangle()
                .fill(Color.blue)
                .frame(width: rect.width, height: rect.height)
                .mask(HoleShapeMask(in: rect).fill(style: FillStyle(eoFill: true)))
        }
    }
    

    【讨论】:

    • 这就是我要找的! eoFill 是关键,看起来像...
    【解决方案5】:

    我还没有测试过,但是你能做这样的事情吗:

    extension UIView {
        func mask(_ rect: CGRect, invert: Bool = false) {
            let maskLayer = CAShapeLayer()
            let path = CGMutablePath()
    
            if (invert) {
                path.addRect(bounds)
            }
            path.addRect(rect)
            maskLayer.path = path
    
            if (invert) {
                maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
            }
    
            // Set the mask of the view.
            layer.mask = maskLayer
        }
    }
    
    struct MaskView: UIViewRepresentable {
        @Binding var child: UIHostingController<ImageView>
        @Binding var rect: CGRect
        @Binding var invert: Bool
    
        func makeUIView(context: UIViewRepresentableContext<MaskView>) -> UIView {
            let view = UIView()
    
            self.child.view.mask(self.rect, invert: self.invert)
    
            view.addSubview(self.child.view)
    
            return view
        }
    
        func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<MaskView>) {
    
        }
    }
    

    用法:

    struct ImageView: View {
        var body: some View {
            Image("image1")
        }
    }
    
    struct ContentView: View {
        @State var child = UIHostingController(rootView: ImageView())
        @State var rect: CGRect = CGRect(x: 50, y: 50, width: 50, height: 50)
        @State var invert: Bool = false
    
        var body: some View {
            VStack(alignment: .leading) {
                MaskView(child: self.$child, rect: self.$rect, invert: self.$invert)
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-01-30
      • 2016-12-27
      • 2012-01-07
      • 1970-01-01
      • 2016-09-30
      • 2018-10-05
      相关资源
      最近更新 更多