CAGradientLayer,不好调整,撕裂旋转

我试图让我的CAGradientLayers,我用来创build漂亮的渐变背景,很好地调整旋转和模态视图呈现,但他们不会玩球。

这里是我刚刚创build的video,显示我的问题:注意旋转的撕裂。

另外请注意,这个video是通过在OS X上拍摄iPhone模拟器创build的。我放慢了video中的animation以突出显示我的问题。

问题的video…

这里是我刚刚创build的Xcode项目(这是video中显示的应用程序的源代码),基本上如图所示,问题发生在旋转,特别是当视图模式地呈现时:

Xcode项目,模式呈现与CAGradientLayer背景的意见…

为什么这是值得我明白,使用:

[[self view] setBackgroundColor:[UIColor blackColor]]; 

做一个合理的工作,让过渡更无缝,更不刺激,但是如果你看着video,而我目前处于横向模式,模态地呈现一个视图,你会明白为什么上面的代码不会帮助。

任何想法我可以做什么来解决这个问题?

约翰

当你创build一个图层(比如你的渐变图层)时,没有pipe理图层的视图(即使你将它添加为某个视图图层的子图层)。 像这样的独立层不参与UIViewanimation系统。

因此,当您更新渐变图层的框架时,图层将使用其自己的默认animation参数来animation更改。 (这被称为“隐式animation”。)这些默认参数与用于界面旋转的animation参数不匹配,所以会得到一个奇怪的结果。

我没有看你的项目,但用这个代码重现你的问题是微不足道的:

 @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 

以下是在模拟器中启用慢动作的情况:

旋转不好

幸运的是,这是一个容易解决的问题。 您需要使您的渐变图层由视图pipe理。 通过创build一个使用CAGradientLayer作为其图层的UIView子类来实现这CAGradientLayer 。 代码很小:

 // GradientView.h @interface GradientView : UIView @property (nonatomic, strong, readonly) CAGradientLayer *layer; @end // GradientView.m @implementation GradientView @dynamic layer; + (Class)layerClass { return [CAGradientLayer class]; } @end 

然后,您需要更改您的代码以使用GradientView而不是CAGradientLayer 。 由于您现在使用的是视图而不是图层,因此您可以设置自动resize的掩码,以便将渐变大小保持为超级视图的大小,因此,稍后您无需执行任何操作即可处理旋转:

 @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 

结果如下:

良好的旋转

@ rob的答案最好的部分是视图控制你的图层。 这里是正确覆盖图层类并设置渐变的Swift代码。

 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 func layerClass() -> AnyClass { return CAGradientLayer.self } } 

然后,您可以将视图添加到任意位置的两行中。

 override func viewDidLoad() { super.viewDidLoad() let gradientView = GradientView(frame: self.view.bounds) self.view.insertSubview(gradientView, atIndex: 0) } 

我的迅捷版本:

 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) } } 

请注意,我还必须使用设备比例来计算框架尺寸 – 在方向更改期间(使用自动布局)获得正确的自动resize。

  1. 在Interface Builder中,我添加了一个UIView,并将其类更改为GradientView(上面显示的类)。
  2. 然后我创build了一个出口(myGradientView)。
  3. 最后,在视图控制器中我添加了:

     override func viewDidLayoutSubviews() { self.myGradientView.gradientWithColors(UIColor.whiteColor(), UIColor.blueColor()) } 

请注意,渐变视图是在“layoutSubviews”方法中创build的,因为我们需要最终化的框架来创build渐变图层。

当你插入这段代码并删除willAnimateRotationToInterfaceOrientation:duration: implementation时,它会更好看。

 - (void)viewWillLayoutSubviews { [[[self.view.layer sublayers] objectAtIndex:0] setFrame:self.view.bounds]; } 

然而这不是很优雅。 在一个真正的应用程序中,您应该inheritanceUIView以创build一个渐变视图。 在这个自定义视图中,您可以覆盖layerClass,使其由渐变图层支持:

 + (Class)layerClass { return [CAGradientLayer class]; } 

还要实现layoutSubviews来处理视图边界的变化。

创build此背景视图时,使用自动调整遮罩,以便界面旋转时自动调整边界。

完整的Swift版本。 在viewDidLayoutSubviews ,从拥有该视图的viewController中设置viewFrame

 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] } } 

就个人而言,我宁愿将所有的东西都包含在视图子类中。

这是我的Swift实现:

  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 } } 

另一个快速版本 – 这是不使用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 }) } } 

在控制器我只是打电话给:

 gradientView.setGradientBackground([UIColor.grayColor(), UIColor.whiteColor()]) 

信息

  • 用作一条线路解决scheme
  • 将梯度再次添加到视图中时replace梯度(用于可重复使用)
  • 自动转移
  • 自动删除

细节

Swift 3.1,xCode 8.3.3

 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() } } 

用法

 viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0]) 

使用StoryBoard

视图控制器

 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]) } } 

故事板

 <?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> 

编程

 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]) } }