我有一个子类UIView,我们可以调用CircleView
。 CircleView会自动将角半径设置为其宽度的一半,以使其成为圆形。
问题是当" CircleView"通过AutoLayout约束调整大小...例如在设备旋转 ...它会严重扭曲,直到调整大小,因为" cornerRadius"属性必须赶上,操作系统只发送一个"边界"改变视图的框架。
我想知道是否有人有一个好的,明确的策略来实施" CircleView"以某种方式在这种情况下不会扭曲,但仍会将其内容掩盖为圆形,并允许在所述UIView周围存在边框。
答案 0 :(得分:38)
从iOS 11开始,如果您在动画块中更新它,UIKit将为cornerRadius
设置动画。只需在layer.cornerRadius
动画块中设置您的视图UIView
,或(以处理界面方向更改),将其设置为layoutSubviews
或viewDidLayoutSubviews
。
所以你想要这个:
(我启用了Debug> Slow Animations以使平滑更容易看到。)
咆哮,随意跳过这一段:事实证明这比应该更难,因为iOS SDK没有制作参数(持续时间,时间曲线) )以方便的方式提供自动旋转动画。您可以(我认为)通过覆盖视图控制器上的-viewWillTransitionToSize:withTransitionCoordinator:
来调用转发协调器上的-animateAlongsideTransition:completion:
,并在您传递的回调中获取transitionDuration
和{{1来自completionCurve
。然后你需要将这些信息传递给你的UIViewControllerTransitionCoordinatorContext
,它必须保存它(因为它尚未调整大小!)以及稍后当它收到CircleView
时,它可以使用它使用这些已保存的动画参数为layoutSubviews
创建CABasicAnimation
。并且当动画不动画调整大小时,不要意外创建动画...... 咆哮结束。
cornerRadius
内完全实现的方法。它现在可以运行(在iOS 9中),但我不能保证它将来一直在工作,因为它做了两个假设,理论上将来可能是错误的。
以下方法:在CircleView
中覆盖-actionForLayer:forKey:
以返回一个操作,该操作在运行时为CircleView
安装动画。
这是两个假设:
cornerRadius
和bounds.origin
获得单独的动画。 (现在这是真的,但可能未来的iOS可以使用bounds.size
的单个动画。如果没有找到bounds
动画,检查bounds
动画会很容易。)< / LI>
bounds.size
操作之前,bounds.size
动画已添加到图层。鉴于这些假设,当Core Animation请求cornerRadius
操作时,我们可以从图层中获取cornerRadius
动画,复制它,然后将副本修改为动画bounds.size
。副本具有与原始动画相同的动画参数(除非我们修改它们),因此它具有正确的持续时间和时间曲线。
这是cornerRadius
:
CircleView
请注意,视图的界限是在视图收到class CircleView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
private func updateCornerRadius() {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
之前设置的,因此我们会在更新layoutSubviews
之前设置。这就是在请求cornerRadius
动画之前安装bounds.size
动画的原因。每个属性的动画都安装在属性的设置器中。
当我们设置cornerRadius
时,Core Animation会要求我们运行cornerRadius
:
CAAction
在上面的代码中,如果我们要求 override func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
}
return super.action(for: layer, forKey: event)
}
采取行动,我们会在cornerRadius
上寻找CABasicAnimation
。如果我们找到一个,我们会复制它,将关键路径更改为bounds.size
,并将其保存在自定义cornerRadius
(类CAAction
中,我将在下面显示)。我们还保存Action
属性的当前值,因为Core Animation在更新属性之前调用cornerRadius
。
actionForLayer:forKey:
返回后,Core Animation会更新图层的actionForLayer:forKey:
属性。然后它通过发送cornerRadius
来运行操作。该操作的工作是安装适当的动画。这是runActionForKey:object:arguments:
的自定义子类,我已在CAAction
内嵌套:
CircleView
private class Action: NSObject, CAAction {
var pendingAnimation: CABasicAnimation?
var priorCornerRadius: CGFloat = 0
public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
if let layer = anObject as? CALayer, let pendingAnimation = pendingAnimation {
if pendingAnimation.isAdditive {
pendingAnimation.fromValue = priorCornerRadius - layer.cornerRadius
pendingAnimation.toValue = 0
} else {
pendingAnimation.fromValue = priorCornerRadius
pendingAnimation.toValue = layer.cornerRadius
}
layer.add(pendingAnimation, forKey: "cornerRadius")
}
}
}
} // end of CircleView
方法设置动画的runActionForKey:object:arguments:
和fromValue
属性,然后将动画添加到图层。有一个复杂的问题:UIKit使用“添加”动画,因为如果你在早期动画仍在运行的同时在属性上启动另一个动画,它们的效果会更好。所以我们的行动会检查它。
如果动画是加法的,它会将toValue
设置为新旧角半径之间的差异,并将fromValue
设置为零。由于图层的toValue
属性在动画运行时已经更新,因此在动画开始时添加cornerRadius
使其看起来像旧的角半径,并添加1}}在动画结尾处为零,使其看起来像新的圆角半径。
如果动画不是附加的(如果UIKit创建动画并不会发生,据我所知),它只是以明显的方式设置fromValue
和toValue
为方便起见,这里是整个文件:
fromValue
我的回答受到this answer by Simon的启发。
答案 1 :(得分:2)
此答案以the earlier answer rob mayoff为基础。基本上,我为我们的项目实现了它,它在iPhone(iOS 9和10)上运行得很好,但问题仍然存在于iPad(iOS 9或10)上。
调试,我发现if语句:
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
总是在iPad上失败。看起来动画在iPad上的构建方式与iPhone不同。回顾original answer by Simon,似乎之前的顺序发生了变化。所以我把两个答案结合起来给了我这样的东西:
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
let buildAction: (CABasicAnimation) -> Action = { boundsAnimation in
let animation = boundsAnimation.copy() as! CABasicAnimation
animation.keyPath = "cornerRadius"
let action = Action()
action.pendingAnimation = animation
action.priorCornerRadius = layer.cornerRadius
return action
}
if event == "cornerRadius" {
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
return buildAction(boundsAnimation)
} else if let boundsAnimation = self.action(for: layer, forKey: "bounds") as? CABasicAnimation {
return buildAction(boundsAnimation)
}
}
return super.action(for: layer, forKey: event)
}
通过结合这两个答案,它似乎在iOS 9和iOS下的iPhone和iPad上都能正常工作。我还没有进一步测试过,并且对CoreAnimation不太了解,无法完全理解这一变化。
答案 2 :(得分:1)
在iOS 10中,您不需要创建CAAction,它只需创建一个CABasicAnimation并在您的操作中提供此功能(对于图层:,对于密钥:) - &gt; CAAction? function(参见Swift示例):
private var currentBoundsAnimation: CABasicAnimation? {
return layer.animation(forKey: "bounds.size") as? CABasicAnimation ?? layer.animation(forKey: "bounds") as? CABasicAnimation
}
override public var bounds: CGRect {
didSet {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
}
override public func action(for layer: CALayer, forKey event: String) -> CAAction? {
if(event == "cornerRadius"), let boundsAnimation = currentBoundsAnimation {
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.duration = boundsAnimation.duration
animation.timingFunction = boundsAnimation.timingFunction
return animation
}
return super.action(for: layer, forKey: event)
}
您也可以覆盖layoutSubviews:
,而不是覆盖bounds属性override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
这很神奇,因为CABasicAnimation从模型和表示层中推断出缺少的值和来自的值。要正确设置时序,您需要私有的currentBoundsAnimation属性来获取当前动画(&#34;界限&#34;对于iPad和&#34; bounds.size&#34;对于iPhone),这是在设备轮换时添加的。
答案 3 :(得分:0)
这些翻译答案通常是Objective-c ==&gt;斯威夫特,但是如果有更多顽固的Objective-c作者离开,这里的@ Rob的答案已被翻译......
// see https://stackoverflow.com/a/35714554/294949
#import "RoundView.h"
@interface Action : NSObject<CAAction>
@property(strong,nonatomic) CABasicAnimation *pendingAnimation;
@property(assign,nonatomic) CGFloat priorCornerRadius;
@end
@implementation Action
- (void)runActionForKey:(NSString *)event object:(id)anObject
arguments:(nullable NSDictionary *)dict {
if ([anObject isKindOfClass:[CALayer self]]) {
CALayer *layer = (CALayer *)anObject;
if (self.pendingAnimation.isAdditive) {
self.pendingAnimation.fromValue = @(self.priorCornerRadius - layer.cornerRadius);
self.pendingAnimation.toValue = @(0);
} else {
self.pendingAnimation.fromValue = @(self.priorCornerRadius);
self.pendingAnimation.toValue = @(layer.cornerRadius);
}
[layer addAnimation:self.pendingAnimation forKey:@"cornerRadius"];
}
}
@end
@interface RoundView ()
@property(weak,nonatomic) UIImageView *imageView;
@end
@implementation RoundView
- (void)layoutSubviews {
[super layoutSubviews];
[self updateCornerRadius];
}
- (void)updateCornerRadius {
self.layer.cornerRadius = MIN(self.bounds.size.width, self.bounds.size.height)/2.0;
}
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if ([event isEqualToString:@"cornerRadius"]) {
CABasicAnimation *boundsAnimation = (CABasicAnimation *)[self.layer animationForKey:@"bounds.size"];
CABasicAnimation *animation = [boundsAnimation copy];
animation.keyPath = @"cornerRadius";
Action *action = [[Action alloc] init];
action.pendingAnimation = animation;
action.priorCornerRadius = layer.cornerRadius;
return action;
}
return [super actionForLayer:layer forKey:event];;
}
@end
答案 4 :(得分:-1)
我建议不使用角半径,而是使用CAShapeLayer作为视图内容层的掩码。
您将填充360°圆弧CGPath作为形状图层的形状,并将其设置为图层视图的蒙版。
然后,您可以为遮罩层设置新的缩放变换动画,或者将更改设置为路径半径的动画。两种方法都应保持圆形,尽管缩放变换可能无法在较小像素尺寸下为您提供干净的形状。
时机将是棘手的部分(使用边界动画使掩模层的动画与锁步发生。)