Radial CAGradientLayer not rendering properly at runtime

老子叫甜甜 提交于 2019-12-24 19:42:54

问题


I'm trying to create a radial CAGradientLayer in swift. Normal CAGradientLayers (with default type axial) are no problem. They are rendering fine on the snapshot and on runtime in the simulator.

When I take a snapshot of the UIView that I created (solely for the CAGradientLayer), it shows a beautiful radial CAGradientLayer. However, when I use this view and look at it at runtime in the simulator, it looks awful. I don't know what I'm doing wrong.

This is the code for the CAGradientLayer:

import Foundation

class ChannelGradientView: UIView {

  // MARK: - Init

  override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
  }

  // MARK: - Setup

  private func setup() {
    backgroundColor = .clear
    setupGradient()
  }

  private func setupGradient() {
    let gradient = layer as? CAGradientLayer
    gradient?.type = .radial

    gradient?.colors = [
      UIColor.black.withAlphaComponent(0.8),
      UIColor.black.withAlphaComponent(0)
    ].map { $0.cgColor }

    let blackPoint = CGPoint(x: 1, y: 0)
    let clearPoint = CGPoint(x: 1, y: 1)

    // startpoint is center (for example black here)
    gradient?.startPoint = blackPoint
    gradient?.endPoint = clearPoint
  }

  // MARK: - Layer

  override public class var layerClass: Swift.AnyClass {
    return CAGradientLayer.self
  }
}

This is what it looks like when I take a snapshot unit test (with Nimble Snapshot library):

And this is what it looks like at runtime on a simulator:

Anyone that has an idea of what I'm doing wrong?

EDIT: Running at an actual device, it doesn't even show any gradient layer, not even the crappy one. It's a clear box.


回答1:


Not sure what else might be going on between what you see on Simulator vs Device, but...

If I use your code as-is, I get this (red border to show the frame):

If I change your clearPoint:

//let clearPoint = CGPoint(x: 1, y: 1)
let clearPoint = CGPoint(x: 0, y: 1)    // bottom-left-corner

I get this:

Appearance is the same on Simulator and Device


EDIT

Some additional clarification...

The Radial Gradient doesn't use .startPoint and .endPoint in the same way that an .axial (linear) gradient does.

With .radial, a gradient *ellipse is drawn, using .startPoint as its center, and the difference between startPoint.x and endPoint.x times 2 as its width and the difference between startPoint.y and endPoint.y times 2 as its height.

So, to get the top-right to bottom-left radial gradient you want, you need to set .startPoint to 1,0 and the .endPoint to values which result in a gradient oval of size 2 x 2:

startPoint = 1,0

endPoint = 0,1

    width:  abs(1 - 0) * 2 = 2
    height: abs(0 - 1) * 2 = 2

note that you can achieve the same result with:

startPoint = 1,0

endPoint = 2,-1

    width:  abs(1 - 2) * 2 = 2
    height: abs(0 - (-1)) * 2 = 2

What the .radial gradient doesn't like is to have its width or height set to Zero, which is what we got with:

startPoint = 1,0

endPoint = 1,1

    width:  abs(1 - 1) * 2 = 0   // problem!!!!
    height: abs(0 - 1) * 2 = 2

The result, as we've seen, is the weird line pattern.

Here are some practical examples to demonstrate.

Axial / Linear top-right to bottom-left:

Radial centered and width = view width / height = view height:

Radial centered and width = one-half view width / height = view height:

Radial centered and width = one-half view width / height = view height... Exact same result, but note that the .endPoint.x value is 0.75 instead of 0.25:

Now we change .startPoint.x = 0.75, but we leave .endPoint.x = 0.25, so the center of the ellipse moves to 3/4ths of the width of the view, but the width of the ellipse becomes equal to the width of the view... abs(0.75 - 0.25) * 2 == 1.0:

Change .endPoint.x = 0.5 and the width returns to 1/2 the width of the view... abs(0.75 - 0.5) * 2 = 0.5:

And finally radial gradient from top-right to bottom-left:

Here is the code I used to generate these images. It has a data-block of "Gradient Definitions"... You can add / change those definitions to experiment with the differences.

//
//  GradTestViewController.swift
//
//  Created by Don Mag on 9/12/19.
//

import UIKit

class TestGradientView: UIView {

    // MARK: - Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    // MARK: - Setup
    private func setup() {

        // just set the background to clear and the
        //  gradient colors to blue -> green
        backgroundColor = .clear

        if let gradient = layer as? CAGradientLayer {
            gradient.colors = [
                UIColor.blue,
                UIColor.green,
                ].map { $0.cgColor }
        }

    }

