Friday, 8 July 2011

UIImageView curl shadow effect using shadowPath

In my last post I discussed how to display UIImageView with border and shadow. One important thing to consider while displaying shadow is performance. This can be improved by explicitly specifying shadow path property in CALayer class. According to iOS CALayer class reference documentation "Specifying an explicit path usually improves rendering performance".  If shadowPath property is not defined then the shadow is created using layers composited alpha channel. In this post we will see how to create curl shadow effect using shadowPath property. The output of our image will look like as shown in figure 1.
Figure1: (a) original photo (b) photo with border and curl shadow
As you can see in figure 1 (b) we have added white border of width 3 around original photo. At the bottom left and right of the original image we have added shadow to give curl effect.
  
Implementation
First we extend UIImageView interface with some functions. We create an objective-c class using XCode File new wizard. We name our files UIImageViewBorder.h and UIImageViewBorder.m. In UIImageViewBorder.h file we define our interface as shown below:
#import <Foundation/Foundation.h >
@interface UIImageView (ImageViewBorder)
-(void)setImage:(UIImage*)image borderWidth:(CGFloat)borderWidth shadowDepth:(CGFloat)shadowHeight controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset;
@end
In this interface we define one function which receives original image, border width, depth of shadow and control point offsets. We will discuss these parameters in detail later on.
In UIImageViewBorder.m we first define configureImageViewBorder function. This function draws border and shadow around original image. Please note that I have given complete implementation of UIImageViewBorder.m at the end of this post.
-(void)configureImageViewBorder:(CGFloat)borderWidth shadowDepth:(CGFloat)shadowDepth controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset{
    CALayer* layer = [self layer];
    [layer setBorderWidth:borderWidth];
    [layer setBorderColor:[UIColor whiteColor].CGColor];
    [self setContentMode:UIViewContentModeCenter];
    [layer setShadowColor:[UIColor blackColor].CGColor];
    [layer setShadowOffset:CGSizeMake(0.0, 4.0)];
    [layer setShadowRadius:3.0];
    [layer setShadowOpacity:0.4];
    layer.shadowPath = [self curlShadowPathWithShadowDepth:shadowDepth
                                        controlPointXOffset:controlPointXOffset
                                        controlPointYOffset:controlPointYOffset];
    
}
In this function we first retrieve CALayer of UIImageView. We then set the border width and border color of UIImageView layer. We also set the content mode to center. This property sets the image in center if image size is smaller than the UIImageView. You can change it to other values in order to automatically scale the image to fit UIImageView size. We now set shadow properties of UIImageView layer. First we define shadow color as black. This can be set to any other color if required. We then set shadow offset value to (0.0, 4.0). This value draws shadow at the bottom of the image with no shadow on horizontal axis. The shadow radius defines the blur radius. In this case we have set it to 3.0. We only want to display slight shadow underneath the image. Therefore we have set the shadow opacity value to 0.4. Next we call curlShadowPathWithShadowDepth function to create curl shadow.

Polygon with cubic bezier curve
Before delving into the code we shall discuss how curl shadow is created using Bezier path. Bezier path can be used to create different shapes such as lines, polygons and curves etc. In this scenario we create shadow path which is polygon with cubic curve at the bottom of the image as shown in figure 2.

Figure 2: Curl shadow behind image.
In figure 2 we have shown curl shadow which is drawn behind the image. If we display the image on top of this shadow then the output would be same as shown in figure: 1(b). In order to understand how we can draw shadow path in iOS as shown in figure 2 we will first take a look at similar shadow Path example in image editing tool such as Gimp. Other image editing tools have similar functionalities so you can use any image editing tool you like. In Gimp, Bezier Paths can be drawn using tool called Path tool. You can learn more about creating Bezier paths using Path tool at Gimp's online documentation. Figure 3 shows Bezier path which I have created using Gimp.
Figure 3: Bezier path using Gimp
In figure:3 we have created a polygon with a curve on one side. Following are the steps to draw this polygon.
  1. Move to point (10,10). It is labelled as PolygonTopLeft.
  2. Add a line starting from point (10,10) to point (90,10). It is labelled as PolygonTopRight.
  3. Add a line starting from point (90,10) to point (90,90). The end point is labelled as PolygonBottomRight.
  4. Add a line starting from point (90,90) to point (10,90). The end point is labelled as PolygonBottomLeft.
  5. Close path from point (10,90) to starting point (10,10).
  6. Click on the bottom line between points PolygonBottomLeft and PolygonBottomRight.
  7. Change the location of control points CPoint1 to (70,60) and CPoint2 to (30,60). Cubic curve is created as we change the location of control points.
