每次视图出现时,UIView 层的子层显示不同/随机

Posted

技术标签:

【中文标题】每次视图出现时,UIView 层的子层显示不同/随机【英文标题】:UIView layer's sublayers display differently/randomly each time view appears 【发布时间】:2016-05-06 22:36:05 【问题描述】:

我有一个简单的自定义 CALayer,可以在我的 UIView 上创建叠加渐变效果。代码如下:

class GradientLayer: CALayer 

var locations: [CGFloat]?
var origin: CGPoint?
var radius: CGFloat?
var color: CGColor?

convenience init(view: UIView, locations: [CGFloat]?, origin: CGPoint?, radius: CGFloat?, color: UIColor?) 
    self.init()
    self.locations = locations
    self.origin = origin
    self.radius = radius
    self.color = color?.CGColor
    self.frame = view.bounds


override func drawInContext(ctx: CGContext) 
    super.drawInContext(ctx)

    guard let locations = self.locations else  return 
    guard let origin = self.origin else  return 
    guard let radius = self.radius else  return 

    let colorSpace = CGColorGetColorSpace(color)
    let colorComponents = CGColorGetComponents(color)

    let gradient = CGGradientCreateWithColorComponents(colorSpace, colorComponents, locations, locations.count)
    CGContextDrawRadialGradient(ctx, gradient, origin, CGFloat(0), origin, radius, [.DrawsAfterEndLocation])


我在这里初始化和设置这些层:

override func viewWillLayoutSubviews() 
    super.viewWillLayoutSubviews()
    let gradient1 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX, y: view.frame.midY), radius: 100.0, color: UIColor(white: 1.0, alpha: 0.2))

    let gradient2 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX-20, y: view.frame.midY+20), radius: 160.0, color: UIColor(white: 1.0, alpha: 0.2))

    let gradient3 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX+30, y: view.frame.midY-30), radius: 300.0, color: UIColor(white: 1.0, alpha: 0.2))

    gradient1.setNeedsDisplay()
    gradient2.setNeedsDisplay()
    gradient3.setNeedsDisplay()

    view.layer.addSublayer(gradient1)
    view.layer.addSublayer(gradient2)
    view.layer.addSublayer(gradient3)

该视图似乎在大多数情况下都能正常显示,但(似乎)随机我会得到不同的渲染,如下所示。下面是一些例子(第一个是我想要的):

是什么导致了这个故障?如何每次只加载第一个?

【问题讨论】:

【参考方案1】:

你有几个问题。

首先,您应该将渐变视为 stops 的数组,其中 stop 有两部分:颜色和位置。您必须有相同数量的颜色和位置,因为每个站点都有一个。例如,如果您查看有关 components 参数的 CGGradientCreateWithColorComponents 文档,则可以看到这一点:

此数组中的项目数应该是count 与颜色空间中的组件数的乘积。

这是一个乘积(乘法的结果),因为您有 count 个色标,并且每个色标都需要一套完整的颜色分量。

您没有提供足够的颜色分量。你的GradientLayer 可以有任意数量的位置(你给它两个)但只有一种颜色。您正在获取一种颜色的组件并将其作为组件数组传递给CGGradientCreateWithColorComponents,但数组太短了。 Swift 没有捕捉到这个错误——注意你的colorComponents 的类型是UnsafePointer<CGFloat>Unsafe 部分告诉您您处于危险境地。 (您可以通过在 Xcode 中单击选项来查看 colorComponents 的类型。)

由于您没有为components 提供足够大的数组,ios 会使用在您的一种颜色的组件之后发生在内存中的任何随机值。这些可能会随着运行而变化,并且通常不是您想要的。

事实上,你甚至不应该使用CGGradientCreateWithColorComponents。您应该使用CGGradientCreateWithColors,它接受CGColor 的数组,因此它不仅使用起来更简单,而且更安全,因为它少了一个UnsafePointer

GradientLayer 应该是这样的:

