将 UIView (Overlay) 与应用背景混合

Posted

技术标签:

【中文标题】将 UIView (Overlay) 与应用背景混合【英文标题】:Blend UIView (Overlay) with app background 【发布时间】:2020-05-11 14:54:34 【问题描述】:

我想使用特殊的混合模式(在我的例子中为叠加模式)将 UIView 与我的应用背景混合。但是,要混合的视图包含在复杂的视图层次结构中。

可以使用view.layer.compositingFilter = "overlayBlendMode" 将视图与其直接同级视图混合,但该视图不会与非同级视图混合,例如应用背景。

为了重现问题,我做了以下操场:

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController 
    override func loadView() 
        let parentView = UIView()
        parentView.backgroundColor = .purple

        // Child view
        let childView = UIView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
        childView.layer.borderColor = UIColor.orange.cgColor
        childView.layer.borderWidth = 3
        parentView.addSubview(childView)

        // Child child view
        let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
        childChildView.backgroundColor = .white
        childChildView.layer.compositingFilter = "overlayBlendMode"
        childView.addSubview(childChildView)

        self.view = parentView
    

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

我们可以在这里看到,白色的子子视图没有被混合:

而视图应该像这样混合显示(边框不应该改变颜色):

为了创建第二张图片,我在childView 而不是childChildView 上应用了合成滤镜,这将混合所有其他子视图——因此这不是我想要的。我只是想要混合这个特定的视图。

注意:这个视图应该可以移动,因为它在 UIScrollView 内。

编辑:带有图像背景和滚动视图的更复杂示例

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController 

    override func loadView() 
        let parentView = UIView()

        // Background image
        let backgroundImageView = UIImageView(image: UIImage(named: "image.jpg")!)
        backgroundImageView.frame = UIScreen.main.bounds
        parentView.addSubview(backgroundImageView)

        // Page view (horizontal scrollview)
        let pageView = UIScrollView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
        pageView.contentSize = CGSize(width: 600, height: 200)
        pageView.flashScrollIndicators()
        pageView.layer.borderColor = UIColor.orange.cgColor
        pageView.layer.borderWidth = 3
        parentView.addSubview(pageView)

        // Child view (vertical scrollview)
        let childView = UIScrollView(frame: CGRect(x: 20, y: 20, width: 100, height: 150))
        childView.contentSize = CGSize(width: 100, height: 300)
        childView.layer.borderColor = UIColor.green.cgColor
        childView.layer.borderWidth = 3
        pageView.addSubview(childView)

        // Child child view
        let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
        childChildView.backgroundColor = .white
        childChildView.layer.compositingFilter = "overlayBlendMode"
        childView.addSubview(childChildView)

        self.view = parentView
    


// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

【问题讨论】:

我认为 compositingFilterios 中不受支持(如文档中所写)...这真的适用于设备吗? 是的,它也适用于设备,我不知道为什么它在文档中写得相反 【参考方案1】:

更新 2:

我尝试了几种方法,包括添加图层或创建使用背景图像作为输入图像的自定义图像过滤器,但这些解决方案都没有得到预期的结果。主要问题始终是视图层次结构。

一旦创建 childChildView 但在显示之前,我可能已经通过使用生成的视图图像或实际背景图像作为 childView 的内容背景找到了解决方案。我已经稍微更改了您的示例代码,以在 parentView 中添加滚动视图和背景图像。看看这是否适合你/是你想要的结果:

    import UIKit
import PlaygroundSupport

