miércoles, 15 de febrero de 2012

Mimic iPad system keyboard with Core Graphics

Since iOS 3.2, UIResponder class has a property called inputView. This UIView will be shown like the keyboard when the responder becomes first responder. Using this property we can make our own custom keyboards, allowing for specialized input in our apps like Numbers for iPad:


But, how to mimic system keyboard appearance to avoid crap like the following?


First, draw the keyboard background gradient. Subclass UIView with your own class (JACustomKeyboard) and use this drawRect code:

- (void)drawRect:(CGRect)rect
{
    // Drawing code
    CGContextRef context=UIGraphicsGetCurrentContext();
    
    CGContextSaveGState(context);
    
    UIColor *ucolor1=[UIColor colorWithRed:157.0/255.0 green:157.0/255.0 blue:167.0/255 alpha:1];
    UIColor *ucolor2=[UIColor colorWithRed:67.0/255.0 green:67.0/255.0 blue:75.0/255 alpha:1];
    
    CGColorRef color1=ucolor1.CGColor;
    CGColorRef color2=ucolor2.CGColor;
    
    CGGradientRef gradient;
    CGFloat locations[2] = { 0.0, 1.0 };
    NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    CGRect currentBounds = self.bounds;
    CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
    CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
    
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    
    ucolor1=[UIColor colorWithRed:50.0/255.0 green:50.0/255.0 blue:50.0/255 alpha:1];
    ucolor2=[UIColor colorWithRed:191.0/255.0 green:191.0/255.0 blue:191.0/255 alpha:1];
    
    [ucolor1 setFill];
    CGContextFillRect(context, CGRectMake(0, 0, rect.size.width, 1));
    [ucolor2 setFill];
    CGContextFillRect(context, CGRectMake(0, 1, rect.size.width, 1));
    
    CGContextRestoreGState(context);
}

Yo may have noticed there are two calls to CGContextFillRect in addition to CGContextDrawLinearGradient. With those we are drawing the upper border of the keyboard as seen on the system one:


To get our keys, we will subclass UIControl with a class named JACustomKey declared as follows:

@interface ANCustomKey : UIControl

@property (nonatomic, strong) NSString *text;
@property (nonatomic, strong) UIImage *image;

@end

We will use text and image properties to set the the symbol for the key.

First, use CoreAnimation to drop the key shadow:

-(id)initWithFrame:(CGRect)frame{
    self=[super initWithFrame:frame];
    if (self) {
        self.layer.shadowOffset=CGSizeMake(0, 1);
        self.layer.shadowRadius=1;
        self.layer.shadowOpacity=0.7;
        self.layer.shadowColor=[UIColor blackColor].CGColor;
        self.layer.shadowPath=[self newPathForRoundedRect:self.bounds radius:7];
        self.text=@"A";
    }
    return self;
}

Next, we add some easy common stuff:

-(void)setImage:(UIImage *)image{
    _text=nil;
    if (image!=_image) {
        _image=image;
    }
    [self setNeedsDisplay];
}

-(void)setText:(NSString *)text{
    _image=nil;
    if (text!=_text) {
        _text=text;
    }
    [self setNeedsDisplay];
}

-(void)setHighlighted:(BOOL)highlighted{
    [super setHighlighted:highlighted];
    [self setNeedsDisplay];
}

-(void)setSelected:(BOOL)selected{
    [super setSelected:selected];
    [self setNeedsDisplay];
}

-(void)setEnabled:(BOOL)enabled{
    [super setEnabled:enabled];
    [self setNeedsDisplay];
}

-(BOOL)isOpaque{
    return NO;
}

-(UIColor *)backgroundColor{
    return [UIColor clearColor];
}

Now, we are ready to get into Core Graphics to draw our beautiful system-like key. In a first approach, we should draw a background gradient, a top inner light shadow, a bottom inner dark shadow and a border stroke:

