动画 CALayer 的蒙版大小变化

发布于 2024-09-03 08:03:43 字数 404 浏览 10 评论 0原文

我有一个 UIView 子类,它在其 CALayer 上使用 CAShapeLayer 掩码。面具采用独特的形状,具有三个圆角,其余角有一个切出的矩形。

当我使用标准动画块调整 UIView 的大小时,UIView 本身及其 CALayer 的大小调整得很好。然而,蒙版是立即应用的,这会导致一些绘图问题。

我尝试使用 CABasicAnimation 为蒙版的大小调整设置动画,但没有成功地获得调整大小的动画。

我可以以某种方式在蒙版上实现动画调整大小效果吗?我是否需要去掉蒙版,或者我是否必须更改当前绘制蒙版的方式(使用 - (void)drawInContext:(CGContextRef)ctx)。

干杯, 亚历克斯

I have a UIView subclass which uses a CAShapeLayer mask on its CALayer. The mask uses a distinct shape, with three rounded corners and a cut out rectangle in the remaining corner.

When I resize my UIView using a standard animation block, the UIView itself and its CALayer resize just fine. The mask, however, is applied instantly, which leads to some drawing issues.

I've tried animating the mask's resizing using a CABasicAnimation but didn't have any luck getting the resizing animated.

Can I somehow achieve an animated resizing effect on the mask? Do I need to get rid of the mask, or will I have to change something about the way I currently draw the mask (using - (void)drawInContext:(CGContextRef)ctx).

Cheers,
Alex

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

葬シ愛 2024-09-10 08:03:43

我找到了这个问题的解决方案。其他答案部分正确并且有帮助。

以下几点对于理解解决方案非常重要:

  • mask 属性本身不可设置动画。
  • 由于遮罩是 CALayer,因此它可以自行设置动画。
  • 框架不可设置动画,请使用边界和位置。这可能不适用于您(如果您没有尝试为框架设置动画),但对我来说是一个问题。 (请参阅 Apple QA 1620
  • 视图层的遮罩不与 UIView 绑定,因此它将不会接收应用于视图层的核心动画事务。
  • 我们直接修改 CALayer,因此我们不能指望 UIView 知道我们要做什么,因此 UIView 动画不会创建核心动画事务来包含对属性的更改。

为了解决这个问题,我们必须自己利用核心动画,而不能依赖 UIView 动画块来为我们完成工作。

只需创建一个具有与 [UIView animateWithDuration:...] 相同持续时间的 CATransaction 即可。这将创建一个单独的动画,但如果您的持续时间和缓动函数相同,则它应该与动画块中的其他动画完全相同。

NSTimeInterval duration = 0.5;// match this to the value of the UIView animateWithDuration: call

[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:duration] forKey:kCATransactionAnimationDuration];

self.myView.layer.mask.position = CGPointMake(newX, 0);
self.myView.layer.mask.bounds = CGRectMake(0, 0, newWidth, newHeight);

[CATransaction commit];

I found the solution to this problem. Other answers are partially correct and are helpful.

The following points are important to understanding the solution:

  • The mask property is not animatable itself.
  • Since the mask is a CALayer it can be animated on its own.
  • Frame is not animatable, use bounds and position. This may not apply to you(if you weren't trying to animate the frame), but was an issue for me. (See Apple QA 1620)
  • A view layer's mask is not tied to UIView so it will not receive the core animation transaction that is applied to the view's layer.
  • We are modifying the CALayer directly, so we can't expect that UIView will have any idea of what we are trying to do, so the UIView animation won't create the core animation transaction to include changes to our properties.

In order to solve, we are going to have to tap into Core Animation ourselves, and can't rely on the UIView animation block to do the work for us.

Simply create a CATransaction with the same duration that you are using with [UIView animateWithDuration:...]. This will create a separate animation, but if your durations and easing function is the same, it should animate exactly with the other animations in your animation block.

NSTimeInterval duration = 0.5;// match this to the value of the UIView animateWithDuration: call

[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:duration] forKey:kCATransactionAnimationDuration];

self.myView.layer.mask.position = CGPointMake(newX, 0);
self.myView.layer.mask.bounds = CGRectMake(0, 0, newWidth, newHeight);

[CATransaction commit];
梨涡 2024-09-10 08:03:43

我使用 CAShapeLayer 通过将 self.layer.mask 设置为形状层来屏蔽 UIView。

为了在视图大小发生变化时对遮罩进行动画处理,我覆盖了 -setBounds: ,以便在动画期间更改边界时对遮罩层路径进行动画处理。

