问题
I'm trying to get my CAGradientLayers, that i'm using to create nice gradient backgrounds, to resize nicely on rotation and modal view presentation, but they will not play ball.
Here is a video I just created showing my problem: Notice the tearing on rotation.
Also please note this video was created by filming the iPhone Simulator on OS X. I have slowed down the animations in the video to highlight my issue.
Video of Problem...
Here is an Xcode project which I just created (which is the source for the app shown in the video), basically as illustrated the issue occurs on rotation and especially when views are presented modally:
Xcode Project, modally presenting views with CAGradientLayer backgrounds...
For what it's worth I understand that using:
[[self view] setBackgroundColor:[UIColor blackColor]];
does a reasonable job of making the transitions a bit more seamless and less jarring, but if you look at the video when I, whilst currently in landscape mode, modally present a view, you will see why the above code will not help.
Any ideas what I can do to sort this out?
John
回答1:
When you create a layer (like your gradient layer), there's no view managing the layer (even when you add it as a sublayer of some view's layer). A standalone layer like this doesn't participate in the UIView
animation system.
So when you update the frame of the gradient layer, the layer animates the change with its own default animation parameters. (This is called “implicit animation”.) These default parameters don't match the animation parameters used for interface rotation, so you get a weird result.
I didn't look at your project but it's trivial to reproduce your problem with this code:
@interface ViewController ()
@property (nonatomic, strong) CAGradientLayer *gradientLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.gradientLayer = [CAGradientLayer layer];
self.gradientLayer.colors = @[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor blackColor].CGColor ];
[self.view.layer addSublayer:self.gradientLayer];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.gradientLayer.frame = self.view.bounds;
}
@end
Here's what that looks like, with slow motion enabled in the simulator:
Fortunately, this is an easy problem to fix. You need to make your gradient layer be managed by a view. You do that by creating a UIView
subclass that uses a CAGradientLayer
as its layer. The code is tiny:
// GradientView.h
@interface GradientView : UIView
@property (nonatomic, strong, readonly) CAGradientLayer *layer;
@end
// GradientView.m
@implementation GradientView
@dynamic layer;
+ (Class)layerClass {
return [CAGradientLayer class];
}
@end
Then you need to change your code to use GradientView
instead of CAGradientLayer
. Since you're using a view now instead of a layer, you can set the autoresizing mask to keep the gradient sized to its superview, so you don't have to do anything later to handle rotation:
@interface ViewController ()
@property (nonatomic, strong) GradientView *gradientView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.gradientView = [[GradientView alloc] initWithFrame:self.view.bounds];
self.gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.gradientView.layer.colors = @[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor blackColor].CGColor ];
[self.view addSubview:self.gradientView];
}
@end
Here's the result:
回答2:
The best part about @rob's answer is that the view controls the layer for you. Here is the Swift code that properly overrides the layer class and sets the gradient.
import UIKit
class GradientView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
autoresizingMask = [.flexibleWidth, .flexibleHeight]
guard let theLayer = self.layer as? CAGradientLayer else {
return;
}
theLayer.colors = [UIColor.whiteColor.cgColor, UIColor.lightGrayColor.cgColor]
theLayer.locations = [0.0, 1.0]
theLayer.frame = self.bounds
}
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
}
You can then add the view in two lines wherever you want.
override func viewDidLoad() {
super.viewDidLoad()
let gradientView = GradientView(frame: self.view.bounds)
self.view.insertSubview(gradientView, atIndex: 0)
}
回答3:
My swift version:
import UIKit
class GradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
func gradientWithColors(firstColor : UIColor, _ secondColor : UIColor) {
let deviceScale = UIScreen.mainScreen().scale
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRectMake(0.0, 0.0, self.frame.size.width * deviceScale, self.frame.size.height * deviceScale)
gradientLayer.colors = [ firstColor.CGColor, secondColor.CGColor ]
self.layer.insertSublayer(gradientLayer, atIndex: 0)
}
}
Note that I also had to use the device scale to calculate the frame size - to get correct auto-sizing during orientation changes (with auto-layout).
- In Interface Builder, I added a UIView and changed its class to GradientView (the class shown above).
- I then created an outlet for it (myGradientView).
Finally, In the view controller I added:
override func viewDidLayoutSubviews() { self.myGradientView.gradientWithColors(UIColor.whiteColor(), UIColor.blueColor()) }
Note that the gradient view is created in a "layoutSubviews" method, since we need a finalized frame to create the gradient layer.
回答4:
It will look better when you insert this piece of code and remove the willAnimateRotationToInterfaceOrientation:duration:
implementation.
- (void)viewWillLayoutSubviews
{
[[[self.view.layer sublayers] objectAtIndex:0] setFrame:self.view.bounds];
}
This is however not very elegant. In a real application you should subclass UIView to create a gradient view. In this custom view you can override layerClass so that it is backed by a gradient layer:
+ (Class)layerClass
{
return [CAGradientLayer class];
}
Also implement layoutSubviews
to handle when the bounds of the view changes.
When creating this background view use autoresizing masks so that the bounds automatically adjust on interface rotations.
回答5:
Complete Swift version. Set viewFrame
from the viewController that owns this view in viewDidLayoutSubviews
import UIKit
class MainView: UIView {
let topColor = UIColor(red: 146.0/255.0, green: 141.0/255.0, blue: 171.0/255.0, alpha: 1.0).CGColor
let bottomColor = UIColor(red: 31.0/255.0, green: 28.0/255.0, blue: 44.0/255.0, alpha: 1.0).CGColor
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupGradient()
}
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
return layer as! CAGradientLayer
}
var viewFrame: CGRect! {
didSet {
self.bounds = viewFrame
}
}
private func setupGradient() {
gradientLayer.colors = [topColor, bottomColor]
}
}
回答6:
Another swift version - which is not using drawRect.
class UIGradientView: UIView {
override class func layerClass() -> AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
return layer as! CAGradientLayer
}
func setGradientBackground(colors: [UIColor], startPoint: CGPoint = CGPoint(x: 0.5, y: 0), endPoint: CGPoint = CGPoint(x: 0.5, y: 1)) {
gradientLayer.startPoint = startPoint
gradientLayer.endPoint = endPoint
gradientLayer.colors = colors.map({ (color) -> CGColor in return color.CGColor })
}
}
In controller I just call:
gradientView.setGradientBackground([UIColor.grayColor(), UIColor.whiteColor()])
回答7:
Info
- Use as one line solution
- Replacing gradient when you add it to the view again (to use in reusables)
- Automatically transiting
- Automatically removing
Details
Swift 3.1, xCode 8.3.3
Solution
import UIKit
extension UIView {
func addGradient(colors: [UIColor], locations: [NSNumber]) {
addSubview(ViewWithGradient(addTo: self, colors: colors, locations: locations))
}
}
class ViewWithGradient: UIView {
private var gradient = CAGradientLayer()
init(addTo parentView: UIView, colors: [UIColor], locations: [NSNumber]){
super.init(frame: CGRect(x: 0, y: 0, width: 1, height: 2))
restorationIdentifier = "__ViewWithGradient"
for subView in parentView.subviews {
if let subView = subView as? ViewWithGradient {
if subView.restorationIdentifier == restorationIdentifier {
subView.removeFromSuperview()
break
}
}
}
let cgColors = colors.map { (color) -> CGColor in
return color.cgColor
}
gradient.frame = parentView.frame
gradient.colors = cgColors
gradient.locations = locations
backgroundColor = .clear
parentView.addSubview(self)
parentView.layer.insertSublayer(gradient, at: 0)
parentView.backgroundColor = .clear
autoresizingMask = [.flexibleWidth, .flexibleHeight]
clipsToBounds = true
parentView.layer.masksToBounds = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let parentView = superview {
gradient.frame = parentView.bounds
}
}
override func removeFromSuperview() {
super.removeFromSuperview()
gradient.removeFromSuperlayer()
}
}
Usage
viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
Using StoryBoard
ViewController
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var viewWithGradient: UIView!
override func viewDidLoad() {
super.viewDidLoad()
viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
}
}
StoryBoard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="stackoverflow_17555986" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uii-31-sl9">
<rect key="frame" x="66" y="70" width="243" height="547"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="wfy-db-euE" firstAttribute="top" secondItem="uii-31-sl9" secondAttribute="bottom" constant="50" id="a7J-Hq-IIq"/>
<constraint firstAttribute="trailingMargin" secondItem="uii-31-sl9" secondAttribute="trailing" constant="50" id="i9v-hq-4tD"/>
<constraint firstItem="uii-31-sl9" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="50" id="wlO-83-8FY"/>
<constraint firstItem="uii-31-sl9" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="50" id="zb6-EH-j6p"/>
</constraints>
</view>
<connections>
<outlet property="viewWithGradient" destination="uii-31-sl9" id="FWB-7A-MaH"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
Programmatically
import UIKit
class ViewController2: UIViewController {
@IBOutlet weak var viewWithGradient: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let viewWithGradient = UIView(frame: CGRect(x: 10, y: 20, width: 30, height: 40))
view.addSubview(viewWithGradient)
viewWithGradient.translatesAutoresizingMaskIntoConstraints = false
let constant:CGFloat = 50.0
NSLayoutConstraint(item: viewWithGradient, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: constant).isActive = true
NSLayoutConstraint(item: viewWithGradient, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailingMargin
, multiplier: 1.0, constant: -1*constant).isActive = true
NSLayoutConstraint(item: viewWithGradient, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottomMargin
, multiplier: 1.0, constant: -1*constant).isActive = true
NSLayoutConstraint(item: viewWithGradient, attribute: .top, relatedBy: .equal, toItem: view, attribute: .topMargin
, multiplier: 1.0, constant: constant).isActive = true
viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])
}
}
回答8:
Personally, I prefer to keep everything self-contained within the view subclass.
Here's my Swift implementation:
import UIKit
@IBDesignable
class GradientBackdropView: UIView {
@IBInspectable var startColor: UIColor=UIColor.whiteColor()
@IBInspectable var endColor: UIColor=UIColor.whiteColor()
@IBInspectable var intermediateColor: UIColor=UIColor.whiteColor()
var gradientLayer: CAGradientLayer?
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
super.drawRect(rect)
if gradientLayer == nil {
self.addGradientLayer(rect: rect)
} else {
gradientLayer?.removeFromSuperlayer()
gradientLayer=nil
self.addGradientLayer(rect: rect)
}
}
override func layoutSubviews() {
super.layoutSubviews()
if gradientLayer == nil {
self.addGradientLayer(rect: self.bounds)
} else {
gradientLayer?.removeFromSuperlayer()
gradientLayer=nil
self.addGradientLayer(rect: self.bounds)
}
}
func addGradientLayer(rect rect:CGRect) {
gradientLayer=CAGradientLayer()
gradientLayer?.frame=self.bounds
gradientLayer?.colors=[startColor.CGColor,intermediateColor.CGColor,endColor.CGColor]
gradientLayer?.startPoint=CGPointMake(0.0, 1.0)
gradientLayer?.endPoint=CGPointMake(0.0, 0.0)
gradientLayer?.locations=[NSNumber(float: 0.1),NSNumber(float: 0.5),NSNumber(float: 1.0)]
self.layer.insertSublayer(gradientLayer!, atIndex: 0)
gradientLayer?.transform=self.layer.transform
}
}
来源:https://stackoverflow.com/questions/17555986/cagradientlayer-not-resizing-nicely-tearing-on-rotation