Objective c 在UITableviewCell高度设置动画的同时设置CALayer阴影的动画

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

我有一个UITableView,我正试图使用它的
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") } }