class RadialGradientLayer: CALayer 

    struct Stop 
        var location: CGFloat
        var color: UIColor
    

    var stops: [Stop]  didSet  self.setNeedsDisplay()  
    var origin: CGPoint  didSet  self.setNeedsDisplay()  
    var radius: CGFloat  didSet  self.setNeedsDisplay()  

    init(stops: [Stop], origin: CGPoint, radius: CGFloat) 
        self.stops = stops
        self.origin = origin
        self.radius = radius
        super.init()
        needsDisplayOnBoundsChange = true
    

    override init(layer other: AnyObject) 
        guard let other = other as? RadialGradientLayer else  fatalError() 
        stops = other.stops
        origin = other.origin
        radius = other.radius
        super.init(layer: other)
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

    override func drawInContext(ctx: CGContext) 
        let locations = stops.map  $0.location 
        let colors = stops.map  $0.color.CGColor 

        locations.withUnsafeBufferPointer  pointer in
            let gradient = CGGradientCreateWithColors(nil, colors, pointer.baseAddress)
            CGContextDrawRadialGradient(ctx, gradient, origin, 0, origin, radius, [.DrawsAfterEndLocation])
        
    


下一个问题。每次系统调用viewWillLayoutSubviews 时,您都会添加更多渐变层。它可以多次调用该函数!例如,如果您的应用程序支持界面旋转,或者如果有来电并且 iOS 使状态栏变高,它将调用它。 (您可以通过选择 Hardware > Toggle In-Call Status Bar 在模拟器中进行测试。)

您需要创建一次渐变图层,并将它们存储在属性中。如果它们已经创建,您需要更新它们的框架而不是创建新层:

class ViewController: UIViewController 

    private var gradientLayers = [RadialGradientLayer]()

    override func viewWillLayoutSubviews() 
        super.viewWillLayoutSubviews()

        if gradientLayers.isEmpty 
            createGradientLayers()
        

        for layer in gradientLayers 
            layer.frame = view.bounds
        
    

    private func createGradientLayers() 
        let bounds = view.bounds
        let mid = CGPointMake(bounds.midX, bounds.midY)
        typealias Stop = RadialGradientLayer.Stop

        for (point, radius, color) in [
            (mid, 100, UIColor(white:1, alpha:0.2)),
            (CGPointMake(mid.x - 20, mid.y + 20), 160, UIColor(white:1, alpha:0.2)),
            (CGPointMake(mid.x + 30, mid.y - 30), 300, UIColor(white:1, alpha:0.2))
            ] as [(CGPoint, CGFloat, UIColor)] 

                let stops: [RadialGradientLayer.Stop] = [
                    Stop(location: 0, color: color),
                    Stop(location: 1, color: color.colorWithAlphaComponent(0))]

                let layer = RadialGradientLayer(stops: stops, origin: point, radius: radius)
                view.layer.addSublayer(layer)
                gradientLayers.append(layer)
        
    


【讨论】:

您能解释一下init(layer other: AnyObject) 的用途吗? 来自文档:“这个初始化器用于创建图层的影子副本,例如,presentationLayer 方法。 […] 如果你正在实现一个自定义层子类,你可以重写这个方法并使用它将实例变量的值复制到新对象中。”【参考方案2】:

您的问题是您在 viewWillLayoutSubviews 函数中编写的代码,当视图加载时它会被多次调用,只需添加一个检查以运行一次或更好,然后在 viewdidlayoutsubviews 中添加一个检查以运行一次

【讨论】:

以上是关于每次视图出现时,UIView 层的子层显示不同/随机的主要内容,如果未能解决你的问题,请参考以下文章

子视图的子层与更高的子视图重叠

如何保持 CALayer(CATiledLayer 的子层)在缩放后不改变其比例?

为啥添加的子图层不显示在屏幕截图中?

UIView 层不随视图调整大小?

具有弹簧效果的旋转动画

当视图消失时,UIView动画停止,重新出现时不再恢复