使用核心动画层创建可动画的半透明覆盖层

6
我正在创建一个在我的应用程序中移动的聚光灯,就像这样:
Spotlight A Spotlight B
在示例应用程序中(如上所示),背景图层为蓝色,我有一层覆盖在其上,使其变暗,除了一个显示正常的圆形外。我让它工作了(您可以在下面的代码中看到它的工作方式)。在我的真实应用程序中,其他CALayer中存在实际内容,而不仅仅是蓝色。
这是我的问题:它不会动画。我使用CGContext绘制创建圆形(它是黑色图层中的空点)。当您单击示例应用程序中的按钮时,我会在不同大小的不同位置绘制圆形。
我希望它平稳地转换和缩放,而不是像现在这样跳动。可能需要使用不同的创建聚光灯效果的方法,或者可能有我不知道的隐式动画-drawLayer:inContext:调用的方法。
很容易创建示例应用程序:
  1. 新建Cocoa应用程序(使用ARC)
  2. 添加Quartz框架
  3. 将自定义视图和按钮拖放到XIB上
  4. 将自定义视图链接到新类(SpotlightView),提供下面的代码
  5. 删除SpotlightView.h,因为我已经包含了它的内容
  6. 将按钮的输出链接到-moveSpotlight:动作
//
//  SpotlightView.m
//

#import <Quartz/Quartz.h>

@interface SpotlightView : NSView
- (IBAction)moveSpotlight:(id)sender;
@end

@interface SpotlightView ()
@property (strong) CALayer *spotlightLayer;
@property (assign) CGRect   highlightRect;
@end


@implementation SpotlightView

@synthesize spotlightLayer;
@synthesize highlightRect;

- (id)initWithFrame:(NSRect)frame {
    if ((self = [super initWithFrame:frame])) {
        self.wantsLayer = YES;

        self.highlightRect = CGRectNull;

        self.spotlightLayer = [CALayer layer];
        self.spotlightLayer.frame = CGRectInset(self.layer.bounds, -50, -50);
        self.spotlightLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;

        self.spotlightLayer.opacity = 0.60;
        self.spotlightLayer.delegate = self;

        CIFilter *blurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
        [blurFilter setValue:[NSNumber numberWithFloat:5.0]
                      forKey:@"inputRadius"];
        self.spotlightLayer.filters = [NSArray arrayWithObject:blurFilter];

        [self.layer addSublayer:self.spotlightLayer];
    }

    return self;
}

- (void)drawRect:(NSRect)dirtyRect {}

- (void)moveSpotlight:(id)sender {
    [self.spotlightLayer setNeedsDisplay];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    if (layer == self.spotlightLayer) {
        CGContextSaveGState(ctx);

        CGColorRef blackColor = CGColorCreateGenericGray(0.0, 1.0);

        CGContextSetFillColorWithColor(ctx, blackColor);
        CGColorRelease(blackColor);

        CGContextClearRect(ctx, layer.bounds);
        CGContextFillRect(ctx, layer.bounds);

        // Causes the toggling
        if (CGRectIsNull(self.highlightRect) || self.highlightRect.origin.x != 25) {
            self.highlightRect = CGRectMake(25, 25, 100, 100);
        } else {
            self.highlightRect = CGRectMake(NSMaxX(self.layer.bounds) - 50,
                                            NSMaxY(self.layer.bounds) - 50,
                                            25, 25);
        }

        CGRect drawnRect = [layer convertRect:self.highlightRect
                                    fromLayer:self.layer];
        CGMutablePathRef highlightPath = CGPathCreateMutable();
        CGPathAddEllipseInRect(highlightPath, NULL, drawnRect);
        CGContextAddPath(ctx, highlightPath);

        CGContextSetBlendMode(ctx, kCGBlendModeClear);
        CGContextFillPath(ctx);

        CGPathRelease(highlightPath);
        CGContextRestoreGState(ctx);
    }
    else {
        CGColorRef blueColor = CGColorCreateGenericRGB(0, 0, 1.0, 1.0);
        CGContextSetFillColorWithColor(ctx, blueColor);
        CGContextFillRect(ctx, layer.bounds);
        CGColorRelease(blueColor);
    }
}

@end