class MyViewController : UIViewController 

    override func loadView() 
        let parentView = UIView()
        parentView.backgroundColor = .purple

        let imageName = "image.jpg"
        let image = UIImage(named: imageName)
        let imageWidth = Int((image?.size.width)!)
        let imageheight = Int((image?.size.height)!)
        let imageView = UIImageView(image: image!)
        imageView.frame = CGRect(x: 50, y: 50, width: imageWidth , height: imageheight)
        parentView.addSubview(imageView)

        // Child view as UIScrollView
        let childView = UIScrollView(frame: CGRect(x: 55, y: 55, width: imageWidth - 10, height: imageheight - 10 ))
        childView.contentSize = CGSize(width: imageWidth - 10, height: 5000)
        childView.layer.borderColor = UIColor.orange.cgColor
        childView.flashScrollIndicators()
        childView.layer.borderWidth = 10
        parentView.addSubview(childView)

        // ChildChild view
        let childChildView = UIView(frame: CGRect(x: 15, y: 100, width: 85, height: imageheight - 180))
        childChildView.layer.compositingFilter = "overlayBlendMode"
        childChildView.backgroundColor = .white

        //Creating a static image of the background views BEFORE adding the childChildView.
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        format.preferredRange = .standard ///color profile
        ///Change the imageView to the parentView size of the app. Not available if not set in the playground.
        let renderer = UIGraphicsImageRenderer(size: imageView.bounds.size, format: format)
        let imageBG = renderer.image  context in
            ///This draws all subviews of the parentView one after the other.
            ///Because the background image is not a parent of our current view, otherwise childView.drawHierachy would have been enough
            for subview in parentView.subviews 
                    ///Skip specific views or view classes you don't want to be added to the image. or if you only need the parentView itself rendered remove the for in loop.
                    subview.drawHierarchy(in: imageView.bounds, afterScreenUpdates: true)
            
        
        //Adding the static background image. This could simply also be the actual image: UIImage if no other views are supposed to be used.
        childView.layer.contents = imageBG.cgImage

        childView.addSubview(childChildView)

        self.view = parentView
    


// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

结果如下:

更新:

图像中的颜色具有误导性,因为您可以假设正常的透明效果是相同的。但是正如 Coconuts 指出的那样,overlayBlendMode 完全不同。我认为问题在于 compositingFilter 仅适用于下面的视图,即使此视图是透明的。 我尝试通过使用从 childview 中切出 childchild 大小的正方形的掩码来找到解决方法。但这也不起作用,因为掩码也适用于所有子视图。我让它工作的唯一方法是让 childchildview 成为 childview 的兄弟,或者是背景视图的直接子视图。但不确定这在 Coconuts 提到的复杂视图层次结构中是否可行。

    // Sibling view with adjusted x and y
    let childChildView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
    childChildView.backgroundColor = .white
    childChildView.layer.compositingFilter = "overlayBlendMode"

    parentView.addSubview(childChildView)

杂项:

仅获取示例图像的视觉结果,而不是实际使用 Coconut 要求的 overlayBlendMode 过滤器。

如果您只需要混合颜色,您可以更改颜色的 alpha 值。

    // Child child view
    let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
    childChildView.backgroundColor = UIColor(white: 1, alpha: 0.5)
    //childChildView.layer.compositingFilter = "overlayBlendMode"
    childView.addSubview(childChildView)

或者试试这个:

    // Child child view
    let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 50))
    childChildView.backgroundColor = .white
    childChildView.layer.opacity = 0.5
    childView.addSubview(childChildView)

拥有多个滚动视图时的其他尝试:

这是一个尝试解决来自 Coconut 添加的具有多个滚动视图的更复杂的视图层次结构。当应用程序更新(重绘)其视图时,性能需要改进或调整背景图像层的背景图像的部分需要同步运行。目前有点落后。

    import UIKit
import PlaygroundSupport

