CAGradientLayer, не сильно изменяя размер, разрывая на вращение

Я пытаюсь получить мои CAGradientLayers, которые я использую для создания хороших градиентных фонов, для удобного изменения размера при ротации и просмотра модального представления, но они не будут играть в мяч.

Вот видео, которое я только что создал, показывая свою проблему: обратите внимание на разрывание при вращении.

Также обратите внимание, что это видео было создано, снимая iPhone Simulator на OS X. Я замедлил анимацию в видео, чтобы выделить мою проблему.

Видео проблемы...

Вот проект Xcode, который я только что создал (который является источником для приложения, показанного на видео), в основном, как показано на рисунке, проблема возникает при вращении, и особенно когда представления представлены модально:

Xcode Project, модально представляя представления с помощью фона CAGradientLayer...

Для чего это стоит, я понимаю, что используя:

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

делает разумную работу по созданию переходов немного более плавным и менее резким, но если вы посмотрите на видео, когда я, пока в настоящее время в ландшафтном режиме, модно представляю представление, вы увидите, почему приведенный выше код не поможет.

Любые идеи, что я могу сделать, чтобы разобраться в этом?

Джон

Ответ 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

Вот что это похоже, с замедленным движением, включенным в симуляторе:

неправильное вращение

К счастью, это простая проблема для исправления. Вам нужно, чтобы ваш уровень градиента управлялся с помощью представления. Вы делаете это, создавая подкласс 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. Поскольку теперь вы используете представление вместо слоя, вы можете установить маску авторезистировки, чтобы сохранить градиент размером до его супервизора, поэтому вам не нужно делать что-либо позже для обработки вращения:

@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

Лучшая часть ответа @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)
}

Ответ 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)
    }
}

Обратите внимание, что мне также приходилось использовать шкалу устройств для вычисления размера кадра - чтобы получить правильную автоматическую калибровку во время изменений ориентации (с автоматической компоновкой).

  • В интерфейсе Builder я добавил UIView и изменил его класс на GradientView (класс, показанный выше).
  • Затем я создал для него выход (myGradientView).
  • Наконец, в контроллере представления я добавил:

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

Обратите внимание, что представление градиента создается в методе layoutSubviews, так как нам нужен финализированный кадр для создания слоя градиента.

Ответ 4

Это будет выглядеть лучше, если вы вставляете этот фрагмент кода и удаляете реализацию willAnimateRotationToInterfaceOrientation:duration:.

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

Это, однако, не очень элегантно. В реальном приложении вы должны подклассифицировать UIView для создания градиентного представления. В этом пользовательском представлении вы можете переопределить layerClass, чтобы он поддерживался слоем градиента:

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

Также используйте layoutSubviews для обработки при изменении границ вида.

При создании этого фонового представления используйте автоматическую маскировку, чтобы границы автоматически настраивались на вращение интерфейса.

Ответ 5

Полная версия 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]
    }
}

Ответ 6

Лично я предпочитаю сохранять все автономные в подклассе вида.

Здесь моя быстрая реализация:

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

Ответ 7

Еще одна быстрая версия - которая не использует 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()])

Ответ 8

Информация

  • Использовать как однострочное решение
  • Замена градиента, когда вы снова добавляете его в представление (для использования в повторном использовании)
  • Автоматический переход
  • Автоматическое удаление

Подробнее

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

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>

Программный

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