29

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

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

我尝试使用 a 为蒙版的大小调整设置动画,CABasicAnimation但没有任何运气来调整大小的动画。

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

干杯,亚历克斯

4

9 回答 9

44

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

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

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

为了解决这个问题,我们将不得不自己利用 Core Animation,而不能依赖 UIView 动画块来为我们完成工作。

只需创建一个CATransaction与您使用的持续时间相同的[UIView animateWithDuration:...]. 这将创建一个单独的动画,但如果您的持续时间和缓动功能相同,它应该与动画块中的其他动画完全一致。

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];
于 2012-11-03T18:17:56.810 回答
13

我通过设置到该形状图层来使用 aCAShapeLayer来掩盖 a 。UIViewself.layer.mask

为了在视图大小发生变化时为-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.

于 2014-03-20T18:05:08.897 回答
5

CALayer 的 mask 属性是不可动画的,这解释了你在那个方向上缺乏运气。

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

干杯,科林

于 2010-06-01T11:29:41.623 回答
5

为 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
于 2014-03-28T11:10:51.573 回答
2

蒙版参数不设置动画,但您可以为设置为蒙版的图层设置动画...

如果您为 CAShapeLayer 的 Path 属性设置动画,则应该为蒙版设置动画。我可以验证这在我自己的项目中是否有效。不过不确定是否使用非矢量蒙版。您是否尝试过为面具的内容属性设置动画?

谢谢,乔恩

于 2010-09-28T12:23:02.940 回答
0

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

于 2012-06-21T07:48:31.267 回答
0

可以为蒙版更改设置动画。

我更喜欢使用 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];
}
于 2013-01-03T12:13:48.703 回答
0

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

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()
于 2019-04-11T15:09:26.193 回答
0

@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
        }
    }
}
于 2021-12-01T05:29:51.633 回答