class MyViewController : UIViewController 

    override func loadView() 

        let parentView = UIView()

        // Background image
        let backgroundImageView = UIImageView(image: UIImage(named: "image.jpg")!)
        backgroundImageView.frame = UIScreen.main.bounds
        parentView.addSubview(backgroundImageView)

        // Page view (horizontal scrollview)
        let pageView = UIScrollView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
        pageView.contentSize = CGSize(width: 600, height: 200)
        pageView.flashScrollIndicators()
        pageView.layer.borderColor = UIColor.yellow.cgColor
        pageView.layer.borderWidth = 3
        pageView.clipsToBounds = true
        parentView.addSubview(pageView)

        // Child view (vertical scrollview)
        let childView = UIScrollView(frame: CGRect(x: 20, y: 20, width: 100, height: 150))
        childView.contentSize = CGSize(width: 100, height: 300)
        childView.layer.borderColor = UIColor.red.cgColor
        childView.layer.borderWidth = 3
        pageView.addSubview(childView)

        // Child child view
        let childChildView = UIView(frame: CGRect(x: 50, y: 50, width: 50, height: 50))

        //Child child view foreground sublayer
        let childChildFrontLayer = CALayer()
        childChildFrontLayer.frame = childChildView.frame.offsetBy(dx: -75, dy: -50)
        childChildFrontLayer.backgroundColor = UIColor.white.cgColor
        childChildFrontLayer.compositingFilter = "overlayBlendMode"

        //Child child view background sublayer
        let childChildBackLayer = CALayer()
        childChildBackLayer.contents = UIImage(named: "image.jpg")?.cgImage
        var absolutFrame = parentView.convert(childChildView.frame, from: childView)
        childChildBackLayer.frame = CGRect(x: -absolutFrame.minX, y: -absolutFrame.minY, width: backgroundImageView.frame.width, height: backgroundImageView.frame.height)

        childChildView.layer.addSublayer(childChildBackLayer)
        childChildView.layer.addSublayer(childChildFrontLayer)
        childView.addSubview(childChildView)

        //Checking for any scrolling. Is slightly faster then the scollview delegate methods but might cause main thread checker warning.
        DispatchQueue.global(qos: .userInteractive).async 
            while true 
                if pageView.isDragging || pageView.isTracking || pageView.isDecelerating || childView.isDragging || childView.isTracking || childView.isDecelerating 
                    absolutFrame = parentView.convert(childChildView.frame, from: childView)
                    DispatchQueue.main.async 
                        childChildBackLayer.frame = CGRect(x: -absolutFrame.minX, y: -absolutFrame.minY, width: backgroundImageView.frame.width, height: backgroundImageView.frame.height)
                    
                
            
        
        self.view = parentView
    

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

【讨论】:

不,重点是能够使用特殊的混合模式进行混合(在我的情况下为叠加) 我是否理解正确,您想使用叠加混合模式(如果您不仅使用紫色背景,我猜它会产生不同的效果?)但它似乎不起作用因为 childchildView 基本上是在尝试将过滤器应用于子视图(即使这个视图只有边框但没有背景颜色?) 是的,childChildView 只与它的直接兄弟混合,而我希望它与它后面的所有视图混合,直到应用程序背景。关于您的更新,我确实不可能在层次结构中向上移动视图,因为它包含在可滚动的滚动视图中 - 另外,这会使整个应用程序视图层次结构变得一团糟:/ 好的,希望这对你有用。否则我没有想法。 ;) 这是一个进步!尽管如此,这还是提出了两点:1)当childView小于图像视图时(当滚动视图不占据整个页面时)你怎么办,以及2)当childView本身在另一个滚动视图中时你怎么办?就我而言,childView 等效项(垂直滚动视图)本身包含在水平滚动视图中以制作“页面”。真的没有办法告诉视图与它后面的任何视图混合,不管视图层次结构如何?【参考方案2】:

与子视图背景相关的问题是否清晰,因此“混合”以提供白色。是否可以将子视图背景颜色设置为与应用背景相同,然后将 childView 混合到 childChildView 中?

【讨论】:

我的应用背景实际上可以是渐变或图像,所以我认为我不能这样做:/

以上是关于将 UIView (Overlay) 与应用背景混合的主要内容,如果未能解决你的问题,请参考以下文章

UIView 背景大小问题

UIView 高度无法跨设备正确缩放图像

Android Overlay机制总结

Android Overlay机制总结

Android Overlay机制总结

使用自动布局为 UIView 应用渐变背景