以下是我的实现方式:(

- (void)setBounds:(CGRect)bounds
{
    [super setBounds:bounds];
    CAPropertyAnimation *boundsAnimation = (CABasicAnimation *)[self.layer animationForKey:@"bounds"];

    // update the mask
    self.maskLayer.frame = self.layer.bounds;

    // if the bounds change happens within an animation, also animate the mask path
    if (!boundsAnimation) {
        self.maskLayer.path = [self createMaskPath];
    } else {
        // copying the original animation allows us to keep all animation settings
        CABasicAnimation *animation = [boundsAnimation copy];
        animation.keyPath = @"path";

        CGPathRef newPath = [self createMaskPath];
        animation.fromValue = (id)self.maskLayer.path;
        animation.toValue = (__bridge id)newPath;

        self.maskLayer.path = newPath;

        [self.maskLayer addAnimation:animation forKey:@"path"];
    }
}

例如,self.maskLayer 设置为 `self.layer.mask)

我的 -createMaskPath 计算我用来屏蔽视图的 CGPathRef 。我还更新了 -layoutSubviews 中的遮罩路径。

I use a CAShapeLayer to mask a UIView by setting self.layer.mask to that shape layer.

To animate the mask whenever the size of the view changes I overwrote the -setBounds: to animate the mask layer path if the bounds are changed during an animation.

Here's how I implemented it:

- (void)setBounds:(CGRect)bounds
{
    [super setBounds:bounds];
    CAPropertyAnimation *boundsAnimation = (CABasicAnimation *)[self.layer animationForKey:@"bounds"];

    // update the mask
    self.maskLayer.frame = self.layer.bounds;

    // if the bounds change happens within an animation, also animate the mask path
    if (!boundsAnimation) {
        self.maskLayer.path = [self createMaskPath];
    } else {
        // copying the original animation allows us to keep all animation settings
        CABasicAnimation *animation = [boundsAnimation copy];
        animation.keyPath = @"path";

        CGPathRef newPath = [self createMaskPath];
        animation.fromValue = (id)self.maskLayer.path;
        animation.toValue = (__bridge id)newPath;

        self.maskLayer.path = newPath;

        [self.maskLayer addAnimation:animation forKey:@"path"];
    }
}

(For the example self.maskLayer is set to `self.layer.mask)

My -createMaskPath calculates the CGPathRef that I use to mask the view. I also update the mask path in -layoutSubviews.

冷情 2024-09-10 08:03:43

CALayer 的 mask 属性不可设置动画,这解释了您在这方面缺乏运气。

蒙版的绘制是否取决于蒙版的框架/边界? (您可以提供一些代码吗?)掩码是否设置了needsDisplayOnBoundsChange属性?

干杯,
科林

The mask property of CALayer is not animatable which explains your lack of luck in that direction.

Does the drawing of your mask depend on the frame/bounds of the mask? (Can you provide some code?) Does the mask have needsDisplayOnBoundsChange property set?

Cheers,
Corin

北笙凉宸 2024-09-10 08:03:43

要对 UIView 的遮罩层的边界变化进行动画处理:子类 UIView,并使用 CATransaction 对遮罩进行动画处理 - 类似于 Kekodas 答案,但更通用:

@implementation UIMaskView

- (void) layoutSubviews {
    [super layoutSubviews];

    CAAnimation* animation = [self.layer animationForKey:@"bounds"];
    if (animation) {
        [CATransaction begin];
        [CATransaction setAnimationDuration:animation.duration];
    }

    self.layer.mask.bounds = self.layer.bounds;
    if (animation) {
        [CATransaction commit];
    }
}

@end

To animate the bounds change of the mask layer of a UIView: subclass UIView, and animate the mask with a CATransaction - similar to Kekodas answer but more general:

@implementation UIMaskView

- (void) layoutSubviews {
    [super layoutSubviews];

    CAAnimation* animation = [self.layer animationForKey:@"bounds"];
    if (animation) {
        [CATransaction begin];
        [CATransaction setAnimationDuration:animation.duration];
    }

    self.layer.mask.bounds = self.layer.bounds;
    if (animation) {
        [CATransaction commit];
    }
}

@end
笑脸一如从前 2024-09-10 08:03:43

mask 参数不会产生动画,但您可以对设置为 mask 的图层进行动画处理...

如果对 CAShapeLayer 的 Path 属性进行动画处理,则应该对 mask 进行动画处理。我可以从我自己的项目中验证这是否有效。但不确定是否使用非矢量掩码。您是否尝试过对蒙版的内容属性进行动画处理?

