Sunday 8 May 2011

iPhone - Custom Alert View

Recently while developing an iPhone application I  needed to display a message box which is similar to UIAlertView but which could be modified by adding different objects like Activity indicator. I also wanted to replicate animation of UIAlertView. I created my custom alert view which I would like to post here. I hope some of the techniques I have used here could be useful to other developers. You can download code from github repository. Please note that I have taken some ideas from Brad Larson's iTunes lecture: Quartz 2D drawing. Here is how the Custom alert view looks like.
CustomAlertView class :
First we need to create a CustomAlertView class in our project. This class is derived from UIView class.

Implement init method
Init method will receive frame and the text which needs to be displayed.
-(id)initWithFrame:(CGRect)frame withText:(NSString*)text{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor clearColor];
        [self initTextLabel:text];
        [self initActivityIndicator];
    }
    return self;
}
We call initTextLabel function which creates label and adjusts its position. The second function initActivityIndicator adjusts activity indicator control.


How to Draw Rounded Rectangle
We define a function which draws a rounded rectangle path around an area given in parameter rect. We call this function from other functions to draw gray and white border rectangles.
-(void)drawRoundedRectangleWithContext:(CGContextRef)context rect:(CGRect)rect radius:(CGFloat)radius{
    //process only if radius is less than half of size and width.
    if (radius >= rect.size.width /2 || radius >= rect.size.height/2)
        return;
    
    CGFloat minx = CGRectGetMinX(rect), midx = CGRectGetMidX(rect), maxx = CGRectGetMaxX(rect);
    CGFloat miny = CGRectGetMinY(rect), midy = CGRectGetMidY(rect), maxy = CGRectGetMaxY(rect);
    
    CGContextMoveToPoint(context, minx, midy);
    CGContextAddArcToPoint(context, minx, miny, midx, miny, radius);
    CGContextAddArcToPoint(context, maxx, miny, maxx, midy, radius);
    CGContextAddArcToPoint(context, maxx, maxy, midx, maxy, radius);
    CGContextAddArcToPoint(context, minx, maxy, minx, midy, radius);
}
Implement DrawRect method:
If you notice our custom alert view has a thick white border. This thick white border is covered by gray colour from outside. The main area of the rectangle is filled by blue color.We also apply gloss and a gradient to give a 3D look to our control.
-(void)drawRect:(CGRect)rect{
    CGRect currentBounds = self.bounds;
    CGRect actualViewRect;
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    actualViewRect = CGRectInset(currentBounds, 0.5, 0.5);
    CGContextSaveGState(context);
    //Draw gray border around rectangle.
    [self drawOuterBorderWithContext:context rect:actualViewRect];
    CGContextRestoreGState(context);
    //Fill Rect with thick border.    
    CGContextSaveGState(context);
    actualViewRect = CGRectInset(actualViewRect, 1.0, 1.0);
    [self drawRectAndInnerBorderWithContext:context rect:actualViewRect];
    CGContextRestoreGState(context);
    //Gloss clipping path.
    [self glossPathClipWithContext:context rect:actualViewRect];
    //Gloss radius.
    [self glossRadiusPathClipWithContext:context];
    //Draw gloss gradient.
    [self drawGlossGradientWithContext:context];
}
In this function after getting the bounds of view, we call CGRectInset(currentBounds, 0.5, 0.5) to get a rectangle which is 0.5 points less than original rectangle. We use this rectangle to draw a gray border by calling [self drawOuterBorderWithContext:context rect:actualViewRect]. Similarly in order to draw a white rectangle border with blue colour we get a rectangle which is 1.0 less in size of original rectangle. We perform this operation by calling [self drawRectAndInnerBorderWithContext:context rect:actualViewRect]. Now we want to apply a gloss and a gradient on our inner rectangle. To perform this we call functions:
//Gloss clipping path.
[self glossPathClipWithContext:context rect:actualViewRect];
//Gloss radius.
[self glossRadiusPathClipWithContext:context];
//Draw gloss gradient.
[self drawGlossGradientWithContext:context];
Draw inner and outer rectangles
As mentioned above gray border is drawn by drawOuterBorderWithContext function.
-(void)drawOuterBorderWithContext:(CGContextRef)context rect:(CGRect)rect{
    CGContextSetLineWidth(context, 1.0);
    CGContextSetStrokeColorWithColor(context, [CustomAlertView grayColor]);
    CGContextBeginPath(context);
    [self drawRoundedRectangleWithContext:context rect:rect];
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathStroke);
}
This function sets line width to 1.0 and stroke to grayColor. It then calls drawRoundedRectangleWithContext function to draw rounded rectangle. Similarly we draw white border and fill blue colour inside our rectangle.
-(void)drawRectAndInnerBorderWithContext:(CGContextRef)context rect:(CGRect)rect{
    //Draw blue background color with white border.
    CGContextSetLineWidth(context, 2.0);
    CGContextSetStrokeColorWithColor(context, [CustomAlertView whiteColor]);
    CGContextSetFillColorWithColor(context, [CustomAlertView translucentBlueColor]);
    CGContextBeginPath(context);
    [self drawRoundedRectangleWithContext:context rect:rect];
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFillStroke);
}
Gloss
In order to apply gloss we first need to calculate clipping path. If we do not define clipping path then gloss would also be applied at the corners of rectangle which is not desirable in case of rounded rectangles.
-(void)glossPathClipWithContext:(CGContextRef)context rect:(CGRect)rect{
    //Gloss clipping path.
    CGContextBeginPath(context);
    [self drawRoundedRectangleWithContext:context rect:rect];
    CGContextClosePath(context);
    CGContextClip(context);
}
In our case clipping path is simple i.e we want to apply gloss only in inner rectangle. This is achieved by passing inner rectangle area to above function. After drawing inner rectangle path in the context we call CGContextClip(context). This clips our inner rectangle.
The next function is glossRadiusPathClipWithContext. This function draws gloss in inner rectangle.
-(void)glossRadiusPathClipWithContext:(CGContextRef)context{
    CGRect currentBounds = self.bounds;
    CGFloat glossRadius = currentBounds.size.width * 2;
    CGPoint glossPoint = CGPointMake(CGRectGetMidX(currentBounds), GLOSS_HEIGHT - glossRadius);
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, glossPoint.x, glossPoint.y);
    CGContextAddArc(context, glossPoint.x, glossPoint.y, glossRadius, 0.0, M_PI, 0);
    CGContextClosePath(context);
    CGContextClip(context);
}
In this function we first calculate the glossRadius. This radius is twice the width of custom alert view. The gloss radius does not necessarily need to be twice of the width of custom alert view. The only requirement is that its value should be big enough to draw a gloss covering the width of alert view.Next we calculate glossPoint. This is the point from where we will draw our arc. The x-axis of this point is mid of inner rectangle where as the y-axis is GLOSS_HEIGHT - glossRadius. GLOSS_HEIGHT is a constant and its value is defined as 30.0. If glossRadius is greater than GLOSS_HEIGHT (which is true in our case) then y-axis value would be negative which means arc point would reside out of our inner rectangle. The idea is that when we draw our arc by calling function: CGContextAddArc(context, glossPoint.x, glossPoint.y, glossRadius, 0.0, M_PI, 0). It produces a big arc which starts from outside of visible rectangle and only some portion of this arc is visible on our alert view.

