web-dev-qa-db-fra.com

CAGradientLayer, ne pas redimensionner correctement, se déchire en rotation

J'essaie d'obtenir mes CAGradientLayers, que j'utilise pour créer de jolis arrière-plans dégradés, pour redimensionner correctement la rotation et la présentation en mode modal, mais ils ne joueront pas la balle.

Voici une vidéo que je viens de créer et montrant mon problème: Remarquez la déchirure en rotation.

Veuillez également noter que cette vidéo a été créée en filmant le simulateur iPhone sur OS X. J'ai ralenti les animations de la vidéo pour mettre en évidence mon problème.

Vidéo du problème ...

Voici un projet Xcode que je viens de créer (qui est la source de l'application présentée dans la vidéo). En gros, comme illustré, le problème se produit lors de la rotation, en particulier lorsque les vues sont présentées sous forme modale:

Xcode Project, présentation modale de vues avec des arrière-plans CAGradientLayer ...

Pour ce que ça vaut, je comprends que l’utilisation de:

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

fait un travail raisonnable en rendant les transitions un peu plus transparentes et moins saccadées, mais si vous regardez la vidéo alors que, alors que je suis actuellement en mode paysage, présente une vue de façon modale, vous verrez pourquoi le code ci-dessus ne vous aidera pas.

Des idées que je peux faire pour résoudre ce problème?

John

50
Woodstock

Lorsque vous créez un calque (comme votre calque de dégradé), aucune vue ne le gère (même lorsque vous l'ajoutez en tant que sous-calque d'un calque de vue). Une couche autonome comme celle-ci ne participe pas au système d'animation UIView.

Ainsi, lorsque vous mettez à jour l'image du calque de dégradé, celui-ci anime la modification avec ses propres paramètres d'animation par défaut. (Ceci s'appelle «animation implicite».) Ces paramètres par défaut ne correspondent pas aux paramètres d'animation utilisés pour la rotation de l'interface. Vous obtenez donc un résultat étrange.

Je n'ai pas regardé votre projet mais il est trivial de reproduire votre problème avec ce 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

Voici à quoi cela ressemble, avec le ralenti activé dans le simulateur:

 bad rotation

Heureusement, il s'agit d'un problème facile à résoudre. Vous devez faire en sorte que votre couche de dégradé soit gérée par une vue. Pour ce faire, vous créez une sous-classe UIView qui utilise CAGradientLayer comme couche. Le code est minuscule:

// GradientView.h

@interface GradientView : UIView

@property (nonatomic, strong, readonly) CAGradientLayer *layer;

@end

// GradientView.m

@implementation GradientView

@dynamic layer;

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

@end

Ensuite, vous devez modifier votre code pour utiliser GradientView au lieu de CAGradientLayer. Étant donné que vous utilisez maintenant une vue au lieu d'un calque, vous pouvez définir le masque de redimensionnement automatique pour conserver la taille du dégradé à son aperçu, afin que vous n'ayez rien à faire ultérieurement pour gérer la 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

Voici le résultat:

 good rotation

151
rob mayoff

La meilleure partie de la réponse de @ rob est que la vue contrôle la couche pour vous. Voici le code Swift qui remplace correctement la classe de calque et définit le dégradé.

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

Vous pouvez ensuite ajouter la vue sur deux lignes où vous le souhaitez.

override func viewDidLoad() {
    super.viewDidLoad()

    let gradientView = GradientView(frame: self.view.bounds)
    self.view.insertSubview(gradientView, atIndex: 0)
}
21
Kevin Marlow

Ma version de Swift:

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

Notez que je devais également utiliser la balance du périphérique pour calculer la taille du cadre - pour obtenir un redimensionnement automatique correct lors des changements d'orientation (avec la mise en page automatique).

  1. Dans Interface Builder, j'ai ajouté un UIView et changé sa classe en GradientView (la classe indiquée ci-dessus).
  2. J'ai ensuite créé un point de vente pour cela (myGradientView).
  3. Enfin, dans le contrôleur de vue, j'ai ajouté:

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

Notez que la vue en dégradé est créée dans une méthode "layoutSubviews", car nous avons besoin d'une image finalisée pour créer le calque en dégradé.

6
GK100

Cela paraîtra mieux lorsque vous insérez cette partie de code et supprimez l'implémentation willAnimateRotationToInterfaceOrientation:duration:.

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

Ce n'est cependant pas très élégant. Dans une application réelle, vous devez créer une vue en dégradé dans la sous-classe UIView. Dans cet affichage personnalisé, vous pouvez remplacer layerClass afin qu’il soit soutenu par un calque de dégradé:

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

Implémentez également layoutSubviews pour gérer le changement de limites de la vue.

Lors de la création de cette vue en arrière-plan, utilisez des masques à redimensionnement automatique afin que les limites soient automatiquement ajustées en fonction des rotations d'interface.

3
Felix

Version complète de Swift. Définissez viewFrame à partir du viewController qui possède cette vue dans 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]
    }
}
2
smileBot

Une autre version de Swift - qui n’utilise pas 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 })
    }
}

Dans le contrôleur, je viens d'appeler:

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

Personnellement, je préfère que tout soit autonome dans la sous-classe view.

Voici ma mise en œuvre 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
                }
            }
0

Info

  • Utiliser comme solution en une seule ligne
  • Remplacement du dégradé lorsque vous l'ajoutez à nouveau à la vue (à utiliser dans les réutilisables)
  • En transit automatique
  • Enlèvement automatique

Détails

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

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

Par programme

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])
    }
}
0
Vasily Bodnarchuk