谢谢,
乔恩

The mask parameter doesn't animate, but you can animate the layer which is set as the mask...

If you animate the CAShapeLayer's Path property, that should animate the mask. I can verify that this works from my own projects. Not sure about using a non-vector mask though. Have you tried animating the contents property of the mask?

Thanks,
Jon

写给空气的情书 2024-09-10 08:03:43

我找不到任何编程解决方案,所以我只是绘制一个具有正确形状和 alpha 值的 png 图像并使用它。这样我就不用戴口罩了

I couldn't find any programmatical solution so I just draw an png image with correct shape and alpha values and used that instead. That way I don't need to use a mask...

故乡的云 2024-09-10 08:03:43

可以对蒙版变化进行动画处理。

我更喜欢使用 CAShapeLayer 作为遮罩层。借助属性路径来动画遮罩更改非常方便。

在对任何更改进行动画处理之前,将源内容转储到实例 CGImageRef 中,并为动画创建一个新层。在动画期间隐藏原始图层并在动画结束时显示它。

以下是在属性路径上创建关键动画的示例代码。
如果您想创建自己的路径动画,请确保路径中始终有相同数量的点。

- (CALayer*)_mosaicMergeLayer:(CGRect)bounds content:(CGImageRef)content isUp:(BOOL)isUp {

    CALayer* layer = [CALayer layer];
    layer.frame = bounds;
    layer.backgroundColor = [[UIColor clearColor] CGColor];
    layer.contents = (id)content;

    CAShapeLayer* maskLayer = [CAShapeLayer layer];
    maskLayer.fillColor = [[UIColor blackColor] CGColor];
    maskLayer.frame = bounds;
    maskLayer.fillRule = kCAFillRuleEvenOdd;
    maskLayer.path = ( isUp ? [self _maskArrowUp:-bounds.size.height*2] : [self _maskArrowDown:bounds.size.height*2] );
    layer.mask = maskLayer;

    CAKeyframeAnimation* ani = [CAKeyframeAnimation animationWithKeyPath:@"path"];
    ani.removedOnCompletion = YES;
    ani.duration = 0.3f;
    ani.fillMode = kCAFillModeForwards;
    ani.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

    NSArray* values = ( isUp ?
        [NSArray arrayWithObjects:
        (id)[self _maskArrowUp:0],
        (id)[self _maskArrowUp:-ceilf(bounds.size.height*1.2)],
        nil] 
    :       
        [NSArray arrayWithObjects:
        (id)[self _maskArrowDown:0],
        (id)[self _maskArrowDown:bounds.size.height],
        nil] 
    );
    ani.values = values;
    ani.delegate = self;
    [maskLayer addAnimation:ani forKey:nil];

    return layer;
}

- (void)_startMosaicMergeAni:(BOOL)up {

    CALayer* overlayer = self.aniLayer;
    CGRect bounds = overlayer.bounds;
    self.firstHalfAni = NO;

    CALayer* frontLayer = nil;
    frontLayer = [self _mosaicMergeLayer:bounds 
                                content:self.toViewSnapshot 
                                isUp:up];
    overlayer.contents = (id)self.fromViewSnapshot;
    [overlayer addSublayer:frontLayer];
}

It is possible to animate the mask change.

I prefer to use CAShapeLayer as the mask layer. It is very convenient to animate a mask change with the help of property path.

Before animate any change, dump the content of the source into an instance CGImageRef, and create a new layer for animation. Hide the original layer during the animation and show it when animation ends.

The following is a sample code for creating key animation on property path.
If you want to create your own path animation, make sure that there are always same number of points in the paths.

- (CALayer*)_mosaicMergeLayer:(CGRect)bounds content:(CGImageRef)content isUp:(BOOL)isUp {

    CALayer* layer = [CALayer layer];
    layer.frame = bounds;
    layer.backgroundColor = [[UIColor clearColor] CGColor];
    layer.contents = (id)content;

    CAShapeLayer* maskLayer = [CAShapeLayer layer];
    maskLayer.fillColor = [[UIColor blackColor] CGColor];
    maskLayer.frame = bounds;
    maskLayer.fillRule = kCAFillRuleEvenOdd;
    maskLayer.path = ( isUp ? [self _maskArrowUp:-bounds.size.height*2] : [self _maskArrowDown:bounds.size.height*2] );
    layer.mask = maskLayer;

    CAKeyframeAnimation* ani = [CAKeyframeAnimation animationWithKeyPath:@"path"];
    ani.removedOnCompletion = YES;
    ani.duration = 0.3f;
    ani.fillMode = kCAFillModeForwards;
    ani.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

    NSArray* values = ( isUp ?
        [NSArray arrayWithObjects:
        (id)[self _maskArrowUp:0],
        (id)[self _maskArrowUp:-ceilf(bounds.size.height*1.2)],
        nil] 
    :       
        [NSArray arrayWithObjects:
        (id)[self _maskArrowDown:0],
        (id)[self _maskArrowDown:bounds.size.height],
        nil] 
    );
    ani.values = values;
    ani.delegate = self;
    [maskLayer addAnimation:ani forKey:nil];

    return layer;
}

