简体   繁体   English

动画 CALayer 的蒙版大小变化

[英]Animating a CALayer's mask size change

I have a UIView subclass which uses a CAShapeLayer mask on its CALayer .我有一个UIView子类,它在其CALayer上使用CAShapeLayer掩码。 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.当我使用标准动画块调整UIView大小时, UIView本身及其CALayer调整大小就好了。 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.我已经尝试使用CABasicAnimation为蒙版的调整大小设置动画,但没有任何运气使调整大小设置动画。

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 ).我是否需要摆脱面具,或者我是否必须改变我目前绘制面具的方式(使用- (void)drawInContext:(CGContextRef)ctx )。

Cheers, Alex干杯,亚历克斯

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. mask属性本身不可动画。
  • Since the mask is a CALayer it can be animated on its own. 由于蒙版是CALayer,因此可以自行设置动画。
  • 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 ) (参见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. 视图图层的蒙版未绑定到UIView,因此它不会接收应用于视图图层的核心动画事务。
  • 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. 我们正在直接修改CALayer,因此我们不能指望UIView会知道我们要做什么,因此UIView动画不会创建核心动画事务以包含对属性的更改。

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. 为了解决这个问题,我们将不得不自己使用Core Animation,并且不能依赖UIView动画块来为我们工作。

Simply create a CATransaction with the same duration that you are using with [UIView animateWithDuration:...] . 只需创建一个CATransaction与您使用的持续时间相同[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];

I use a CAShapeLayer to mask a UIView by setting self.layer.mask to that shape layer. 我使用CAShapeLayer通过将self.layer.mask设置self.layer.mask形状图层来掩盖UIView

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. 要在视图大小发生更改时为蒙版设置动画,我会覆盖-setBounds:如果在动画期间更改边界,则为蒙版图层路径设置动画。

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) (例如, self.maskLayer设置为`self.layer.mask)

My -createMaskPath calculates the CGPathRef that I use to mask the view. 我的-createMaskPath计算用于屏蔽视图的CGPathRef。 I also update the mask path in -layoutSubviews . 我还在-layoutSubviews更新了掩码路径。

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: 要为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

The mask property of CALayer is not animatable which explains your lack of luck in that direction. CALayer的mask属性不具有动画效果,这解释了你在这方面缺乏运气。

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? (你能提供一些代码吗?)掩码是否设置了needsDisplayOnBoundsChange属性?

Cheers, Corin 干杯,科林

The mask parameter doesn't animate, but you can animate the layer which is set as the mask... mask参数不会设置动画,但您可以为设置为蒙版的图层设置动画...

If you animate the CAShapeLayer's Path property, that should animate the mask. 如果为CAShapeLayer的Path属性设置动画,则应该为蒙版设置动画。 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 谢谢,乔恩

I couldn't find any programmatical solution so I just draw an png image with correct shape and alpha values and used that instead. 我找不到任何编程解决方案,所以我只是绘制一个具有正确形状和alpha值的png图像并使用它。 That way I don't need to use a mask... 这样我就不需要使用面具......

It is possible to animate the mask change. 可以为蒙版更改设置动画。

I prefer to use CAShapeLayer as the mask layer. 我更喜欢使用CAShapeLayer作为遮罩层。 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. 在为任何更改设置动画之前,将源的内容转储到实例CGImageRef中,并为动画创建一个新图层。 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];
}

Swift 3+ answer based on Kekoa's solution : 基于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()

Swift implementation of @stigi answer, my mask layer is called shape Layer @stigi答案的Swift实现,我的遮罩层被称为形状层

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
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM