webentwicklung-frage-antwort-db.com.de

CAGradientLayer, nicht schön in der Größe, zerreißt bei Rotation

Ich versuche, meine CAGradientLayers zu erhalten, die ich verwende, um schöne Verlaufshintergründe zu erstellen, die Größe der Rotation und der modalen Ansicht zu ändern, aber sie spielen keinen Ball.

Hier ist ein Video, das ich gerade erstellt habe, um mein Problem zu zeigen: Beachten Sie das Zerreißen beim Drehen.

Bitte beachten Sie auch, dass dieses Video durch das Filmen des iPhone Simulators unter OS X erstellt wurde. Ich habe die Animationen im Video verlangsamt, um mein Problem hervorzuheben.

Video zum Problem ...

Hier ist ein Xcode-Projekt, das ich gerade erstellt habe (was die Quelle für die im Video gezeigte App ist). Grundsätzlich tritt das Problem bei der Rotation auf, insbesondere wenn Ansichten modal dargestellt werden:

Xcode-Projekt, modale Ansichten mit CAGradientLayer-Hintergründen ...

Für das, was es wert ist, verstehe ich, dass

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

es macht eine vernünftige Arbeit, die Übergänge etwas nahtloser und weniger störend zu gestalten, aber wenn Sie sich das Video ansehen, während ich im Querformatmodus eine Ansicht moduliere, werden Sie sehen, warum der obige Code nicht hilft.

Irgendwelche Ideen, was ich dagegen tun kann?

John

50
Woodstock

Wenn Sie eine Ebene erstellen (z. B. eine Verlaufsebene), gibt es keine Ansicht, die die Ebene verwaltet (selbst wenn Sie sie als Unterebene einer Ansichtsebene einer Ansicht hinzufügen). Eine solche eigenständige Ebene nimmt nicht am Animationssystem UIView teil.

Wenn Sie also den Frame der Verlaufsebene aktualisieren, animiert die Ebene die Änderung mit ihren eigenen Standardanimationsparametern. (Dies wird als "implizite Animation" bezeichnet.) Diese Standardparameter stimmen nicht mit den für die Rotation der Benutzeroberfläche verwendeten Animationsparametern überein, sodass Sie ein seltsames Ergebnis erhalten.

Ich habe mir Ihr Projekt nicht angesehen, aber es ist einfach, Ihr Problem mit diesem Code zu reproduzieren:

@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

So sieht das aus, wenn die Zeitlupe im Simulator aktiviert ist:

 bad rotation

Glücklicherweise ist dieses Problem leicht zu beheben. Sie müssen dafür sorgen, dass Ihre Verlaufsebene von einer Ansicht verwaltet wird. Dazu erstellen Sie eine UIView-Unterklasse, die eine CAGradientLayer-Ebene als Layer verwendet. Der Code ist winzig:

// GradientView.h

@interface GradientView : UIView

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

@end

// GradientView.m

@implementation GradientView

@dynamic layer;

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

@end

Dann müssen Sie Ihren Code ändern, um GradientView anstelle von CAGradientLayer zu verwenden. Da Sie jetzt eine Ansicht anstelle einer Ebene verwenden, können Sie die Maske für die automatische Größenänderung so einstellen, dass der Farbverlauf auf die Superviewansicht beschränkt bleibt, sodass Sie später keine weiteren Schritte für die Drehung vornehmen müssen:

@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

Hier ist das Ergebnis:

 good rotation

151
rob mayoff

Das Beste an der Antwort von @ rob ist, dass die Ansicht die Ebene für Sie steuert. Hier ist der Swift-Code, der die Layer-Klasse ordnungsgemäß überschreibt und den Verlauf festlegt.

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

Sie können die Ansicht dann in zwei Zeilen hinzufügen, wo immer Sie möchten.

override func viewDidLoad() {
    super.viewDidLoad()

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

Meine Swift-Version:

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

Beachten Sie, dass ich auch die Maßstabsgröße des Geräts verwenden musste, um die Framegröße zu berechnen, um bei Ausrichtungsänderungen (mit automatischem Layout) eine korrekte automatische Größenanpassung zu erhalten.

  1. Im Interface Builder habe ich ein UIView hinzugefügt und seine Klasse in GradientView (die oben gezeigte Klasse) geändert.
  2. Ich habe dann einen Outlet dafür erstellt (myGradientView).
  3. Zum Schluss habe ich im View Controller hinzugefügt:

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

Beachten Sie, dass die Verlaufsansicht in einer "layoutSubviews" -Methode erstellt wird, da zum Erstellen der Verlaufsebene ein fertiggestellter Rahmen benötigt wird.

6
GK100

Es wird besser aussehen, wenn Sie diesen Code einfügen und die willAnimateRotationToInterfaceOrientation:duration:-Implementierung entfernen.

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

Dies ist jedoch nicht sehr elegant. In einer realen Anwendung sollten Sie die UIView-Klasse unterteilen, um eine Verlaufsansicht zu erstellen. In dieser benutzerdefinierten Ansicht können Sie layerClass überschreiben, sodass sie von einer Verlaufsebene unterstützt wird:

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

Implementieren Sie auch layoutSubviews für die Verarbeitung, wenn sich die Begrenzungen der Ansicht ändern.

Verwenden Sie beim Erstellen dieser Hintergrundansicht Masken mit automatischer Größenänderung, damit die Begrenzungen automatisch an Rotationen der Schnittstelle angepasst werden.

3
Felix

Komplette Swift-Version. Setze viewFrame aus dem viewController, dem diese Ansicht gehört, in 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

Eine andere Swift-Version - die nicht drawRect verwendet.

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

Im Controller rufe ich einfach an:

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

Ich persönlich ziehe es vor, alles innerhalb der Ansichtsunterklasse in sich zu halten.

Hier ist meine schnelle Implementierung:

            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

  • Als einzeilige Lösung verwenden
  • Ersetzen des Farbverlaufs, wenn Sie ihn erneut der Ansicht hinzufügen (zur Verwendung in wiederverwendbaren Elementen)
  • Automatisches Transitieren
  • Automatisch entfernen

Einzelheiten

Swift 3.1, xCode 8.3.3

Lösung

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

Verwendungszweck

viewWithGradient.addGradient(colors: [.blue, .green, .orange], locations: [0.1, 0.3, 1.0])

StoryBoard verwenden

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>

Programmatisch

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