nazbot
nazbot

Reputation: 1697

CALayer flattening sublayers

I am creating a UI where we have a deck of cards that you can swipe off the screen.

What I had hoped to be able to do was create a subclass of UIView which would represent each card and then to modify the transform property to move them back (z-axis) and a little up (y-axis) to get the look of a deck of cards.

Reading up on it I found I needed to use a CATransformLayer instead of the normal CALayer in order for the z-axis to not get flattened. I prototyped this by creating a CATransformLayer which I added to the CardDeckView's layer, and then all my cards are added to that CATransformLayer. The code looks a little bit like this:

In init:

// Initialize the CATransformSublayer
_rootTransformLayer = [self constructRootTransformLayer];
[self.layer addSublayer:_rootTransformLayer];

constructRootTransformLayer (the angle method is redundant, was going to angle the deck but later decided not to):

    CATransformLayer* transformLayer = [CATransformLayer layer];
transformLayer.frame = self.bounds;

// Angle the transform layer so we an see all of the cards
CATransform3D rootRotateTransform =  [self transformWithZRotation:0.0];
transformLayer.transform = rootRotateTransform;
return transformLayer;

Then the code to add the cards looks like:

// Set up a CardView as a wrapper for the contentView
RVCardView* cardView = [[RVCardView alloc] initWithContentView:contentView];

cardView.layer.cornerRadius = 6.0;
if (cardView != nil) {
    [_cardArray addObject:cardView];
    //[self addSubview:cardView];
    [_rootTransformLayer addSublayer:cardView.layer];
    [self setNeedsLayout];
}

Note that what I originally wanted was to simply add the RVCardView directly as a subview - I want to preserve touch events which adding just the layer doesn't do. Unfortunately what ends up happening is the following:

Flattened Cards

If I add the cards to the rootTransformLayer I end up with the right look which is:

enter image description here

Note that I tried using the layerClass on the root view (CardDeckView) which looks like this:

+ (Class) layerClass
{
    return [CATransformLayer class];
}

I've confirmed that the root layer type is now CATransformLayer but I still get the flattened look. What else do I need to do in order to prevent the flattening?

Upvotes: 0

Views: 1168

Answers (1)

Pietro Saccardi
Pietro Saccardi

Reputation: 2622

When you use views, you see a flat scene because there is no perspective set in place. To make a comparison with 3D graphics, like OpenGL, in order to render a scene you must set the camera matrix, the one that transforms the 3D world into a 2D image.
This is the same: sublayers content are transformed using CATransform3D in 3D space but then, when the parent CALayer displays them, by default it projects them on x and y ignoring the z coordinate.

See Adding Perspective to Your Animations on Apple documentation. This is the code you are missing:

CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / eyePosition; // ...on the z axis

myParentDeckView.layer.sublayerTransform = perspective;

Note that for this, you don't need to use CATransformLayer, a simple CALayer would suffice:

CALayer can handle 3D projections

here is the transformation applied to the subviews in the picture (eyePosition = -0.1):

// (from ViewController-viewDidLoad)
for (UIView *v in self.view.subviews) {
    CGFloat dz = (float)(arc4random() % self.view.subviews.count);
    CATransform3D t = CATransform3DRotate(CATransform3DMakeTranslation(0.f, 0.f, dz),
                                          0.02,
                                          1.0, 0.0, 0.0);
    v.layer.transform = t;
}

The reason for using CATransformLayer is pointed out in this question. CALayer "rasterizes" its transformed sublayers and then applies its own transformation, while CATransformLayer preserves the full hierarchy and draws each sublayer independently; it is useful only if you have more than one level of 3D-transformed sublayers. In your case, the scene tree has only one level: the deck view (which itself has the identity matrix as transformation) and the card views, the children (which are instead moved in the 3D space). So CATransformLayer is superfluous in this case.

Upvotes: 5

Related Questions