69

我正在尝试获取我的 CAGradientLayers,我用它来创建漂亮的渐变背景,以便在旋转和模态视图演示时很好地调整大小,但它们不会发挥作用。

这是我刚刚创建的一个视频,显示了我的问题:注意旋转时的撕裂。

另请注意,此视频是通过在 OS X 上拍摄 iPhone 模拟器创建的。我放慢了视频中的动画以突出我的问题。

问题视频...

这是我刚刚创建的一个 Xcode 项目(它是视频中显示的应用程序的源代码),基本上如图所示,问题发生在旋转时,尤其是当视图以模态呈现时:

Xcode 项目,以 CAGradientLayer 背景模态呈现视图...

值得我理解的是,使用:

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

在使过渡更加无缝和不那么刺耳方面做得很合理,但是如果您在我当前处于横向模式时以模态方式呈现视图时观看视频,您会明白为什么上面的代码无济于事。

有什么想法可以解决这个问题吗?

约翰

4

10 回答 10

183

当您创建一个图层(如您的渐变图层)时,没有视图管理该图层(即使您将其添加为某个视图图层的子图层)。像这样的独立层不参与UIView动画系统。

因此,当您更新渐变图层的帧时,图层会使用自己的默认动画参数对更改进行动画处理。(这称为“隐式动画”。)这些默认参数与用于界面旋转的动画参数不匹配,因此您会得到一个奇怪的结果。

我没有查看您的项目,但是使用此代码重现您的问题很简单:

@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

这是在模拟器中启用慢动作的样子:

旋转不良

幸运的是,这是一个很容易解决的问题。您需要使渐变层由视图管理。您可以通过创建一个UIView使用 aCAGradientLayer作为其层的子类来做到这一点。代码很小:

// 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. 由于您现在使用的是视图而不是图层,因此您可以设置自动调整蒙版以将渐变大小保持为其父视图,因此您以后无需执行任何操作来处理旋转:

@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

结果如下:

良好的旋转

于 2013-07-09T21:38:01.723 回答
46

关于@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 var layerClass: AnyClass {
        return CAGradientLayer.self
    }
}

然后,您可以在任意位置添加两行视图。

override func viewDidLoad() {
    super.viewDidLoad()

    let gradientView = GradientView(frame: self.view.bounds)
    self.view.insertSubview(gradientView, atIndex: 0)
}
于 2016-01-14T11:41:05.353 回答
7

我的快速版本:

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

请注意,我还必须使用设备比例来计算帧大小 - 在方向更改期间获得正确的自动调整大小(使用自动布局)。

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

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

请注意,渐变视图是在“layoutSubviews”方法中创建的,因为我们需要一个最终帧来创建渐变层。

于 2015-03-16T05:16:55.820 回答
3

当您插入这段代码并删除willAnimateRotationToInterfaceOrientation:duration:实现时,它会看起来更好。

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

然而,这不是很优雅。在实际应用程序中,您应该继承 UIView 以创建渐变视图。在这个自定义视图中,您可以覆盖 layerClass 以便它由渐变层支持:

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

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

创建此背景视图时,请使用自动调整大小的蒙版,以便边界自动调整界面旋转。

于 2013-07-09T19:37:26.073 回答
2

完整的 Swift 版本。viewFrame从拥有此视图的 viewController 中设置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]
    }
}
于 2016-09-01T18:00:24.423 回答
1

信息

  • 用作一条线解决方案
  • 再次将渐变添加到视图时替换渐变(用于可重用)
  • 自动中转
  • 自动删除

细节

斯威夫特 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])

使用故事板

视图控制器

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])
    }
}
于 2017-07-07T10:47:47.150 回答
1

另一个快速版本 - 不使用 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()])
于 2016-04-10T18:15:51.753 回答
0

您可以从情节提要、xib 或代码中使用它。您可以稍后动态更改颜色(我的情况需要它)

在此处添加完整的可复制粘贴:

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

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

extension GradientView {
    func setVerticalGradientBackground(colors: [CGColor], locations: [CGFloat] = [0, 1]) {
        setGradientBackground(colors: colors, locations: locations, startPoint: .init(x: 0.5, y: 0), endPoint: .init(x: 0.5, y: 1))
    }

    func setHorizontalGradientBackground(colors: [CGColor], locations: [CGFloat] = [0, 1]) {
        setGradientBackground(colors: colors, locations: locations, startPoint: .init(x: 0, y: 0.5), endPoint: .init(x: 1, y: 0.5))
    }

    func setGradientBackground(colors: [CGColor],
                               locations: [CGFloat],
                               startPoint: CGPoint,
                               endPoint: CGPoint) {

        guard let gradientLayer = self.layer as? CAGradientLayer else {
            return
        }

        gradientLayer.colors = colors
        gradientLayer.locations = locations.map { $0 as NSNumber }
        gradientLayer.startPoint = startPoint
        gradientLayer.endPoint = endPoint
        gradientLayer.frame = bounds
    }
}
于 2021-02-04T09:37:04.643 回答
0

简单的方法。每次视图更改大小时,您都可以添加渐变层:

class YourVC: UIViewController {
...
override func viewDidLoad() {
    super.viewDidLoad()
    yourView.addObserver(self, forKeyPath: "bounds", options: [], context: nil)
}
...
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if (object as? NSObject == yourView && keyPath == "bounds") {
        //remove and add again your gradient layer
    }
}
...
于 2021-12-09T09:41:01.300 回答
0

就个人而言,我更喜欢将所有内容都保留在视图子类中。

这是我的 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
                }
            }
于 2016-03-19T07:28:10.567 回答