I am trying to animate the mask layer on a UIView.
Basically this code displays the image below:
let bounds: CGRect = self.manualWBMaskView!.bounds
let maskLayer: CAShapeLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.fillColor = UIColor.blackColor().CGColor
let screenWith : CGFloat = UIScreen.mainScreen().bounds.width
let roundedRectFrame : CGRect = CGRectMake(self.manualWBMaskView!.bounds.midX - (screenWith/4), self.manualWBMaskView!.bounds.midY - (screenWith/4), screenWith/2, screenWith/2)
let path: UIBezierPath = UIBezierPath(roundedRect:roundedRectFrame, cornerRadius:10 )
path.appendPath(UIBezierPath(rect: bounds))
maskLayer.path = path.CGPath
maskLayer.fillRule = kCAFillRuleEvenOdd
self.manualWBMaskView!.layer.mask = maskLayer
I want it to animate to that position of the cutout from being full screen to above position:
I tried UIView animation, and no luck. Since I already have a CAShapeLayer, I should be able to animate that?
EDIT**
Here's the animation code I tried, which didn't work:
//Before Animation Make the Mask fill the whole view:
self.manualWBMaskView!.layer.mask = self.manualWBMaskView!.bounds
var duration: NSTimeInterval = 0.8
CATransaction.begin()
CATransaction[kCATransactionAnimationDuration] = Int(duration)
self.manualWBMaskView!.layer.mask = CGRectMake(self.manualWBMaskView!.bounds.midX - (screenWith/4), self.manualWBMaskView!.bounds.midY - (screenWith/4), screenWith/2, screenWith/2)
CATransaction.commit()
As per 'originaluser2's reply below, I have this:
func presentMaskScreenWithAnimation () {
let bounds: CGRect = self.manualWBMaskView!.bounds
let maskLayer: CAShapeLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.fillColor = UIColor.blackColor().CGColor
let screenWith : CGFloat = UIScreen.mainScreen().bounds.width
let roundedRectFrame : CGRect = self.manualWBMaskView.bounds
let path: UIBezierPath = UIBezierPath(roundedRect:roundedRectFrame, cornerRadius:0 )
path.appendPath(UIBezierPath(rect: bounds))
maskLayer.path = path.CGPath
maskLayer.fillRule = kCAFillRuleEvenOdd
self.manualWBMaskView!.layer.mask = maskLayer
// define your new path to animate the mask layer to
let screenWith2 : CGFloat = UIScreen.mainScreen().bounds.width
let roundedRectFrame2 : CGRect = CGRectMake(self.manualWBMaskView!.bounds.midX - (screenWith2/4), self.manualWBMaskView!.bounds.midY - (screenWith2/4), screenWith2/2, screenWith2/2)
let path2 = UIBezierPath(roundedRect:roundedRectFrame2, cornerRadius:10 )
// create new animation
let anim = CABasicAnimation(keyPath: "path")
// from value is the current mask path
anim.fromValue = maskLayer.path
// to value is the new path
anim.toValue = path2.CGPath
// duration of your animation
anim.duration = 5.0
// custom timing function to make it look smooth
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// add animation
maskLayer.addAnimation(anim, forKey: nil)
// update the path property on the mask layer, using a CATransaction to prevent an implicit animation
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.path = path2.CGPath
maskLayer.fillRule = kCAFillRuleEvenOdd
CATransaction.commit()
}
It does the animation; However, I got two issues now: 1. Shape is all off during the animation, yet if come together at the end of the animation. (Screenshot below)
CABasicAnimation
on the path
property of your mask layerThis will allow you to animate your mask from a given path, to another given path. Core Animation will automatically interpolate the intermediate steps for you.
Something like this should do the trick:
// define your new path to animate the mask layer to
let path = UIBezierPath(roundedRect: CGRectInset(view.bounds, 100, 100), cornerRadius: 20.0)
path.appendPath(UIBezierPath(rect: view.bounds))
// create new animation
let anim = CABasicAnimation(keyPath: "path")
// from value is the current mask path
anim.fromValue = maskLayer.path
// to value is the new path
anim.toValue = path.CGPath
// duration of your animation
anim.duration = 5.0
// custom timing function to make it look smooth
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// add animation
maskLayer.addAnimation(anim, forKey: nil)
// update the path property on the mask layer, using a CATransaction to prevent an implicit animation
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.path = path.CGPath
CATransaction.commit()
Result:
One thing to note with this is that there appears to be a bug in Core Animation, where it's unable to correctly animate from a path that includes rectangle, to a path that includes a rounded rect with a corner radius. I have filed a bug report with Apple for this.
There is a simple fix for this, which is to use a rounded rect with a corner radius of negligible value on the original path (0.0001 works okay for me). A bit hacky – but you can't notice the difference when using the app.
Unfortunately, Apple considers this to be "expected behaviour". They said the following:
For the animation to render correctly, the animated paths should have the same number of elements/segments (ideally of the same type). Otherwise, there is no reasonable way to interpolate between the two.
For instance, if a first path has four elements and the second path has six elements, we can easily create the first four points by averaging the first four start and end points with a weight for current time, but it's impossible to come up with a general purpose way to calculate the intermediate positions for the fifth and sixth points. This is why the animation works just fine for corner radius 0.001 (the same number of elements), but fails for plain rectangular path (different number of elements).
I personally disagree, if you specify a rounded rect of corner radius 0 and then animate to a rounded rect with a corner radius > 0 – an incorrect animation is still obtained, even though both paths should have the "same number of elements". I suspect the reason this doesn't work is the internal implementation checks for a radius of 0, and tries to optimise by using a rectangular path with 4 elements instead, which is why it doesn't work.
I will submit another bug report explaining this when I get the time.
You're force unwrapping your manualWBMaskView
quite a bit. Don't. You need to learn how to properly deal with optionals. Every time you force unwrap something - your app could crash.
You also have a few lines like this:
let maskLayer: CAShapeLayer = CAShapeLayer()
The : CAShapeLayer
here is not only unnecessary - but goes against good practise. You should always let Swift infer the type of a value where it can.