Display Custom Alert view
If you notice when UIAlertView is displayed it produces some animation. We would like to replicate this in our custom alert view. We do this in show function.
-(void)show{
   self.transform = CGAffineTransformMakeScale(0.01, 0.01);
   [self animateView];
}
First we scale down our view to a 0.01 and 0.01 and then we scale it up to bigger size. This gives the impression that view appears from centre. 
-(void)animateView{
 [UIView animateWithDuration:0.15 delay:0
         options:UIViewAnimationCurveEaseInOut
  animations:^{self.transform = CGAffineTransformMakeScale(1.2, 1.2);} 
  completion:^(BOOL finished){[self animateViewScaleDown];}];
}

-(void)animateViewScaleDown{
 [UIView animateWithDuration:0.1 delay:0 
        options:UIViewAnimationCurveEaseInOut
 animations:^{self.transform = CGAffineTransformMakeScale(0.9, 0.9);} 
 completion:^(BOOL finished){ [self animateViewScaleUp];}];
}

-(void)animateViewScaleUp{
 [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationCurveEaseInOut
 animations:^{self.transform = CGAffineTransformMakeScale(1.0, 1.0);} 
 completion:^(BOOL finished){ }];
}
We first scale it up to (1.2, 1.2), then scale it down to (0.9,0.9) and then to its original size i.e (1.0,1.0). This produces an effect of elasticity which you normally see in alert view.
I have attached source code with this example. Please feel free to use or modify it.

No comments:

Post a Comment