-(void)drawRect:(CGRect)rect{
    CGContextRef context=UIGraphicsGetCurrentContext();
    
    CGContextSaveGState(context);
    
    CGPathRef keyBorderPath=[self newPathForRoundedRect:CGRectInset(rect, 0.5, 0.5) radius:7];
    
    
    //gradient
    CGContextSaveGState(context);
    UIColor *ucolor1=[UIColor colorWithRed:239.0/255.0 green:239.0/255.0 blue:241.0/255 alpha:1];
    
    UIColor *ucolor2=[UIColor colorWithRed:211.0/255.0 green:211.0/255.0 blue:217.0/255 alpha:1];
    
    
    if (self.state & (UIControlStateHighlighted | UIControlStateSelected)) {
        ucolor1=[UIColor colorWithRed:179.0/255.0 green:179.0/255.0 blue:187.0/255 alpha:1];
        ucolor2=[UIColor colorWithRed:130.0/255.0 green:130.0/255.0 blue:140.0/255 alpha:1];
    }
    
    CGColorRef color1=ucolor1.CGColor;
    CGColorRef color2=ucolor2.CGColor;
    
    CGGradientRef gradient;
    CGFloat locations[2] = { 0.0, 1.0 };
    NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    CGRect currentBounds = self.bounds;
    CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
    CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);
    
    //bottom inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, -1), 1, [UIColor colorWithWhite:0 alpha:0.5].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //top inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 0, [UIColor colorWithWhite:1 alpha:1].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //border
    CGContextSetLineWidth(context, 1);
    [[UIColor colorWithWhite:0.0 alpha:0.4] setStroke];
    CGContextAddPath(context, keyBorderPath);
    CGContextStrokePath(context);

    CGPathRelease(keyBorderPath);

    CGContextRestoreGState(context);
}

Note that if the key is selected or highlighted, the colors for the gradient change to darker variants. Also, our key border CGPath is created using an external function to create a rounded rect. And this is what we get with this piece of code:


Pretty, but a problem arises: keys at the top look darker that ones at the bottom due to keyboard background gradient... the system keyboard does not look like this. See:



To solve this issue, we should add an additional global gradient to make bottom keys darker than ones at the top. Remember to change the existing gradient colors to a lighter ones as the new adding gradient will darken them:

-(void)drawRect:(CGRect)rect{
    CGContextRef context=UIGraphicsGetCurrentContext();
    
    CGContextSaveGState(context);
    
    CGPathRef keyBorderPath=[self newPathForRoundedRect:CGRectInset(rect, 0.5, 0.5) radius:7];
    
    
    //gradient
    CGContextSaveGState(context);
    UIColor *ucolor1=[UIColor colorWithRed:241.0/255.0 green:241.0/255.0 blue:243.0/255 alpha:1];
    UIColor *ucolor2=[UIColor colorWithRed:223.0/255.0 green:223.0/255.0 blue:231.0/255 alpha:1];
    
    
    if (self.state & (UIControlStateHighlighted | UIControlStateSelected)) {
        ucolor1=[UIColor colorWithRed:179.0/255.0 green:179.0/255.0 blue:187.0/255 alpha:1];
        ucolor2=[UIColor colorWithRed:130.0/255.0 green:130.0/255.0 blue:140.0/255 alpha:1];
    }
    
    CGColorRef color1=ucolor1.CGColor;
    CGColorRef color2=ucolor2.CGColor;
    
    CGGradientRef gradient;
    CGFloat locations[2] = { 0.0, 1.0 };
    NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    CGRect currentBounds = self.bounds;
    CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
    CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);

    //overall gradient
    
    CGContextSaveGState(context);
    ucolor1=[UIColor colorWithRed:0.1 green:0.1 blue:0.11 alpha:0];
    ucolor2=[UIColor colorWithRed:0.1 green:0.1 blue:0.11 alpha:0.4];
    
    color1=ucolor1.CGColor;
    color2=ucolor2.CGColor;
    
    colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    topCenter = CGPointMake(CGRectGetMidX(currentBounds), -(self.frame.origin.y));
    midCenter = CGPointMake(CGRectGetMidX(currentBounds), self.superview.bounds.size.height-(self.frame.origin.y));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);
    
    //bottom inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, -1), 1, [UIColor colorWithWhite:0 alpha:0.5].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //top inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 0, [UIColor colorWithWhite:1 alpha:1].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //border
    CGContextSetLineWidth(context, 1);
    [[UIColor colorWithWhite:0.0 alpha:0.4] setStroke];
    CGContextAddPath(context, keyBorderPath);
    CGContextStrokePath(context);

    CGPathRelease(keyBorderPath);

    CGContextRestoreGState(context);
}

Ok. This is how it looks like:


Way better. Let's draw the key symbol.

Our key will be capable of draw a text string or an image. One or the other, not both. And to mimic real general use keyboards we will use the supplied image as a draw mask: no color, only black (gray if key is disabled). To draw the text, we will use Core Text. And we will not forget to add the final touch of a white shadow to our symbol.

Let's see the code:

-(void)drawRect:(CGRect)rect{
    CGContextRef context=UIGraphicsGetCurrentContext();
    
    CGContextSaveGState(context);
    
    CGPathRef keyBorderPath=[self newPathForRoundedRect:CGRectInset(rect, 0.5, 0.5) radius:7];
    
    
    //gradient
    CGContextSaveGState(context);
    //UIColor *ucolor1=[UIColor colorWithRed:239.0/255.0 green:239.0/255.0 blue:241.0/255 alpha:1];
    UIColor *ucolor1=[UIColor colorWithRed:241.0/255.0 green:241.0/255.0 blue:243.0/255 alpha:1];
    //UIColor *ucolor2=[UIColor colorWithRed:211.0/255.0 green:211.0/255.0 blue:217.0/255 alpha:1];
    UIColor *ucolor2=[UIColor colorWithRed:223.0/255.0 green:223.0/255.0 blue:231.0/255 alpha:1];
    
    if (self.state & (UIControlStateHighlighted | UIControlStateSelected)) {
        ucolor1=[UIColor colorWithRed:179.0/255.0 green:179.0/255.0 blue:187.0/255 alpha:1];
        ucolor2=[UIColor colorWithRed:130.0/255.0 green:130.0/255.0 blue:140.0/255 alpha:1];
    }
    
    CGColorRef color1=ucolor1.CGColor;
    CGColorRef color2=ucolor2.CGColor;
    
    CGGradientRef gradient;
    CGFloat locations[2] = { 0.0, 1.0 };
    NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    CGRect currentBounds = self.bounds;
    CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
    CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);
    
    //overall gradient
    
    CGContextSaveGState(context);
    ucolor1=[UIColor colorWithRed:0.1 green:0.1 blue:0.11 alpha:0];
    ucolor2=[UIColor colorWithRed:0.1 green:0.1 blue:0.11 alpha:0.4];
    
    color1=ucolor1.CGColor;
    color2=ucolor2.CGColor;
    
    colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    topCenter = CGPointMake(CGRectGetMidX(currentBounds), -(self.frame.origin.y));
    midCenter = CGPointMake(CGRectGetMidX(currentBounds), self.superview.bounds.size.height-(self.frame.origin.y));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);
    
    //bottom inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, -1), 1, [UIColor colorWithWhite:0 alpha:0.5].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //top inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 0, [UIColor colorWithWhite:1 alpha:1].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //border
    CGContextSetLineWidth(context, 1);
    [[UIColor colorWithWhite:0.0 alpha:0.4] setStroke];
    CGContextAddPath(context, keyBorderPath);
    CGContextStrokePath(context);
    

    CGPathRelease(keyBorderPath);
    
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 1, [UIColor colorWithWhite:1 alpha:1].CGColor);
    CGContextTranslateCTM(context, 0, rect.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    if (_text) {
        //draw text
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        
        //    create font
        CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica"), 26, NULL);
        
        
        CGMutablePathRef path = CGPathCreateMutable();
        CGRect boundingBox = CTFontGetBoundingBox(font);
        
        //Get the position on the y axis
        float midHeight = rect.size.height / 2;
        midHeight -= boundingBox.size.height / 2;
        
        CGPathAddRect(path, NULL, CGRectMake(0, midHeight, rect.size.width, boundingBox.size.height));
        //CGPathAddRect(path, NULL, rect);
        
        // Initialize an attributed string.
        CFStringRef string = (__bridge_retained CFStringRef)self.text;//CFSTR("A");
        CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
        CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), string);
        
        //    create paragraph style and assign text alignment to it
        CTTextAlignment alignment = kCTCenterTextAlignment;
        CTParagraphStyleSetting _settings[] = {    {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment} };
        CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(_settings, sizeof(_settings) / sizeof(_settings[0]));
        
        //    set paragraph style attribute
        CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTParagraphStyleAttributeName, paragraphStyle);
        
        //    set font attribute
        CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTFontAttributeName, font);
        if (self.state & UIControlStateDisabled) {
            CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTForegroundColorAttributeName, [UIColor colorWithWhite:0.5 alpha:1].CGColor);
        }
        
        //    release paragraph style and font
        CFRelease(paragraphStyle);
        CFRelease(font);
        
        // Create the framesetter with the attributed string.
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
        CFRelease(attrString);
        
        // Create the frame and draw it into the graphics context
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter,
                                                    CFRangeMake(0, 0), path, NULL);
        CFRelease(framesetter);
        CTFrameDraw(frame, context);
        CFRelease(frame);
    }else if(_image){
        // use image as mask to draw the key symbol
        
        CGContextSaveGState(context);
        
        CGImageRef maskRef = _image.CGImage; 
        
        CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(maskRef),
                                            CGImageGetHeight(maskRef),
                                            CGImageGetBitsPerComponent(maskRef),
                                            CGImageGetBitsPerPixel(maskRef),
                                            CGImageGetBytesPerRow(maskRef),
                                            CGImageGetDataProvider(maskRef), NULL, false);
        
        CGRect maskRect=CGRectMake(0, 0, _image.size.width, _image.size.height);
        maskRect=CGRectOffset(maskRect, (rect.size.width-maskRect.size.width)/2.0, (rect.size.height-maskRect.size.height)/2.0);
        maskRect=CGRectIntegral(maskRect);
        CGRect shadowMaskRect=CGRectOffset(maskRect, 0, -0.5);
        
        CGContextClipToMask(context, shadowMaskRect, mask);
        
        [[UIColor colorWithWhite:1 alpha:0.8] setFill];
        
        CGContextFillRect(context, maskRect);
        
        CGContextRestoreGState(context);
        
        CGContextClipToMask(context, maskRect, mask);
        
        if (self.state & UIControlStateDisabled) {
            [[UIColor colorWithWhite:0.5 alpha:1] setFill];
        }else{
            [[UIColor blackColor] setFill];
        }
        
        
        CGContextFillRect(context, maskRect);
        
        CGImageRelease(mask);
    }
    
    
    
    CGContextRestoreGState(context);
}

Et voilà, this is what we get:


Maybe gray tones need a bit of adjustment, but really cool. There is only one thing left: play the system click sound every time a key is pressed.

Easy; from 'Text, Web, and Editing Programming Guide for iOS': "Starting in iOS 4.2, you can play standard system keyboard clicks when a user taps in your custom input views and keyboard accessory views. First, adopt the UIInputViewAudioFeedback protocol in your input view. Then, call the playInputClick method when responding to a key tap in the view."

In our example, we will place input clicks in our JACustomKeyboard class:

@interface JACustomKeyboard : UIView 

@end

@implementation JACustomKeyboard

-(void)processKeyStroke:(id)sender{
    [[UIDevice currentDevice] playInputClick];
}

#pragma mark UIInputViewAudioFeedback

- (BOOL) enableInputClicksWhenVisible {
    return YES;
}

@end

For every custom key in your keyboard add the custom keyboard as target with processKeyStroke: selector for UIControlEventTouchUpInside.

Update

As KKL noted, the code for -(CGRect) newPathForRoundedRect:(CGRect) rect radius:(CGFloat)radius selector is missing. This is it:

-(CGRect) newPathForRoundedRect:(CGRect) rect radius:(CGFloat)radius{
 CGMutablePathRef retPath = CGPathCreateMutable();
    
 CGRect innerRect = CGRectInset(rect, radius, radius);
    
 CGFloat inside_right = innerRect.origin.x + innerRect.size.width;
 CGFloat outside_right = rect.origin.x + rect.size.width;
 CGFloat inside_bottom = innerRect.origin.y + innerRect.size.height;
 CGFloat outside_bottom = rect.origin.y + rect.size.height;
    
 CGFloat inside_top = innerRect.origin.y;
 CGFloat outside_top = rect.origin.y;
 CGFloat outside_left = rect.origin.x;
    
 CGPathMoveToPoint(retPath, NULL, innerRect.origin.x, outside_top);
    
 CGPathAddLineToPoint(retPath, NULL, inside_right, outside_top);
 CGPathAddArcToPoint(retPath, NULL, outside_right, outside_top, outside_right, inside_top, radius);
 CGPathAddLineToPoint(retPath, NULL, outside_right, inside_bottom);
 CGPathAddArcToPoint(retPath, NULL,  outside_right, outside_bottom, inside_right, outside_bottom, radius);
    
 CGPathAddLineToPoint(retPath, NULL, innerRect.origin.x, outside_bottom);
 CGPathAddArcToPoint(retPath, NULL,  outside_left, outside_bottom, outside_left, inside_bottom, radius);
 CGPathAddLineToPoint(retPath, NULL, outside_left, inside_top);
 CGPathAddArcToPoint(retPath, NULL,  outside_left, outside_top, innerRect.origin.x, outside_top, radius);
    
 CGPathCloseSubpath(retPath);
    
 return retPath;
}