Objective c 在UITableviewCell高度设置动画的同时设置CALayer阴影的动画
我有一个UITableView,我正试图使用它的Objective c 在UITableviewCell高度设置动画的同时设置CALayer阴影的动画,objective-c,uitableview,core-animation,calayer,Objective C,Uitableview,Core Animation,Calayer,我有一个UITableView,我正试图使用它的beginUpdate和endUpdates方法展开和折叠它,并在发生时显示一个下拉阴影。在自定义UITableViewCell中,我有一个层,我在布局子视图中为其创建阴影: self.shadowLayer.shadowColor = self.shadowColor.CGColor; self.shadowLayer.shadowOffset = CGSizeMake(self.shadowOffsetWidth, self.shadowOff
beginUpdate
和endUpdates
方法展开和折叠它,并在发生时显示一个下拉阴影。在自定义UITableViewCell中,我有一个层,我在布局子视图中为其创建阴影:
self.shadowLayer.shadowColor = self.shadowColor.CGColor;
self.shadowLayer.shadowOffset = CGSizeMake(self.shadowOffsetWidth, self.shadowOffsetHeight);
self.shadowLayer.shadowOpacity = self.shadowOpacity;
self.shadowLayer.masksToBounds = NO;
self.shadowLayer.frame = self.layer.bounds;
// this is extremely important for performance when drawing shadows
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:self.shadowLayer.frame cornerRadius:self.cornerRadius];
self.shadowLayer.shadowPath = shadowPath.CGPath;
我在viewDidLoad
中将此层添加到UITableViewCell中:
self.shadowLayer = [CALayer layer];
self.shadowLayer.backgroundColor = [UIColor whiteColor].CGColor;
[self.layer insertSublayer:self.shadowLayer below:self.contentView.layer];
据我所知,当我调用BeginUpdate
时,如果当前运行循环不存在,则会为当前运行循环生成一个隐式CALayerTransaction
。此外,还将调用layoutSubviews
。这里的问题是,根据UITableViewCell的新大小立即绘制生成的阴影。我真的需要阴影来继续以预期的方式投射,因为实际层正在设置动画
由于我创建的层不是背景层,因此它在没有明确指定CATTransaction(这是预期的)的情况下设置动画。但是,据我所知,我确实需要一些方法来掌握beginUpdates
/endUpdates
CATransaction,并在其中执行动画。如果有,我该怎么做?UITableView
可能不会创建CATransaction
,或者如果是,它正在等待,直到您结束更新。我的理解是,表视图只是合并了这些函数之间的所有更改,然后根据需要创建动画。您无法获得它正在提交的实际动画参数的控制柄,因为我们不知道实际发生的时间。在UIScrollView
中设置内容偏移更改动画时也会发生同样的情况:系统没有提供有关动画本身的上下文,这令人沮丧。也无法查询系统当前的CATransaction
s
您最好检查UITableView
正在创建的动画,并在自己的动画中模拟相同的计时参数。在CALayer
上滑动add(uquo:forKey:)
可以让您检查正在添加的所有动画。您当然不希望实际附带此功能,但我经常在调试中使用此技术来确定添加了哪些动画及其属性
我怀疑您将不得不在tableView(\ux0:willDisplayCell:for:row:)
中提交您自己的阴影层动画,以获得适当的单元格。因此,我猜您有这样的情况:
(我在模拟器中打开了“调试>慢速动画”),你不喜欢阴影跳到新大小的方式。您希望这样做:
你可以找到我的测试项目
拾取动画参数并在表视图的动画块中添加动画很棘手,但并非不可能。最棘手的部分是,您需要更新阴影视图本身或阴影视图的直接超级视图的layoutSubviews
方法中的shadowPath
。在我的演示视频中,这意味着阴影路径
需要通过绿框视图或绿框的即时超级视图的布局子视图
方法进行更新
我选择创建一个ShadowingView
类,其唯一任务是绘制其子视图之一的阴影并设置其动画。以下是界面:
@interface ShadowingView : UIView
@property (nonatomic, strong) IBOutlet UIView *shadowedView;
@end
@interface ShadowingViewAction : NSObject <CAAction>
@property (nonatomic, strong) CABasicAnimation *pendingAnimation;
@property (nonatomic) CGPathRef priorPath;
@end
为了使用阴影视图
,我将其添加到我故事板中的单元格视图中。实际上,它嵌套在单元格内的堆栈视图中。然后我添加了绿色框作为ShadowingView
的子视图,并将shadowedView
插座连接到绿色框
ShadowingView
实现包括三个部分。一种是它的layoutSubviews
方法,它在自己的图层上设置图层阴影属性,以便在其shadowedView
子视图周围绘制阴影:
@implementation ShadowingView
- (void)layoutSubviews {
[super layoutSubviews];
CALayer *layer = self.layer;
layer.backgroundColor = nil;
CALayer *shadowedLayer = self.shadowedView.layer;
if (shadowedLayer == nil) {
layer.shadowColor = nil;
return;
}
NSAssert(shadowedLayer.superlayer == layer, @"shadowedView must be my direct subview");
layer.shadowColor = UIColor.blackColor.CGColor;
layer.shadowOffset = CGSizeMake(0, 1);
layer.shadowOpacity = 0.5;
layer.shadowRadius = 3;
layer.masksToBounds = NO;
CGFloat radius = shadowedLayer.cornerRadius;
layer.shadowPath = CGPathCreateWithRoundedRect(shadowedLayer.frame, radius, radius, nil);
}
当此方法在动画块内运行时(如表格视图为单元格大小的更改设置动画),并且该方法设置了阴影路径
,则核心动画会在更新阴影路径
后查找要运行的“动作”。它看起来的一种方式是将actionForLayer:forKey:
发送到层的代理,代理是ShadowingView
。因此,我们重写actionForLayer:forKey:
,以便在可能和适当的情况下提供操作。如果不行,我们就叫super
重要的是要了解,核心动画在实际更改阴影路径
的值之前,要求从阴影路径设置器内部执行操作
为了提供该操作,我们确保关键点是@“shadowPath”
,并且shadowPath
存在一个现有值,并且bounds.size
的层上已经有一个动画。为什么要查找现有的bounds.size
动画?因为现有动画具有持续时间和计时功能,所以我们应该使用它来设置阴影路径的动画。如果一切正常,我们抓取现有的阴影路径
,复制动画,将其存储在动作中,然后返回动作:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (![event isEqualToString:@"shadowPath"]) { return [super actionForLayer:layer forKey:event]; }
CGPathRef priorPath = layer.shadowPath;
if (priorPath == NULL) { return [super actionForLayer:layer forKey:event]; }
CAAnimation *sizeAnimation = [layer animationForKey:@"bounds.size"];
if (![sizeAnimation isKindOfClass:[CABasicAnimation class]]) { return [super actionForLayer:layer forKey:event]; }
CABasicAnimation *animation = [sizeAnimation copy];
animation.keyPath = @"shadowPath";
ShadowingViewAction *action = [[ShadowingViewAction alloc] init];
action.priorPath = priorPath;
action.pendingAnimation = animation;
return action;
}
@end
这是Rob Mayoff用Swift写的回答。可以节省一些时间
请不要对此投反对票。投票支持罗布·马约夫的解决方案。这是可怕的,正确的
import UIKit
class AnimatingShadowView: UIView {
struct DropShadowParameters {
var shadowOpacity: Float = 0
var shadowColor: UIColor? = .black
var shadowRadius: CGFloat = 0
var shadowOffset: CGSize = .zero
static let defaultParameters = DropShadowParameters(shadowOpacity: 0.15,
shadowColor: .black,
shadowRadius: 5,
shadowOffset: CGSize(width: 0, height: 1))
}
@IBOutlet weak var contentView: UIView! // no sense in have a shadowView without content!
var shadowParameters: DropShadowParameters = DropShadowParameters.defaultParameters
private func apply(dropShadow: DropShadowParameters) {
let layer = self.layer
layer.shadowColor = dropShadow.shadowColor?.cgColor
layer.shadowOffset = dropShadow.shadowOffset
layer.shadowOpacity = dropShadow.shadowOpacity
layer.shadowRadius = dropShadow.shadowRadius
layer.masksToBounds = false
}
override func layoutSubviews() {
super.layoutSubviews()
let layer = self.layer
layer.backgroundColor = nil
let contentLayer = self.contentView.layer
assert(contentLayer.superlayer == layer, "contentView must be a direct subview of AnimatingShadowView!")
self.apply(dropShadow: self.shadowParameters)
let radius = contentLayer.cornerRadius
layer.shadowPath = UIBezierPath(roundedRect: contentLayer.frame, cornerRadius: radius).cgPath
}
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
guard event == "shadowPath" else {
return super.action(for: layer, forKey: event)
}
guard let priorPath = layer.shadowPath else {
return super.action(for: layer, forKey: event)
}
guard let sizeAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation else {
return super.action(for: layer, forKey: event)
}
let animation = sizeAnimation.copy() as! CABasicAnimation
animation.keyPath = "shadowPath"
let action = ShadowingViewAction()
action.priorPath = priorPath
action.pendingAnimation = animation
return action
}
}
private class ShadowingViewAction: NSObject, CAAction {
var pendingAnimation: CABasicAnimation? = nil
var priorPath: CGPath? = nil
// CAAction Protocol
func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
guard let layer = anObject as? CALayer, let animation = self.pendingAnimation else {
return
}
animation.fromValue = self.priorPath
animation.toValue = layer.shadowPath
layer.add(animation, forKey: "shadowPath")
}
}
假设您正在手动设置阴影路径
,这里有一个受其他解决方案启发的解决方案,它使用更少的代码完成同样的事情
请注意,我有意构建自己的cabasicanition
,而不是复制bounds.size
动画,就像在我自己的测试中一样,我发现在复制动画仍在进行时切换复制的动画可能会导致动画捕捉到它的toValue
,而不是从当前值平稳过渡
import UIKit
class AnimatingShadowView: UIView {
struct DropShadowParameters {
var shadowOpacity: Float = 0
var shadowColor: UIColor? = .black
var shadowRadius: CGFloat = 0
var shadowOffset: CGSize = .zero
static let defaultParameters = DropShadowParameters(shadowOpacity: 0.15,
shadowColor: .black,
shadowRadius: 5,
shadowOffset: CGSize(width: 0, height: 1))
}
@IBOutlet weak var contentView: UIView! // no sense in have a shadowView without content!
var shadowParameters: DropShadowParameters = DropShadowParameters.defaultParameters
private func apply(dropShadow: DropShadowParameters) {
let layer = self.layer
layer.shadowColor = dropShadow.shadowColor?.cgColor
layer.shadowOffset = dropShadow.shadowOffset
layer.shadowOpacity = dropShadow.shadowOpacity
layer.shadowRadius = dropShadow.shadowRadius
layer.masksToBounds = false
}
override func layoutSubviews() {
super.layoutSubviews()
let layer = self.layer
layer.backgroundColor = nil
let contentLayer = self.contentView.layer
assert(contentLayer.superlayer == layer, "contentView must be a direct subview of AnimatingShadowView!")
self.apply(dropShadow: self.shadowParameters)
let radius = contentLayer.cornerRadius
layer.shadowPath = UIBezierPath(roundedRect: contentLayer.frame, cornerRadius: radius).cgPath
}
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
guard event == "shadowPath" else {
return super.action(for: layer, forKey: event)
}
guard let priorPath = layer.shadowPath else {
return super.action(for: layer, forKey: event)
}
guard let sizeAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation else {
return super.action(for: layer, forKey: event)
}
let animation = sizeAnimation.copy() as! CABasicAnimation
animation.keyPath = "shadowPath"
let action = ShadowingViewAction()
action.priorPath = priorPath
action.pendingAnimation = animation
return action
}
}
private class ShadowingViewAction: NSObject, CAAction {
var pendingAnimation: CABasicAnimation? = nil
var priorPath: CGPath? = nil
// CAAction Protocol
func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
guard let layer = anObject as? CALayer, let animation = self.pendingAnimation else {
return
}
animation.fromValue = self.priorPath
animation.toValue = layer.shadowPath
layer.add(animation, forKey: "shadowPath")
}
}