Search code examples
iosswiftuikitcore-animationcalayer

CALayers: A) Can I draw directly on them and B) How to make a width adjustment on a superlayer affect a sublayer


So my goal is to make a sort of sliding door animation in response to a swipe gesture. You can see a GIF of my current animation here (ignore the fact that the gesture behaves opposite to what you'd expect).

Here's how I'm currently accomplishing this: I have a subclass of UIView I'm calling DoorView. DoorView has three CALayers: the base superlayer that comes with every UIView; a sublayer called doorLayer which is the white rectangle that slides; and another sublayer called frameLayer which is the "doorframe" (the black border around doorLayer). The doorLayer and the frameLayer have their own separate animations that are triggered in sequence.

Here's what I need to add to DoorView: a simple rectangle that represents a door handle. At the moment I don't plan to give the door handle its own animation. Instead, I want it to simply be "attached" to the doorLayer so that it animates along with any animations applied to doorLayer.

This is where my first question comes in: I know that I can add another layer (let's call it handleLayer) and add it as a sublayer to doorLayer. But is there a way to simply "draw" a small rectangle on doorLayer without needing an extra layer? And if so, is this preferable for any reason?

Now for my second question: so at the moment I am in fact using a separate layer called handleLayer which is added as a sublayer to doorLayer. You can see a GIF of the animation with the handleLayer here.

And here is the animation being applied to doorLayer:

UIView.animateWithDuration(1.0, animations: { () -> Void in
            self.doorLayer.frame.origin.x = self.doorLayer.frame.maxX
            self.doorLayer.frame.size.width = 0
}

This animation shifts the origin of doorLayer's frame to the door's right border while decrementing its width, resulting in the the appearance of a door sliding to the right and disappearing as it does so.

As you can see in the above GIF, the origin shift of doorLayer is applied to its handleLayer sublayer, as desired. But the width adjustment does not carry over to the handleLayer. And this is good, because I don't want the handle to be getting narrower at the same rate as the doorLayer.

Instead what is desired is that the handleLayer moves with the doorLayer, but retains its size. But when the doorLayer disappears into the right side of the doorframe, the handle disappears with it (as it would look with a normal door). Any clue what the best way to accomplish this is?

Currently in my doorLayer's animation, I added this line:

if self.doorLayer.frame.size.width <= self.handleLayer.frame.size.width {
                self.handleLayer.frame.size.width = 0
}

But that results in this, which isn't quite right.

Thanks for any help!


Solution

  • From a high level, you would need to

    • Make your sliding layer a child of your outline layer
    • Make your outline layer masks its bounds
    • Animate your sliding layer's transform using a x translation
    • On completion of the animation, animate your outline layer's transform using a scale translation
    • Reverse the animations to close the door again
    • Your doorknob layer is fine as is and no need to animate it separately.

    I took a shot at it for fun and here's what I came up with. I didn't use a swipe gesture, but it could just as easily by added. I trigger the animation with a tap on the view. Tap again to toggle back.

    func didTapView(gesture:UITapGestureRecognizer) {
    
        // Create a couple of closures to perform the animations. Each
        // closure takes a completion block as a parameter. This will
        // be used as the completion block for the Core Animation transaction's
        // completion block.
    
        let slideAnimation = {
            (completion:(() -> ())?) in
            CATransaction.begin()
            CATransaction.setCompletionBlock(completion)
            CATransaction.setAnimationDuration(1.0)
            if CATransform3DIsIdentity(self.slideLayer.transform) {
                self.slideLayer.transform = CATransform3DMakeTranslation(220.0, 0.0, 0.0)
            } else {
                self.slideLayer.transform = CATransform3DIdentity
            }
            CATransaction.commit()
        }
    
        let scaleAnimation = {
            (completion:(() -> ())?) in
            CATransaction.begin()
            CATransaction.setCompletionBlock(completion)
            CATransaction.setAnimationDuration(1.0)
            if CATransform3DIsIdentity(self.baseLayer.transform) {
                self.baseLayer.transform = CATransform3DMakeScale(2.0, 2.0, 2.0)
            } else {
                self.baseLayer.transform = CATransform3DIdentity
            }
            CATransaction.commit()
        }
    
        // Check to see if the slide layer's transform is the identity transform
        // which would mean that the door is currently closed.
        if CATransform3DIsIdentity(self.slideLayer.transform) {
            // If the door is closed, perform the slide animation first
            slideAnimation( {
                // And when it completes, perform the scale animation
                scaleAnimation(nil) // Pass nil here since we're done animating
            } )
        } else {
            // Otherwise the door is open, so perform the scale (down)
            // animation first
            scaleAnimation( {
                // And when it completes, perform the slide animation
                slideAnimation(nil) // Pass nil here since we're done animating
            })
        }
    }
    

    Here's how the layers are setup initially:

    func addLayers() {
        baseLayer = CALayer()
        baseLayer.borderWidth = 10.0
        baseLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 220, height: 500.0)
        baseLayer.masksToBounds = true
        baseLayer.position = self.view.center
    
        slideLayer = CALayer()
        slideLayer.bounds = baseLayer.bounds
        slideLayer.backgroundColor = UIColor.whiteColor().CGColor
        slideLayer.position = CGPoint(x: baseLayer.bounds.size.width / 2.0, y: baseLayer.bounds.size.height / 2.0)
    
        let knobLayer = CALayer()
        knobLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 20.0, height: 20.0)
        knobLayer.cornerRadius = 10.0 // Corner radius with half the size of the width and height make it round
        knobLayer.backgroundColor = UIColor.blueColor().CGColor
        knobLayer.position = CGPoint(x: 30.0, y: slideLayer.bounds.size.height / 2.0)
    
        slideLayer.addSublayer(knobLayer)
    
        baseLayer.addSublayer(slideLayer)
    
        self.view.layer.addSublayer(baseLayer)
    }
    

    And here's what the animation looks like:

    Door Animation

    You can see a full Xcode project here: https://github.com/perlmunger/Door