It is important to understand that the cubic Bezier curve (which is bottom curve in figure 3) uses four points. Start point, end point and two control points. Cubic Bezier curve only passes through start and end points. It normally does not pass through control points as shown in figure 3. These control points are used to control the shape of the curve. If you look closely in figure 3 you will notice that the dashed line passing through PolygonBottomLeft and CPoint2 is tangent to the surface of the curve. Similarly dashed line passing through PolygonBottomRight and CPoint1 is also tangent to the surface of the curve. The shape of the cubic curve changes as we move the position of the control points i.e CPoint1 and CPoint2. If you want to know more about cubic Bezier curve then I will recommend to search Bezier Curve lecture videos available on YouTube. There are few very good lecture videos on this topic delivered by university lecturers. Now we are ready to see how we draw similar shadow path in iOS. We create shadow path in curlShadowPathWithShadowDepth function whose implementation is given below:
-(CGPathRef)curlShadowPathWithShadowDepth:(CGFloat)shadowDepth controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset
{
    
    CGSize viewSize = [self bounds].size;
    CGPoint polyTopLeft = CGPointMake(0.0, controlPointYOffset);
    CGPoint polyTopRight = CGPointMake(viewSize.width, controlPointYOffset);
    CGPoint polyBottomLeft = CGPointMake(0.0, viewSize.height + shadowDepth);
    CGPoint polyBottomRight = CGPointMake(viewSize.width, viewSize.height +  shadowDepth);
    
    CGPoint controlPointLeft = CGPointMake(controlPointXOffset , controlPointYOffset);
    CGPoint controlPointRight = CGPointMake(viewSize.width - controlPointXOffset,  controlPointYOffset);
    
    UIBezierPath* path = [UIBezierPath bezierPath];
    
    [path moveToPoint:polyTopLeft];
    [path addLineToPoint:polyTopRight];
    [path addLineToPoint:polyBottomRight];
    [path addCurveToPoint:polyBottomLeft 
            controlPoint1:controlPointRight
            controlPoint2:controlPointLeft];
    
    [path closePath];  
    return path.CGPath;
}
This function gets three parameters: shadowDepth, controlPointXOffset and controlPointYOffset. ShadowDepth is used to define the shadow area outside of the image which we want to display at the bottom of the image. controlPointXOffset and controlPointYOffset parameters are used to define two control points. After receiving input parameters we first get the bounds of the UIImageView. We then set the four vertices of the polygon which we will use as shadow path. polyTopLeft is set to (0.0, controlPointYOffset). Please note that we are using controlPointYOffset as the Y coordinate of topLeft point. The reason is that in this scenario we place the control point at same height as the polygon. Similarly topRight point is set to (viewSize.width, controlPointYOffset). BottomLeft point is set to (0.0, viewSize.height + shadowDepth).  If you notice we have added shadowDepth in Y coordinate of BottomLeft point. The reason is that the shadow path covers some area at the bottom of the image. Similarly we have set the bottomRight point to (viewSize.width, viewSize.height + shadowDepth).  Now we want to define two control points. First control point is set to (controlPointXOffset, controlPointYOffset) and second control point is set to (viewSize.width - controlPointXOffset, controlPointYOffset). We now use the four vertices and the two control points to define the shadow path. We use UIBezierPath to define polygon and cubic Bezier curve. The process of drawing Bezier path is similar to the process which we have discussed in Figure 3. We first move to topLeft point. We draw a line from topLeft to topRight. We then draw a line from topRight to bottomRight. We draw a curve from bottomRight to bottomLeft using two control points. We then use [path closePath] function to draw line from bottomLeft to topLeft. This process is shown in figure 4.
Figure 4: shadow path drawing
Now we define rescaleImage function. This function scales the image if its size is greater than the image view bounds. We have already discussed this function in our post to create shadow and border around UIImageView.
-(UIImage*)rescaleImage:(UIImage*)image{
    UIImage* scaledImage = image;
    
    CALayer* layer = self.layer;
    CGFloat borderWidth = layer.borderWidth;
    
    //if border is defined 
    if (borderWidth > 0)
    {
        //rectangle in which we want to draw the image.
        CGRect imageRect = CGRectMake(0.0, 0.0, self.bounds.size.width - 2 * borderWidth,self.bounds.size.height - 2 * borderWidth);
        //Only draw image if its size is bigger than the image rect size.
        if (image.size.width > imageRect.size.width || image.size.height > imageRect.size.height)
        {
            UIGraphicsBeginImageContext(imageRect.size);
            [image drawInRect:imageRect];
            scaledImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
        }        
    }
    return scaledImage;
}

