Thursday, June 23, 2011

Dynamic view reflection using CAReplicatorLayer



If you've done UIView reflection on iOS before, you've probably used a method that creates an image from the current view and redraws it with a gradient underneath the view. For many applications this is sufficient, but it has problems: it's slow, and for some objects (e.g. UIWebView) you're really not sure when the object is done drawing so you have to tweak it with arbitrary timers to create the reflection after the object is done rendering. Yuk.

An alternative is to use a CAReplicatorLayer (It was mentioned in a WWDC session this year). The CAReplicatorLayer lets you create copies of your main layer that update in real time. You specify the number of layers and then you specify their offset attributes from the primary layer.

Below is some code that uses the CAReplicatorLayer to reflect a view. The important stuff in this example is in the view's "layoutSubviews" method. Also note that to create a CAReplicatorLayer for your subview, you've got to override the subview's "layerClass" method as shown here.

Try it with a UIWebView or other scrollable, dynamically updating subview - it's pretty cool :)

UPDATE: In the interest of completeness, I've reworked some of the code to be a little more usable...



#import "Reflector.h"
#import <QuartzCore/QuartzCore.h>



@interface MySubview : UIWebView
@end

@implementation MySubview

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

@end


@implementation Reflector

MySubview *subview;
CAGradientLayer *gl=nil;
BOOL hasReflector;

- (void)setup
{
        hasReflector = YES;
        
        [self setBackgroundColor:[UIColor blackColor]];
        subview = [[MySubview alloc] initWithFrame:self.bounds];
        [self addSubview:subview];
        [subview setBackgroundColor:[UIColor clearColor]];
        
        // . . . do whatever with the subview to create content
        [subview setScalesPageToFit:YES];
        [subview loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.flickr.com"]]];

        
}

- (void)layoutSubviews
{
        
        if (hasReflector){
                int gap = 4; // gap between the subview and its reflection
                
                float h1 = ceil(self.frame.size.height *.6);         // subview height
                float h2 = self.frame.size.height - (h1 + gap);        // reflection height
                                
                // size the subview to make space for the reflection
                [subview setFrame:CGRectMake(0, 0, self.frame.size.width, h1)];
                
                // since the replicated layers will be sized using a scale
                // transform, we need to translate our absolute heights into
                // a scalar.
                double scale = (h2/h1);
                
                // configure the subviews replicator layer.  just two instances - the first is the
                // "real-life" rendering of the subview, the 2nd is the reflection
                CAReplicatorLayer *l = (CAReplicatorLayer *) subview.layer;
                l.instanceCount = 2;
                
                // position the instance transform.  the reflection instance will be
                // scaled by "scale" and is centered within the space of the original
                // instance, thus we compute "delta" by taking the original height, 
                // subtracting out the reflection layer size, and then dividing by half.
        
                
                double delta = (h1 - h2) / 2.0 ;
                CATransform3D t = CATransform3DMakeTranslation(0, (h1+gap)-delta, 0);
                t = CATransform3DRotate(t, M_PI, 1, 0, 0);
                t = CATransform3DScale(t, 1, scale, 1);
                
                l.instanceTransform = t;
                
                if (gl == nil){
                        // add a black gradient layer
                        gl = [CAGradientLayer layer];
                        CGColorRef c1 = [[UIColor colorWithRed:0 green:0 blue:0 alpha:.5] CGColor];
                        CGColorRef c2 = [[UIColor colorWithRed:0 green:0 blue:0 alpha:1] CGColor];
                        [gl setColors:[NSArray arrayWithObjects:(id)c1, (id)c2, nil]];
                        
                        [self.layer addSublayer:gl];
                }
                
                // position the gradient layer over the replication layer 2nd instance
                [gl setAnchorPoint:CGPointMake(0, 0)];
                [gl setFrame:CGRectMake(0, h1 + gap, self.frame.size.width, h2)];
                
        }
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
        self = [super initWithCoder:aDecoder];
        if (self){
                [self setup];
        }
        return self;
}
- (void)dealloc {
    [super dealloc];
}


@end

4 comments:

  1. Thanks, Interesting tutorial (Wasnt aware of CAReplicatorLayer), but the code seems riddled with errors? Figured it out mostly for myself, but probably needs some work!

    Might also be nicer (And more useful) if instead of a black gradient overlay mask, you apply the gradient to the mask property instead, so that you can overlay i onto any coloured/textured background.

    ReplyDelete
  2. c.b. - thanks for the comments - yes, the original post was somewhat thrown together as I was in the midst of some other things - I've updated the code today.

    With regards to applying a gradient layer to the mask property ... I'm not really sure how that would work since there's no way to directly access a CAReplicatorLayer instance and we'd only want to apply it to the second one. Adding it to the top (like I do with the CAGradientLayer) wouldn't seem to work either since you'd be masking out the gradient layer and not what's underneath...

    ReplyDelete
  3. something like this seems to work - it won't plug directly into your code as I hacked it around a bit... The anchor/frame will need tweaking


    if (gl == nil)
    {
    // add a black gradient layer
    gl = [CAGradientLayer layer];
    CGColorRef c1 = [[UIColor colorWithRed:0 green:0 blue:0 alpha:1] CGColor];
    CGColorRef c2 = [[UIColor colorWithRed:0 green:0 blue:0 alpha:1] CGColor];
    CGColorRef c3 = [[UIColor colorWithRed:0 green:0 blue:0 alpha:0.4] CGColor];
    CGColorRef c4 = [[UIColor colorWithRed:0 green:0 blue:0 alpha:0] CGColor];
    [gl setColors:[NSArray arrayWithObjects:(id)c1, (id)c2, (id)c3, (id)c4, nil]];

    [gl setLocations:[NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0], [NSNumber numberWithFloat:0.5], [NSNumber numberWithFloat:0.51], [NSNumber numberWithFloat:0.65],nil]];

    self.layer.mask = gl;
    }

    // position the gradient layer over the replication layer 2nd instance
    [gl setAnchorPoint:CGPointMake(0, 0)];
    [gl setFrame:CGRectMake(0, 0, self.frame.size.width, h1+h1)];

    This sets the top half of the gradient to be solid - no alpha, for the top part of the image, and then fades out from half way down. for the reflection.

    ReplyDelete
  4. Also not sure you need to scale the reflection in the Y axis - Seems to break the laws of physics!

    ReplyDelete