在UITableviewCell高度动画时同时动画CALayer阴影

时间:2017-05-12 18:08:49

标签: objective-c uitableview core-animation calayer

我有一个UITableView,我试图使用其beginUpdatesendUpdates方法进行展开和折叠,并显示一个投影。在我的自定义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并执行动画。如果有的话,我该怎么做?

4 个答案:

答案 0 :(得分:9)

所以我猜你有这样的事情:

bad animation

(我在模拟器中启用了“Debug> Slow Animations”。)而且你不喜欢阴影跳到新大小的方式。你想要这个:

good animation

您可以找到我的测试项目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
    }
    
}