你尝试过在暗色聚光灯层上设置比例和平移变换吗? - David Rönnqvist
@DavidRönnqvist 那样做不行,因为带有聚光灯的图层大小是整个“superlayer”的大小。 - Dov
聚光灯可以成为遮罩层吗?您可以将聚光灯绘制到其自己的图层中,并将其设置为黑暗层的 -mask: 属性。这样,我认为您可以在不考虑黑暗层的大小和位置的情况下动画化聚光灯的大小和位置。尽管我还没有尝试过。 - David Rönnqvist
@DavidRönnqvist 这太接近了,但我需要反转掩码,而且我看不到简单的方法来做到这一点。如果我必须将整个掩码层涂黑并在其中切一个洞,那么我又回到了起点。 - Dov
如果可以的话,您可以获取背景(上面是蓝色)的图像表示,并将其再次绘制在暗层上,并使用遮罩将其限制在聚光灯上。这样,您就可以对遮罩进行动画处理,但前提是下面的内容相对静态。 - David Rönnqvist
不幸的是,随着聚光灯的移动,内容经常发生变化。 - Dov
1个回答

6
我终于搞定了。促使我找到答案的是听说了 CAShapeLayer 类。一开始,我认为这会是一个比在标准 CALayer 中绘制和清除图层内容更简单的方法。但是我阅读了 CAShapeLayer 的文档中关于 path 属性的说明,它可以被动画化,但不能隐式实现。

虽然图层遮罩可能更直观优雅,但似乎无法使用遮罩来隐藏所属图层的一部分,而不是显示一部分,因此我无法使用它。我对这个解决方案感到满意,因为它很清楚发生了什么。我希望它能使用隐式动画,但动画代码只有几行。

下面,我修改了问题中的示例代码以添加平滑动画。(我删除了 CIFilter 代码,因为它是多余的。解决方案仍然可以与滤镜一起使用。)

//
//  SpotlightView.m
//

#import <Quartz/Quartz.h>

@interface SpotlightView : NSView
- (IBAction)moveSpotlight:(id)sender;
@end

@interface SpotlightView ()
@property (strong) CAShapeLayer *spotlightLayer;
@property (assign) CGRect   highlightRect;
@end


@implementation SpotlightView

@synthesize spotlightLayer;
@synthesize highlightRect;

- (id)initWithFrame:(NSRect)frame {
    if ((self = [super initWithFrame:frame])) {
        self.wantsLayer = YES;

        self.highlightRect = CGRectNull;

        self.spotlightLayer = [CAShapeLayer layer];
        self.spotlightLayer.frame = self.layer.bounds;
        self.spotlightLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
        self.spotlightLayer.fillRule = kCAFillRuleEvenOdd;

        CGColorRef blackoutColor = CGColorCreateGenericGray(0.0, 0.60);
        self.spotlightLayer.fillColor = blackoutColor;
        CGColorRelease(blackoutColor);

        [self.layer addSublayer:self.spotlightLayer];
    }

    return self;
}

- (void)drawRect:(NSRect)dirtyRect {}

- (CGPathRef)newSpotlightPathInRect:(CGRect)containerRect
                      withHighlight:(CGRect)spotlightRect {
    CGMutablePathRef shape = CGPathCreateMutable();

    CGPathAddRect(shape, NULL, containerRect);

    if (!CGRectIsNull(spotlightRect)) {
        CGPathAddEllipseInRect(shape, NULL, spotlightRect);
    }

    return shape;
}

- (void)moveSpotlight {
    CGPathRef toShape = [self newSpotlightPathInRect:self.spotlightLayer.bounds
                                       withHighlight:self.highlightRect];

    CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    pathAnimation.fromValue = (__bridge id)self.spotlightLayer.path;
    pathAnimation.toValue   = (__bridge id)toShape;

    [self.spotlightLayer addAnimation:pathAnimation forKey:@"path"];
    self.spotlightLayer.path = toShape;

    CGPathRelease(toShape);
}

- (void)moveSpotlight:(id)sender {
    if (CGRectIsNull(self.highlightRect) || self.highlightRect.origin.x != 25) {
        self.highlightRect = CGRectMake(25, 25, 100, 100);
    } else {
        self.highlightRect = CGRectMake(NSMaxX(self.layer.bounds) - 50,
                                        NSMaxY(self.layer.bounds) - 50,
                                        25, 25);
    }

    [self moveSpotlight];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    CGColorRef blueColor = CGColorCreateGenericRGB(0, 0, 1.0, 1.0);
    CGContextSetFillColorWithColor(ctx, blueColor);
    CGContextFillRect(ctx, layer.bounds);
    CGColorRelease(blueColor);
}

@end

非常感谢,这真的帮了我很大的忙!我需要一些东西可以使我的分段圆环看起来在收缩,但实际上并没有缩小。现在我可以做到了。 - Jelle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接