All of the above functions are called from setImage function which we define in UIImageViewBorder.h interface file.
-(void)setImage:(UIImage*)image borderWidth:(CGFloat)borderWidth shadowDepth:(CGFloat)shadowDepth controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset
{
    self.backgroundColor = [UIColor lightGrayColor];
    [self configureImageViewBorder:borderWidth shadowDepth:shadowDepth controlPointXOffset:controlPointXOffset controlPointYOffset:controlPointYOffset];
    UIImage* scaledImage = [self rescaleImage:image];
    self.image = scaledImage;
}
This function sets the background color of the UIImageView to gray. This color can be changed to any other color as required. It then calls configureImageViewBorder function to draw border and shadow. It then scales the image if required and sets it to the image property of UIImageView.

How to Use
We are now ready to use UIImageViewBorder.h and UIImageViewBorder.m. Create an application with UIViewController. Add UIImageView control on UIViewController. Create UIImageViewBorder.h and UIImageViewBorder.m files. Copy implementation of these files as given at the end of this post. Now in viewDidLoad you can call this function.
-(void)viewDidLoad{
    UIImage* image = [UIImage imageNamed:@"dog"];
    [imageView setImage:image borderWidth:3.0 shadowDepth:10.0 controlPointXOffset:30.0 controlPointYOffset:70.0];
}
In this particular example I have used UIImageView of size(100,100). I have given shadowDepth to 10.0, controlPointXOffset to 30.0 and controlPointYOffset to 70.0. You might think how can we find suitable control point values. In my experience one way to find is to create an empty image in Gimp or any other image editing tool. Create a polygon as I have shown in Figure 3 and adjust the control point positions to get your desired result.

Note:
I have noticed that the border of UIImageView does not update properly if autoresizingMask property of UIImageView is set to either UIViewAutoresizingFlexibleWidth or UIViewAutoresizingFlexibleHeight or both. In above example I am using default value which is UIViewAutoresizingNone. I have set this value using interface builder. Please feel free to contribute if you know how to fix this problem.

Here is the complete implementation of UIImageViewBorder.h and UIImageViewBorder.m.
//
//  UIImageViewBorder.h
//  CustomTableView
//

#import <Foundation/Foundation.h>

@interface UIImageView (ImageViewBorder)
-(void)setImage:(UIImage*)image borderWidth:(CGFloat)borderWidth shadowDepth:(CGFloat)shadowHeight controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset;
@end

//
//  UIImageViewBorder.m
//  CustomTableView
//
#import "UIImageViewBorder.h"
#import "QuartzCore/QuartzCore.h"

@interface UIImageView (private)
-(UIImage*)rescaleImage:(UIImage*)image;
-(void)setImage:(UIImage*)image withBorderWidth:(CGFloat)borderWidth shadowDepth:(CGFloat)shadowDepth controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset;
-(CGPathRef)curlShadowPathWithShadowDepth:(CGFloat)shadowDepth controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset;

@end

@implementation UIImageView (ImageViewBorder)

