我有一个UITableView,我试图使用其beginUpdates
和endUpdates
方法进行展开和折叠,并显示一个投影。在我的自定义UITableViewCell中,我有一个图层,我在layoutSubviews
中创建了一个阴影:
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];
据我了解,当我调用beginUpdates
时,如果不存在,则为当前运行循环创建隐式CALayerTransaction
。此外,layoutSubviews
也会被调用。这里的问题是根据UITableViewCell的新大小立即绘制生成的阴影。我真的需要阴影继续以预期的方式投射,因为实际的图层是动画。
由于我创建的图层不是支持CALayer,因此它会动画而不显式指定CATransaction,这是预期的。但是,据我所知,我真的需要一些方法来抓住beginUpdates
/ endUpdates
CATransaction并执行动画。如果有的话,我该怎么做?
答案 0 :(得分:9)
所以我猜你有这样的事情:
(我在模拟器中启用了“Debug> Slow Animations”。)而且你不喜欢阴影跳到新大小的方式。你想要这个:
您可以找到我的测试项目in this github repository。
获取动画参数并在表格视图的动画块中添加动画非常棘手但并非不可能。最棘手的部分是您需要更新阴影视图本身的shadowPath
方法中的layoutSubviews
,或阴影视图的直接超视图。在我的演示视频中,这意味着shadowPath
需要通过绿框视图的layoutSubviews
方法或绿框的即时超级视图进行更新。
我选择创建一个ShadowingView
类,其唯一的工作是绘制和动画其子视图的阴影。这是界面:
@interface ShadowingView : UIView
@property (nonatomic, strong) IBOutlet UIView *shadowedView;
@end
要使用ShadowingView
,我将其添加到我的故事板中的单元格视图中。实际上,它嵌套在单元格内的堆栈视图中。然后我将绿色框添加为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);
}
当在动画块内部运行此方法时(就像表视图动画化单元格大小的变化一样),并且方法设置为shadowPath
,Core Animation会查找“动作”更新shadowPath
后运行。其中一种展示方式是将actionForLayer:forKey:
发送到图层的代理,代理是ShadowingView
。因此,如果可能且适当,我们会覆盖actionForLayer:forKey:
以提供操作。如果我们不能,我们只需拨打super
。
重要的是要了解Core Animation要求shadowPath
setter内部的操作,之前实际更改shadowPath
的值。
要提供操作,我们会确保密钥为@"shadowPath"
,shadowPath
存在现有值,并且bounds.size
图层上已存在动画。为什么我们要查找现有的bounds.size
动画?因为现有动画具有持续时间和计时功能,我们应该使用它来为shadowPath
设置动画。如果一切正常,我们抓住现有的shadowPath
,制作动画的副本,将它们存储在动作中,然后返回动作:
- (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
动作是什么样的?这是界面:
@interface ShadowingViewAction : NSObject <CAAction>
@property (nonatomic, strong) CABasicAnimation *pendingAnimation;
@property (nonatomic) CGPathRef priorPath;
@end
实施需要runActionForKey:object:arguments:
方法。在此方法中,我们使用已保存的旧actionForLayer:forKey:
和新的shadowPath
更新我们在shadowPath
中创建的动画,然后我们将动画添加到图层。
我们还需要管理已保存路径的保留计数,因为ARC无法管理CGPath
个对象。
@implementation ShadowingViewAction
- (void)runActionForKey:(NSString *)event object:(id)anObject arguments:(NSDictionary *)dict {
if (![anObject isKindOfClass:[CALayer class]] || _pendingAnimation == nil) { return; }
CALayer *layer = anObject;
_pendingAnimation.fromValue = (__bridge id)_priorPath;
_pendingAnimation.toValue = (__bridge id)layer.shadowPath;
[layer addAnimation:_pendingAnimation forKey:@"shadowPath"];
}
- (void)setPriorPath:(CGPathRef)priorPath {
CGPathRetain(priorPath);
CGPathRelease(_priorPath);
_priorPath = priorPath;
}
- (void)dealloc {
CGPathRelease(_priorPath);
}
@end
答案 1 :(得分:3)
这是Rob Mayoff用Swift写的答案。可以节省一些时间。
请不要赞成这一点。 Upvote Rob Mayoff的解决方案。它真棒,而且正确。
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")
}
}
答案 2 :(得分:0)
UITableView
可能无法创建CATransaction
,或者如果是,则会等到您结束更新后再等待。我的理解是表视图只是合并这些函数之间的所有更改,然后根据需要创建动画。你没有办法处理它提交的实际动画参数,因为我们不知道实际发生的时间。当您在UIScrollView
中设置内容偏移量更改时,会发生同样的事情:系统不提供有关动画本身的上下文,这令人沮丧。也无法向系统查询当前CATransaction
。
您可以做的最好的事情就是检查UITableView
正在创建的动画,并模拟您自己动画中的相同计时参数。 add(_:forKey:)
上的Swizzling CALayer
可以让您检查所有添加的动画。您当然不希望实际发布此内容,但我经常在调试中使用此技术来确定要添加的动画及其属性。
我怀疑你必须在tableView(_:willDisplayCell:for:row:)
中为适当的单元格提交自己的阴影图层动画。
答案 3 :(得分:0)
假设您手动设置shadowPath
,这是一个受其他方法启发的解决方案,该解决方案使用更少的代码即可完成相同的工作。
请注意,我故意构建自己的CABasicAnimation
而不是完全复制bounds.size
动画,因为在我自己的测试中,我发现在动画进行过程中切换复制的动画可能会导致要捕捉到其toValue
的动画,而不是从其当前值平滑过渡。
class ViewWithAutosizedShadowPath: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let oldShadowPath = layer.shadowPath
let newShadowPath = CGPath(rect: bounds, transform: nil)
if let boundsAnimation = layer.animation(forKey: "bounds.size") as? CABasicAnimation {
let shadowPathAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowPathAnimation.duration = boundsAnimation.duration
shadowPathAnimation.timingFunction = boundsAnimation.timingFunction
shadowPathAnimation.fromValue = oldShadowPath
shadowPathAnimation.toValue = newShadowPath
layer.add(shadowPathAnimation, forKey: "shadowPath")
}
layer.shadowPath = newShadowPath
}
}