Animate CALayer mask change with fade effect

2019-07-06 03:15发布

问题:

I have a UIView, with view.layer.mask set to an instance of CAShapeLayer. The shape layer contains a path, and now I want to add a hole to this shape by adding a second shape with even/odd rule, and fade-in the appearance of this hole.

The problem is that adding to path doesn't seem to be animatable:

[UIView animateWithDuration:2 animations:^() {
    CGPathAddPath(parentPath, nil, holePath);
    [v.layer.mask didChangeValueForKey:@"path"];
}];

How would I animate this?

回答1:

After some fiddling, found a workaround:

  1. Create a layer with two sublayers with two desired shapes, and use it as a mask
  2. Animate opacity of the first sublayer (without a hole) from 1 to 0.

This works because child CAShapeLayer instances appear to be used as a union. When you hide the first sublayer without a hole, only the hole will be uncovered, the shared area will not change.

CGMutablePathRef p = CGPathCreateMutable();

// First path
CGPathAddPath(p, nil, outerPath);
CAShapeLayer *mask1 = [CAShapeLayer layer];
mask1.path = p;

// 2nd path with a hole
CGPathAddPath(p, nil, innerPath);
CAShapeLayer *mask2 = [CAShapeLayer layer];
mask2.path = p;
mask2.fillRule = kCAFillRuleEvenOdd;

CGPathRelease(p);

// Create actual mask that hosts two submasks
CALayer *mask = [CALayer layer];
[mask addSublayer:mask1];
[mask addSublayer:mask2];
myView.layer.mask = mask;
mask.frame = v.layer.bounds;
mask1.frame = mask.bounds;
mask2.frame = mask.bounds;

// ...

// Hide mask #1
CABasicAnimation *a = [CABasicAnimation animationWithKeyPath:@"opacity"];
a.fromValue = @1.0;
a.toValue = @0.0;
a.duration = 1.0;
a.fillMode = kCAFillModeForwards; // Don't reset back to original state
a.removedOnCompletion = NO;
[mask1 addAnimation:a forKey:@"hideMask1"];


回答2:

You can't use UIView animation to animate CALayers.

Most layer property changes do animation by default (implicit animation). As I recall, shape layer path changes are an exception to that.

You'll need to create a CAAnimation object where the property you are animating is the path on your mask layer.

However, that probably won't give the effect you want. The reason is that when you change a path on a shape layer, Core Animation tries to animate the change in shape of the path. Furthermore, path changes only work properly when the starting and ending paths have the same number and type of control points.

I'm not sure how you'd achieve a cross-fade between 2 different masks without a lot of work.

Off the top of my head, the only way I can think of to do this would be to create a snapshot of the new view appearance with the changed mask (probably using Core Image filters) and then do a cross-fade of a layer that displays that snapshot. Once the crossfade is complete, you would install the new path in your mask layer without animation and then remove the snapshot bitmap, revealing the real view underneath.

There might be a simpler way to achieve what you're after but I don't know what that would be. Maybe one of the CA experts that contributes to SO could chime in here.