问题
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