    // MARK: - Layer
    override public class var layerClass: Swift.AnyClass {
        return CAGradientLayer.self
    }

}

struct GradDef {
    var gradType: CAGradientLayerType = .axial
    var startPoint: CGPoint = CGPoint(x: 1.0, y: 0.0)
    var endPoint: CGPoint = CGPoint(x: 0.0, y: 1.0)
}

class GradTestViewController: UIViewController {

    var theButton: UIButton = {
        let v = UIButton()
        v.setTitle("Tap", for: .normal)
        v.setTitleColor(.white, for: .normal)
        v.setTitleColor(.lightGray, for: .highlighted)
        v.backgroundColor = .red
        return v
    }()

    var counterLabel: UILabel = {
        let v = UILabel()
        return v
    }()

    var descLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        return v
    }()

    var gradContainerView: UIView = {
        let v = UIView()
        return v
    }()

    var gradView: TestGradientView = {
        let v = TestGradientView()
        return v
    }()

    var tlLabel: UILabel = {
        let v = UILabel()
        v.text = "0,0"
        return v
    }()

    var trLabel: UILabel = {
        let v = UILabel()
        v.text = "1,0"
        return v
    }()

    var blLabel: UILabel = {
        let v = UILabel()
        v.text = "0,1"
        return v
    }()

    var brLabel: UILabel = {
        let v = UILabel()
        v.text = "1,1"
        return v
    }()

    var theStackView: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.alignment = .center
        v.distribution = .fill
        v.spacing = 20.0
        return v
    }()

    var gradDefs: [GradDef] = [
        GradDef(gradType: .axial,  startPoint: CGPoint(x: 1.0,  y: 0.0), endPoint: CGPoint(x: 0.0,  y: 1.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.5,  y: 0.5), endPoint: CGPoint(x: 0.0,  y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.5,  y: 0.5), endPoint: CGPoint(x: 0.25, y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.5,  y: 0.5), endPoint: CGPoint(x: 0.75, y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.75, y: 0.5), endPoint: CGPoint(x: 0.25, y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 0.75, y: 0.5), endPoint: CGPoint(x: 1.0,  y: 0.0)),
        GradDef(gradType: .radial, startPoint: CGPoint(x: 1.0,  y: 0.0), endPoint: CGPoint(x: 0.0,  y: 1.0)),
    ]

    var idx: Int = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        [theButton, counterLabel, gradContainerView, gradView, tlLabel, trLabel, blLabel, brLabel, descLabel, theStackView].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }

        [theButton, counterLabel, gradContainerView, descLabel].forEach {
            theStackView.addArrangedSubview($0)
        }

        [gradView, tlLabel, trLabel, blLabel, brLabel].forEach {
            gradContainerView.addSubview($0)
        }

        [counterLabel, tlLabel, trLabel, blLabel, brLabel, descLabel].forEach {
            $0.font = UIFont.monospacedDigitSystemFont(ofSize: 14.0, weight: .regular)
        }

        NSLayoutConstraint.activate([

            gradView.widthAnchor.constraint(equalToConstant: 120),
            gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor),
            gradView.centerXAnchor.constraint(equalTo: gradContainerView.centerXAnchor),
            gradView.centerYAnchor.constraint(equalTo: gradContainerView.centerYAnchor),

            tlLabel.centerXAnchor.constraint(equalTo: gradView.leadingAnchor, constant: 0.0),
            blLabel.centerXAnchor.constraint(equalTo: gradView.leadingAnchor, constant: 0.0),
            trLabel.centerXAnchor.constraint(equalTo: gradView.trailingAnchor, constant: 0.0),
            brLabel.centerXAnchor.constraint(equalTo: gradView.trailingAnchor, constant: 0.0),

            tlLabel.bottomAnchor.constraint(equalTo: gradView.topAnchor, constant: -2.0),
            trLabel.bottomAnchor.constraint(equalTo: gradView.topAnchor, constant: -2.0),

            blLabel.topAnchor.constraint(equalTo: gradView.bottomAnchor, constant: 2.0),
            brLabel.topAnchor.constraint(equalTo: gradView.bottomAnchor, constant: 2.0),

            tlLabel.topAnchor.constraint(equalTo: gradContainerView.topAnchor, constant: 4.0),
            tlLabel.leadingAnchor.constraint(equalTo: gradContainerView.leadingAnchor, constant: 4.0),

            brLabel.trailingAnchor.constraint(equalTo: gradContainerView.trailingAnchor, constant: -4.0),
            brLabel.bottomAnchor.constraint(equalTo: gradContainerView.bottomAnchor, constant: -4.0),

            ])

        view.addSubview(theStackView)

        NSLayoutConstraint.activate([

            theStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
            theStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),

            theButton.widthAnchor.constraint(equalToConstant: 160.0),
            descLabel.widthAnchor.constraint(equalToConstant: 240.0),

            ])

        theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)

        idx = -1
        didTap(nil)
    }

    @IBAction func didTap(_ sender: Any?) {

        guard let gLayer = gradView.layer as? CAGradientLayer else {
            fatalError("Could not get the gradient layer!")
        }

        idx += 1

        if idx >= gradDefs.count {
            idx = 0
        }

        let gDef = gradDefs[idx]

        gLayer.type = gDef.gradType
        gLayer.startPoint = gDef.startPoint
        gLayer.endPoint = gDef.endPoint

        var s = ""
        s += "Gradient Type: " + (gDef.gradType == CAGradientLayerType.axial ? "Axial" : "Radial")
        s += "\n\n"
        s += "Start Point: \(gDef.startPoint)"
        s += "\n"
        s += "End Point:   \(gDef.endPoint)"

        if gDef.gradType == CAGradientLayerType.radial {
            let w = abs(gDef.startPoint.x - gDef.endPoint.x) * 2
            let h = abs(gDef.startPoint.y - gDef.endPoint.y) * 2
            s += "\n\n"
            s += "\t" + "Radial Width:"
            s += "\n"
            s += "\t\t" + "abs(\(gDef.startPoint.x) - \(gDef.endPoint.x)) * 2 == \(w)"
            s += "\n\n"
            s += "\t" + "Radial Height:"
            s += "\n"
            s += "\t\t" + "abs(\(gDef.startPoint.y) - \(gDef.endPoint.y)) * 2 == \(h)"
        }

        s += "\n"

        descLabel.text = s

        counterLabel.text = "Variation \(idx + 1) of \(gradDefs.count)"

    }

}

