【问题标题】:CAGradientLayer, not resizing nicely, tearing on rotationCAGradientLayer,不能很好地调整大小,旋转时撕裂
【发布时间】:2013-07-07 12:25:36
【问题描述】:

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

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

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

Video of Problem...

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

Xcode Project, modally presenting views with CAGradientLayer backgrounds...

对于我理解使用的价值:

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

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

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

约翰

【问题讨论】:

    标签: iphone ios objective-c uiviewcontroller cagradientlayer


    【解决方案1】:

    当您创建一个图层(如您的渐变图层)时,没有视图管理该图层(即使您将其添加为某个视图图层的子图层)。像这样的独立层不参与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
    

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

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

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

    结果如下:

    【讨论】:

    • 多么好的答案。太棒了谢谢。 - 2 件快速的事情,1. 什么是 uiview 的层?每个视图都有一个图层吗?它是否像被绘制到的视图的实际组件? 2.在delegate方法中willAnimateRotationToInterfaceOrientation:duration可以设置duration吗?或者它只是由系统设置并且可以用于杆?
    • 每个视图都有自己的专用层。您可以创建其他图层,就像您对渐变图层所做的那样。阅读“View Architecture Fundamentals” in the View Programming Guide for iOS。您无法更改自转动画的持续时间。
    • 太好了,感谢所有答案,那么持续时间何时作为输入参数传递?
    • 我可以建议编辑这个答案以与 Swift 一起使用吗?这是一个很棒的答案,应该以两种语言提供以供将来参考。
    • 您也可以通过直接在添加渐变的 UIView 上设置 layers.colors 属性来实现相同的目的。这节省了对子层的需要。只需将情节提要中的类属性设置为GradientView.h,然后调用[(CAGradientLayer *)[[self myGradientView] layer] setColors:...(但可能不适用于self.view
    【解决方案2】:

    关于@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)
    }
    

    【讨论】:

    • 我没有使用-theLayer.locations = [0.0, 1.0],而是像这样使用了起点和终点-theLayer.startPoint = CGPoint(x: 0.5, y: 0.0); theLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
    • 先生,你有我的尊重,我处理了大约 2 天,我终于找到了你
    • 这很好,但是,我在主视图中使用一个视图,并且我正在以编程方式执行它并使用约束。我只是无法在该子视图中获得渐变。它在masterview上效果很好。任何帮助将不胜感激
    • @user3194306 您可能需要提供代码,以便我们更好地了解到底发生了什么。有可能是您使用约束的方式。
    【解决方案3】:

    我的快速版本:

    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”方法中创建的,因为我们需要一个最终帧来创建渐变层。

    【讨论】:

    • 按照这个答案中的代码,旋转有一个非常明显的动画。相反,您应该让视图通过覆盖视图的图层来处理您的渐变,而不是插入您自己的子图层。
    • 这会在每个 viewDidLayoutSubviews 调用上添加一个新的渐变层,这可能会导致内存问题。
    • 在将文本添加到弹出视图时,我很难让我的渐变视图具有正确的大小。只需使用 viewDidLayoutSubviews() 而不是 viewDidLoad() 来设置渐变就解决了我的问题!不需要子类。但是为了在旋转设备时获得正确的渐变,我必须使用子视图。我最终使用了没有 Rect 的 Dan 的解决方案。
    • 如果你要插入子层,那么子类化到 GradientView 是没有意义的:/
    【解决方案4】:

    插入这段代码并删除willAnimateRotationToInterfaceOrientation:duration:实现会更好看。

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

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

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

    同时实现layoutSubviews 来处理视图边界的变化。

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

    【讨论】:

    • 那里有有趣的东西......那么AnimateRotationToInterfaceOrientation:duration -- 什么时候应该使用它? viewWillLayoutSubviews 不是在旋转后调用,因此不会为旋转设置动画吗?
    • viewWillLayoutSubviews 在旋转动画块内被调用。不幸的是,独立层不参与UIView 动画系统,所以这并不能解决问题。
    【解决方案5】:

    完整的 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]
        }
    }
    

    【讨论】:

      【解决方案6】:

      另一个 swift 版本 - 不使用 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()])
      

      【讨论】:

      • 这也适用于 tableView 背景和屏幕旋转:让 gradientView: gradientView = gradientView(), gradientView.setGradientBackground(colors: [UIColor.white, UIColor.black]), tableView.backgroundView =渐变视图
      【解决方案7】:

      信息

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

      详情

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

      使用故事板

      视图控制器

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

      【讨论】:

        【解决方案8】:

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

        这是我的 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
                        }
                    }
        

        【讨论】:

          【解决方案9】:

          您可以在故事板、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
              }
          }
          

          【讨论】:

            【解决方案10】:

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

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

            【讨论】:

              猜你喜欢
              • 2014-12-18
              • 1970-01-01
              • 2018-10-30
              • 2013-03-12
              • 1970-01-01
              • 2015-08-25
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多