- (void)_startMosaicMergeAni:(BOOL)up {

    CALayer* overlayer = self.aniLayer;
    CGRect bounds = overlayer.bounds;
    self.firstHalfAni = NO;

    CALayer* frontLayer = nil;
    frontLayer = [self _mosaicMergeLayer:bounds 
                                content:self.toViewSnapshot 
                                isUp:up];
    overlayer.contents = (id)self.fromViewSnapshot;
    [overlayer addSublayer:frontLayer];
}
早茶月光 2024-09-10 08:03:43

Swift 3+ 基于 Kekoa 的解决方案的答案:

let duration = 0.15 //match this to the value of the UIView.animate(withDuration:) call

CATransaction.begin()
CATransaction.setValue(duration, forKey: kCATransactionAnimationDuration)

myView.layer.mask.position = CGPoint(x: [new X], y: [new Y]) //just an example

CATransaction.commit()

Swift 3+ answer based on Kekoa's solution:

let duration = 0.15 //match this to the value of the UIView.animate(withDuration:) call

CATransaction.begin()
CATransaction.setValue(duration, forKey: kCATransactionAnimationDuration)

myView.layer.mask.position = CGPoint(x: [new X], y: [new Y]) //just an example

CATransaction.commit()
‖放下 2024-09-10 08:03:43

快速实现@stigi答案,我的遮罩层称为形状层

override var bounds: CGRect {
    didSet {
        // debugPrint(self.layer.animationKeys()) //Very useful for know if animation is happening and key name
        let propertyAnimation = self.layer.animation(forKey: "bounds.size")

        self.shapeLayer?.frame = self.layer.bounds

        // if the bounds change happens within an animation, also animate the mask path
        if let boundAnimation = propertyAnimation as? CABasicAnimation {
            // copying the original animation allows us to keep all animation settings
            if let basicAnimation = boundAnimation.copy() as? CABasicAnimation {
                basicAnimation.keyPath = "path"

                let newPath = UIBezierPathUtils.customShapePath(rect: self.layer.bounds, cornersTriangleSize: cornerTriangleSize).cgPath
                basicAnimation.fromValue = self.shapeLayer?.path
                basicAnimation.toValue = newPath

                self.shapeLayer?.path = newPath
                self.shapeLayer?.add(basicAnimation, forKey: "path")
            }
        } else {
            self.shapeLayer?.path = UIBezierPathUtils.customShapePath(rect: self.layer.bounds, cornersTriangleSize: cornerTriangleSize).cgPath
        }
    }
}

Swift implementation of @stigi answer, my mask layer is called shape Layer

override var bounds: CGRect {
    didSet {
        // debugPrint(self.layer.animationKeys()) //Very useful for know if animation is happening and key name
        let propertyAnimation = self.layer.animation(forKey: "bounds.size")

        self.shapeLayer?.frame = self.layer.bounds

        // if the bounds change happens within an animation, also animate the mask path
        if let boundAnimation = propertyAnimation as? CABasicAnimation {
            // copying the original animation allows us to keep all animation settings
            if let basicAnimation = boundAnimation.copy() as? CABasicAnimation {
                basicAnimation.keyPath = "path"

                let newPath = UIBezierPathUtils.customShapePath(rect: self.layer.bounds, cornersTriangleSize: cornerTriangleSize).cgPath
                basicAnimation.fromValue = self.shapeLayer?.path
                basicAnimation.toValue = newPath

                self.shapeLayer?.path = newPath
                self.shapeLayer?.add(basicAnimation, forKey: "path")
            }
        } else {
            self.shapeLayer?.path = UIBezierPathUtils.customShapePath(rect: self.layer.bounds, cornersTriangleSize: cornerTriangleSize).cgPath
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文