-(UIImage*)rescaleImage:(UIImage*)image{
    UIImage* scaledImage = image;
    
    CALayer* layer = self.layer;
    CGFloat borderWidth = layer.borderWidth;
    
    //if border is defined 
    if (borderWidth > 0)
    {
        //rectangle in which we want to draw the image.
        CGRect imageRect = CGRectMake(0.0, 0.0, self.bounds.size.width - 2 * borderWidth,self.bounds.size.height - 2 * borderWidth);
        //Only draw image if its size is bigger than the image rect size.
        if (image.size.width > imageRect.size.width || image.size.height > imageRect.size.height)
        {
            UIGraphicsBeginImageContext(imageRect.size);
            [image drawInRect:imageRect];
            scaledImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
        }        
    }
    return scaledImage;
}

-(void)configureImageViewBorder:(CGFloat)borderWidth shadowDepth:(CGFloat)shadowDepth controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset{
    CALayer* layer = [self layer];
    [layer setBorderWidth:borderWidth];
    [self setContentMode:UIViewContentModeCenter];
    [layer setBorderColor:[UIColor whiteColor].CGColor];
    [layer setShadowColor:[UIColor blackColor].CGColor];
    [layer setShadowOffset:CGSizeMake(0.0, 4.0)];
    [layer setShadowRadius:3.0];
    [layer setShadowOpacity:0.4];
    layer.shadowPath = [self curlShadowPathWithShadowDepth:shadowDepth
                                        controlPointXOffset:controlPointXOffset
                                        controlPointYOffset:controlPointYOffset];
    
}


-(CGPathRef)curlShadowPathWithShadowDepth:(CGFloat)shadowDepth controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset
{
    
    CGSize viewSize = [self bounds].size;
    CGPoint polyTopLeft = CGPointMake(0.0, controlPointYOffset);
    CGPoint polyTopRight = CGPointMake(viewSize.width, controlPointYOffset);
    CGPoint polyBottomLeft = CGPointMake(0.0, viewSize.height + shadowDepth);
    CGPoint polyBottomRight = CGPointMake(viewSize.width, viewSize.height +  shadowDepth);
    
    CGPoint controlPointLeft = CGPointMake(controlPointXOffset , controlPointYOffset);
    CGPoint controlPointRight = CGPointMake(viewSize.width - controlPointXOffset,  controlPointYOffset);
    
    UIBezierPath* path = [UIBezierPath bezierPath];
    
    [path moveToPoint:polyTopLeft];
    [path addLineToPoint:polyTopRight];
    [path addLineToPoint:polyBottomRight];
    [path addCurveToPoint:polyBottomLeft 
            controlPoint1:controlPointRight
            controlPoint2:controlPointLeft];
    
    [path closePath];  
    return path.CGPath;
}


-(void)setImage:(UIImage*)image borderWidth:(CGFloat)borderWidth shadowDepth:(CGFloat)shadowDepth controlPointXOffset:(CGFloat)controlPointXOffset controlPointYOffset:(CGFloat)controlPointYOffset
{
    self.backgroundColor = [UIColor lightGrayColor];
    [self configureImageViewBorder:borderWidth shadowDepth:shadowDepth controlPointXOffset:controlPointXOffset controlPointYOffset:controlPointYOffset];
    UIImage* scaledImage = [self rescaleImage:image];
    self.image = scaledImage;
}

@end

7 comments:

  1. Thanks for sharing. It works well for me.

    ReplyDelete
  2. does it work with rectangular image?

    ReplyDelete
  3. Hi awesome article!

    It worked really well on my emulator, but when I deployed to the device 4S (iOS 5). It crashed on the line where you specify the shadowPath and pass the CGFloat values...i couldn't figure out why :(

    ReplyDelete
  4. [SOLVED] UIImageView curl shadow effect using shadowPath shadowPath crash.

    Download the source code here
    http://iphonesdkpro.com/%5BSOLVED%5D+UIImageView+curl+shadow+effect+using+shadowPath+shadowPath+crash

    ReplyDelete
  5. Fixed shadowPath crash
    Another try at the download url
    http://bit.ly/L729FS

    ReplyDelete