Everything's done in code - no @IBOutlets or @IBActions, so just create a new view controller and assign its Custom Class to GradTestViewController.




回答2:


here is the fix, set your point as follows:

    let blackPoint = CGPoint(x: 1.0, y: 0.0)
    let clearPoint = CGPoint(x: 0.0, y: 1.0)

Tell me if its ok in your environment.

--

I put your whole code in a playground as follows, where I made some adjustments (I stuck the gradient in the right top corner):

//: A UIKit based Playground for presenting user interface

import UIKit
import PlaygroundSupport



class ChannelGradientView: UIView {

  // MARK: - Init

  override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
  }

  // MARK: - Setup

  private func setup() {
    backgroundColor = .clear
    setupGradient()
  }

  private func setupGradient() {
    guard let gradient = layer as? CAGradientLayer else { return }
    //let gradient = CAGradientLayer()
    gradient.type = .radial
    gradient.frame = frame
    gradient.colors = [
        UIColor.black.cgColor,
        UIColor.clear.cgColor
    ]

    let blackPoint = CGPoint(x: 1.0, y: 0.0)
    let clearPoint = CGPoint(x: 0.0, y: 1.0)

    // startpoint is center (for example black here)
    gradient.startPoint = blackPoint
    gradient.endPoint = clearPoint
  }

  // MARK: - Layer

  override public class var layerClass: Swift.AnyClass {
    return CAGradientLayer.self
  }
}


class MyViewController : UIViewController {
    override func loadView() {
        let view = UIView()
        self.view = view

        view.backgroundColor = .white

        let frame = CGRect(x: 150, y: 200, width: 200, height: 200)

        let iv = UIImageView()
        iv.frame = frame
        iv.alpha = 0.8
        iv.contentMode = .scaleAspectFill
        if let url = URL(string: "https://images-na.ssl-images-amazon.com/images/I/61VpNkHPRoL._SX679_.jpg"),
            let img = try? Data(contentsOf: url) {
            iv.image = UIImage(data: img)
        }
        view.addSubview(iv)
        iv.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            iv.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        iv.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        iv.topAnchor.constraint(equalTo: view.topAnchor),
        iv.bottomAnchor.constraint(equalTo: view.bottomAnchor),

        ])


        let gd = ChannelGradientView()
        view.addSubview(gd)
        gd.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
        gd.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        gd.topAnchor.constraint(equalTo: view.topAnchor),
        gd.widthAnchor.constraint(equalTo: view.widthAnchor),
        gd.heightAnchor.constraint(equalTo: gd.widthAnchor),

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

And it does display properly:



来源:https://stackoverflow.com/questions/57889427/radial-cagradientlayer-not-rendering-properly-at-runtime

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!