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.
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
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:
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:
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)
}
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).
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é.
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.
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]
}
}
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()])
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
}
}
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])
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])
}
}