Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Speech Bubble in iOS SDK using Objective-C

I am working on a children's book app and would like to dynamically populate speech bubble(s) for character dialogues as shown in attached screenshots. Screenshots are from Cinderella app which was developed using Cocos2D (or 3D) I guess. However, I would like to achieve this functionality using objective-c and iOS sdk.

One approach would be:

Calculate the size of the text given a font and size (using the NSString method sizeWithFont:constrainedToSize:), then draw a rounded rectangle (using CALayer) to enclose that size.

For drawing rounded rectangle, set up a CALayer with a background color, border width and border color and color radius, and add that to a UIView object. That would give a speech bubble effect very easily.

Now, how would I get a corner that pointed to the character's mouth? How do I pop-out and pop-in the CALayer from character's mouth, how would I implement this animation? My main scene (illustration) would be an UIImageview and this dialogue pop-op should appear from character's mouth in a animated way and after few seconds it should disappear as if its going back in character's mouth. You suggestions would be greatly appreciated.

If you are aware of some sample code/article some place, please route me accordingly.

Here is a video link of the app which shows how character dialogues pop-in and out as speech bubble: http://www.youtube.com/watch?v=umfNJLyrrNg

enter image description here

like image 358
Alex Avatar asked Jun 25 '12 19:06

Alex


3 Answers

here is my implementation by modifying this apple tutorial:

https://developer.apple.com/library/ios/samplecode/quartzdemo/Listings/QuartzDemo_QuartzCurves_m.html

- (void)drawRect:(CGRect)rect
{
    CGContextRef context=UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, .5f);
    CGContextSetStrokeColorWithColor(context, [UIColor lightGrayColor].CGColor);
    CGContextSetRGBFillColor(context, 1, 1, 1, 1);

    rect.origin.y++;
    CGFloat radius = cornerRadius;

    CGFloat minx = CGRectGetMinX(rect), midx = CGRectGetMidX(rect), maxx = CGRectGetMaxX(rect);
    CGFloat miny = CGRectGetMinY(rect), midy = CGRectGetMidY(rect), maxy = CGRectGetMaxY(rect);

    CGMutablePathRef outlinePath = CGPathCreateMutable();

    if (![User isCurrentUser:message.user])
    {
        minx += 5;
    
        CGPathMoveToPoint(outlinePath, nil, midx, miny);
        CGPathAddArcToPoint(outlinePath, nil, maxx, miny, maxx, midy, radius);
        CGPathAddArcToPoint(outlinePath, nil, maxx, maxy, midx, maxy, radius);
        CGPathAddArcToPoint(outlinePath, nil, minx, maxy, minx, midy, radius);
        CGPathAddLineToPoint(outlinePath, nil, minx, miny + 20);
        CGPathAddLineToPoint(outlinePath, nil, minx - 5, miny + 15);
        CGPathAddLineToPoint(outlinePath, nil, minx, miny + 10);
        CGPathAddArcToPoint(outlinePath, nil, minx, miny, midx, miny, radius);
        CGPathCloseSubpath(outlinePath);
    
        CGContextSetShadowWithColor(context, CGSizeMake(0,1), 1, [UIColor lightGrayColor].CGColor);
        CGContextAddPath(context, outlinePath);
        CGContextFillPath(context);
    
        CGContextAddPath(context, outlinePath);
        CGContextClip(context);
        CGPoint start = CGPointMake(rect.origin.x, rect.origin.y);
        CGPoint end = CGPointMake(rect.origin.x, rect.size.height);
        CGContextDrawLinearGradient(context, [self normalGradient], start, end, 0);
    }
    else
    {
        maxx-=5;
        CGPathMoveToPoint(outlinePath, nil, midx, miny);
        CGPathAddArcToPoint(outlinePath, nil, minx, miny, minx, midy, radius);
        CGPathAddArcToPoint(outlinePath, nil, minx, maxy, midx, maxy, radius);
        CGPathAddArcToPoint(outlinePath, nil, maxx, maxy, maxx, midy, radius);
        CGPathAddLineToPoint(outlinePath, nil, maxx, miny + 20);
        CGPathAddLineToPoint(outlinePath, nil, maxx + 5, miny + 15);
        CGPathAddLineToPoint(outlinePath, nil, maxx, miny + 10);
        CGPathAddArcToPoint(outlinePath, nil, maxx, miny, midx, miny, radius);
        CGPathCloseSubpath(outlinePath);
    
        CGContextSetShadowWithColor(context, CGSizeMake(0,1), 1, [UIColor lightGrayColor].CGColor);
        CGContextAddPath(context, outlinePath);
        CGContextFillPath(context);
    
        CGContextAddPath(context, outlinePath);
        CGContextClip(context);
        CGPoint start = CGPointMake(rect.origin.x, rect.origin.y);
        CGPoint end = CGPointMake(rect.origin.x, rect.size.height);
        CGContextDrawLinearGradient(context, [self greenGradient], start, end, 0);
    }


    CGContextDrawPath(context, kCGPathFillStroke);

}

- (CGGradientRef)normalGradient
{

    NSMutableArray *normalGradientLocations = [NSMutableArray arrayWithObjects:
                                               [NSNumber numberWithFloat:0.0f],
                                               [NSNumber numberWithFloat:1.0f],
                                               nil];


    NSMutableArray *colors = [NSMutableArray arrayWithCapacity:2];

    UIColor *color = [UIColor whiteColor];
    [colors addObject:(id)[color CGColor]];
    color = [StyleView lightColorFromColor:[UIColor cloudsColor]];
    [colors addObject:(id)[color CGColor]];
    NSMutableArray  *normalGradientColors = colors;

    int locCount = [normalGradientLocations count];
    CGFloat locations[locCount];
    for (int i = 0; i < [normalGradientLocations count]; i++)
    {
        NSNumber *location = [normalGradientLocations objectAtIndex:i];
        locations[i] = [location floatValue];
    }
    CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();

    CGGradientRef normalGradient = CGGradientCreateWithColors(space, (__bridge CFArrayRef)normalGradientColors, locations);
    CGColorSpaceRelease(space);

    return normalGradient;
}

Which results in this... enter image description here

like image 187
Misha Avatar answered Nov 10 '22 00:11

Misha


Another way of doing this is through the use of images with cap insets. Take a look at the UIImage method:

resizableImageWithCapInsets:

Basically you can create an image of a minimum sized bubble, and have it stretch indefinitely while maintaining the look of the 'bubble' borders.

The code below specifies an image where 12 pixels on the top, left, bottom and right be preserved when stretching/resizing the image:

UIImage *bubble = [[UIImage imageNamed:@"bubble.png"] 
                            resizableImageWithCapInsets:UIEdgeInsetsMake(12, 12, 12, 12)];
UIImageView *imgView = [[[UIImageView alloc] initWithImage:bubble] autorelease];
imgView.frame = CGRectZero;

Animating the size change of a UIImageView comes for free:

[UIView animateWithDuration:0.3
                 animations:^(void) {
                     imgView.frame = CGRectMake(50, 50, 200, 100);
                 } completion:^(BOOL finished) {

                 }];
like image 26
hundreth Avatar answered Nov 09 '22 22:11

hundreth


The three20 framework (actually, Three20Style) has got a shaped window class called TTSpeechBubbleShape, which is basically what you are looking for.

Now, you have two options, as I see it:

  1. either use Three20; or

  2. analyze the TTSpeechBubbleShape class to see how this is implemented and do the same in your app.

like image 3
sergio Avatar answered Nov 09 '22 23:11

sergio