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