iOS Animate Mask Over UIImage

2019-02-07 14:20发布

问题:

I am using Swift 1.2 and my goal is to animate an image mask over a static UIImage. What I have implemented is a swift version of masking an image that I originally found in Objective-C.

func maskImage(image: UIImage, mask: UIImage) -> UIImage! {

    let maskRef = mask.CGImage;

    let mask = CGImageMaskCreate(
        CGImageGetWidth(maskRef),
        CGImageGetHeight(maskRef),
        CGImageGetBitsPerComponent(maskRef),
        CGImageGetBitsPerPixel(maskRef),
        CGImageGetBytesPerRow(maskRef),
        CGImageGetDataProvider(maskRef), nil, false);

    let masked = CGImageCreateWithMask(image.CGImage, mask);
    let retImage = UIImage(CGImage: masked);
    return retImage;
}

It works great! However, putting it in motion is my challenge.

Is there a way to either iteratively apply the mask with a different horizontal offset or a better way to approach this problem entirely - perhaps with a CALayer implementation?

Thanks for your help!

EDIT: Based on what was posted as an answer, I added this:

    let image = UIImage(named: "clouds");

    let imageView = UIImageView(image: image);
    let layer = CALayer();
    layer.contents = UIImage(named: "alpha-mask")?.CGImage;
    layer.bounds = CGRectMake(0, 0, image.size.width, image.size.height);

    // For other folks learning, this did not work
    //let animation = CABasicAnimation(keyPath: "bounds.origin.x");

    // This does work
    let animation = CABasicAnimation(keyPath: "position.x");

    animation.duration = 2;
    animation.delegate = self;
    animation.fillMode = kCAFillModeForwards;
    animation.repeatCount = 0;
    animation.fromValue = 0.0;
    animation.toValue = image.size.width;
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear);
    animation.removedOnCompletion = false;

    layer.addAnimation(animation, forKey: "transform");

    imageView.layer.mask = layer;

    self.addSubview(imageView);

I am able to see the alpha mask properly, but the animation does not work. Any ideas?

EDIT: I modified the code above and it works! I needed to make the keyPath position.x. See above

回答1:

You do indeed want to use a CALayer - or rather, a CAShapeLayer.

You can create a CAShapeLayer and install it as as the mask on another layer.

You can create a CAAnimation that animates changes to the shape layer's path, or you can animate changes to the layer's strokeStart and/or strokeEnd properties.

If you animate the path, the one rule you want to follow is to make sure that the starting and ending path have the same number and type of control points. Otherwise the animation is "undefined", and the results can be very strange.

I have a development blog post that outlines how it's done:

http://wareto.com/using-core-animation-groups-to-create-animation-sequences-2

It's primarily about using CAAnimationGroups, but it also includes a working example of animating changes to a CAShapeLayer that's used as the mask of an image view's layer.

Below is a GIF of the mask animation that it creates - a "clock wipe" that shows and hides an image view:

Unfortunately it's written in Objective-C, but the Core Animation calls are nearly identical in Swift. Let me know if you have any problems figuring out how to adapt it.

The meat of the animation code is this method:

- (IBAction)doMaskAnimation:(id)sender;
{

  waretoLogoLarge.hidden = FALSE;//Show the image view

  //Create a shape layer that we will use as a mask for the waretoLogoLarge image view
  CAShapeLayer *maskLayer = [CAShapeLayer layer];

  CGFloat maskHeight = waretoLogoLarge.layer.bounds.size.height;
  CGFloat maskWidth = waretoLogoLarge.layer.bounds.size.width;

  CGPoint centerPoint;
  centerPoint = CGPointMake( maskWidth/2, maskHeight/2);

  //Make the radius of our arc large enough to reach into the corners of the image view.
  CGFloat radius = sqrtf(maskWidth * maskWidth + maskHeight * maskHeight)/2;

  //Don't fill the path, but stroke it in black.
  maskLayer.fillColor = [[UIColor clearColor] CGColor];
  maskLayer.strokeColor = [[UIColor blackColor] CGColor];

  maskLayer.lineWidth = radius; //Make the line thick enough to completely fill the circle we're drawing

  CGMutablePathRef arcPath = CGPathCreateMutable();

  //Move to the starting point of the arc so there is no initial line connecting to the arc
  CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y-radius/2);

  //Create an arc at 1/2 our circle radius, with a line thickess of the full circle radius
  CGPathAddArc(arcPath,
               nil,
               centerPoint.x,
               centerPoint.y,
               radius/2,
               3*M_PI/2,
               -M_PI/2,
               YES);

  maskLayer.path = arcPath;

  //Start with an empty mask path (draw 0% of the arc)
  maskLayer.strokeEnd = 0.0;

  CFRelease(arcPath);

  //Install the mask layer into out image view's layer.
  waretoLogoLarge.layer.mask = maskLayer;

  //Set our mask layer's frame to the parent layer's bounds.
  waretoLogoLarge.layer.mask.frame = waretoLogoLarge.layer.bounds;

  //Create an animation that increases the stroke length to 1, then reverses it back to zero.
  CABasicAnimation *swipe = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
  swipe.duration = 2;
  swipe.delegate = self;
  [swipe setValue: theBlock forKey: kAnimationCompletionBlock];

  swipe.timingFunction = [CAMediaTimingFunction 
    functionWithName:kCAMediaTimingFunctionLinear];
  swipe.fillMode = kCAFillModeForwards;
  swipe.removedOnCompletion = NO;
  swipe.autoreverses = YES;

  swipe.toValue = [NSNumber numberWithFloat: 1.0];

  [maskLayer addAnimation: swipe forKey: @"strokeEnd"];
}

I have another blog entry that IS in Swift that shows how to create and animate a pie chart using a CAShapeLayer. That project animates shape, not a mask, but the only real difference is whether you install the shape layer as a regular content layer or as a mask on another layer like the backing layer of an image view.

You can check out that project at this link:

http://wareto.com/swift-piecharts