UIView Layer Mask Animate
Asked Answered
G

1

12

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

enter image description here

I want it to animate to that position of the cutout from being full screen to above position:

enter image description here

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)

  1. Mask is inverted. It 's visible inside the shape, but transparent around it. I need the opposite. Inside the shape being transparent.

enter image description here

Galatia answered 6/4, 2016 at 18:32 Comment(4)
Where is your animation code?Sausa
I apologize. Updated the OP, with the code I tried.Galatia
Why do you keep on force unwrapping manualWBMaskView? You're just asking for a crash. Please learn how to properly deal with optionals.Mimicry
I admit, I am not an expert in that. However, Xcode kept forcing me to the enter the '!'Galatia
M
31

You'll want to use a CABasicAnimation on the path property of your mask layer

This 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:

enter image description here

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.


Bug report response from Apple

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.


A couple of other things...

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.

Mimicry answered 6/4, 2016 at 19:59 Comment(11)
That seems to work. Looks like I am almost there. Couple of minor issues on that animation. I updated the original post.Galatia
@gizmodo I have edited my answer. The key points are firstly you need to append a path of your view's bounds to the path you're animating to in order to get the inverted effect. Secondly, you need to change your original path to use a corner radius of 0.0001 - as there appears to be a bug in Core Animation where it can't properly animate from a rect to a rounded rect.Mimicry
Works PERFECTLY! I just had to add following to make it stop from disappearing: anim.fillMode = kCAFillModeForwards and anim.removedOnCompletion = falseGalatia
@gizmodo no! Please don't do that. When is the animation disappearing? It's working fine for me. You shouldn't use removedOnCompletion or fillMode to persist animations, as its best to keep the model layer and presentation layer in sync. Try removing the CATransaction wrapper - and move the maskLayer.path = path.CGPath to before you add the animation.Mimicry
Also, I have submitted a bug report to Apple regarding the rect to rounded rect animation bug.Mimicry
Animation was disappearing upon completion.Galatia
Moving maskLayer.path = path.CGPath to before animation didn't work :(Galatia
Let us continue this discussion in chat.Mimicry
@gizmodo If you're interested, I had a response from Apple regarding the animation from rect to rounded rect bug. I have updated my post with it.Mimicry
Interesting response. Noted!Galatia
I can't get it to work on a CGMutablePath that creates an arc, going from a small radius to a larger one.Poe

© 2022 - 2024 — McMap. All rights reserved.